Compare commits

...

11 Commits

Author SHA1 Message Date
0967bab53d rename: FAMILY_CONVENTIONS → HERMES_AGENT_CONVENTIONS
[小糯]
2026-04-23 18:41:18 +08:00
559913466a refactor(personality): CONVENTIONS 改从 cfg 读取,不再依赖本地文件
- loadConventions() 从 cfg get FAMILY_CONVENTIONS 读取(shared scope)
- 改一处即所有 agent 即时可用,无需 git pull + sync
- 移除 CONVENTIONS_PATH 文件依赖

[小糯]
2026-04-23 18:37:30 +08:00
c5559b134d feat(personality): switch 时自动更新 SOUL.md,append CONVENTIONS.md
切换人格时同时写 SOUL.md = personality prompt + ~/.hermes/CONVENTIONS.md。
清除人格时 SOUL.md 只保留 conventions 内容。
CONVENTIONS.md 由 skills repo sync.sh 维护,集中管理家族共识。

[小糯]
2026-04-23 18:29:38 +08:00
69d9c69358 fix: correct package name to @shazhou/hermes-harness — 小糯 2026-04-21 11:35:17 +08:00
96156cef27 chore: bump version to 0.1.1 — 小糯 2026-04-21 11:34:20 +08:00
5b2d33411e Merge pull request 'fix: auxiliary switch reads provider from cfg registry' (#3) from fix/models-review-feedback into main 2026-04-21 03:32:52 +00:00
8074e4cfc3 fix: auxiliary switch reads provider from cfg registry
Auxiliary model switching now looks up provider details from cfg
registry (base_url, api_key) and adds custom: prefix automatically,
matching the behavior of main provider switching.

— 小糯
2026-04-21 11:27:12 +08:00
d736d43f72 feat: read providers from cfg registry instead of config.yaml
- providers/list/test now read from cfg (HERMES_CUSTOM_PROVIDERS)
- switch writes target provider into config.yaml custom_providers and restarts gateway
- standard providers (openrouter, anthropic) clear custom_providers on switch
- fallback to config.yaml if cfg unavailable

Signed-off-by: Xiaonuo <xiaonuo@shazhou.work>
2026-04-21 11:07:31 +08:00
f1c65b2c1b Merge pull request 'fix: models review feedback' (#2) from fix/models-review-feedback into main 2026-04-21 02:50:37 +00:00
a11bb53539 fix: address review feedback from PR #1
1. switch 后提示重启 gateway 以生效
2. auxiliary tasks 从 config.yaml 动态读取,不再硬编码
3. Telegram 删消息加 3s delay,确保 hermes 先处理
4. api_key_env 已在前一个 commit 修复

Ref: PR #1 review by tuanzi
Signed-off-by: Xiaonuo <xiaonuo@shazhou.work>
2026-04-21 10:50:18 +08:00
b6ee3e071f Merge pull request 'feat: models command' (#1) from feat/models-command into main 2026-04-21 02:42:02 +00:00
3 changed files with 165 additions and 45 deletions

View File

@ -1,6 +1,6 @@
{ {
"name": "hermes-harness", "name": "@shazhou/hermes-harness",
"version": "0.1.0", "version": "0.1.1",
"description": "Hermes Agent CLI harness tools", "description": "Hermes Agent CLI harness tools",
"type": "module", "type": "module",
"bin": { "bin": {

View File

@ -1,10 +1,12 @@
import { readFileSync, writeFileSync, existsSync } from "fs"; import { readFileSync, writeFileSync, existsSync } from "fs";
import { execSync } from "child_process";
import { homedir } from "os"; import { homedir } from "os";
import { join } from "path"; import { join } from "path";
import { parse, stringify } from "yaml"; import { parse, stringify } from "yaml";
const CONFIG_PATH = join(homedir(), ".hermes", "config.yaml"); const CONFIG_PATH = join(homedir(), ".hermes", "config.yaml");
const ENV_PATH = join(homedir(), ".hermes", ".env"); const ENV_PATH = join(homedir(), ".hermes", ".env");
const CFG_PROVIDERS_KEY = "HERMES_CUSTOM_PROVIDERS";
// ── Types ────────────────────────────────────────────────────────────── // ── Types ──────────────────────────────────────────────────────────────
@ -36,7 +38,7 @@ interface Config {
[key: string]: unknown; [key: string]: unknown;
} }
const AUXILIARY_TASKS = [ const DEFAULT_AUXILIARY_TASKS = [
"vision", "vision",
"web_extract", "web_extract",
"compression", "compression",
@ -47,6 +49,18 @@ const AUXILIARY_TASKS = [
"approval", "approval",
]; ];
function getAuxiliaryTasks(): string[] {
try {
const config = loadConfig();
const aux = config.auxiliary || {};
const tasks = Object.keys(aux).filter(
(k) => typeof aux[k] === "object" && aux[k]?.provider !== undefined
);
if (tasks.length > 0) return tasks;
} catch {}
return DEFAULT_AUXILIARY_TASKS;
}
// ── Helpers ──────────────────────────────────────────────────────────── // ── Helpers ────────────────────────────────────────────────────────────
function loadConfig(): Config { function loadConfig(): Config {
@ -60,10 +74,43 @@ function saveConfig(config: Config) {
writeFileSync(CONFIG_PATH, stringify(config, { lineWidth: 120 }), "utf-8"); writeFileSync(CONFIG_PATH, stringify(config, { lineWidth: 120 }), "utf-8");
} }
/**
* Read all available providers from cfg (central registry).
* Falls back to config.yaml custom_providers if cfg unavailable.
*/
function getCfgProviders(): CustomProvider[] {
try {
const raw = execSync(`cfg get ${CFG_PROVIDERS_KEY}`, {
encoding: "utf-8",
timeout: 5000,
}).trim();
if (!raw) return [];
return JSON.parse(raw) as CustomProvider[];
} catch {
return [];
}
}
/**
* Get providers from cfg (authoritative source).
* The config.yaml custom_providers is now only a "currently active" slot.
*/
function getAllProviders(): CustomProvider[] {
const cfgProviders = getCfgProviders();
if (cfgProviders.length > 0) return cfgProviders;
// Fallback to config.yaml if cfg has nothing
const config = loadConfig();
return config.custom_providers || [];
}
function getCustomProviders(config: Config): CustomProvider[] { function getCustomProviders(config: Config): CustomProvider[] {
return config.custom_providers || []; return config.custom_providers || [];
} }
function findProviderFromRegistry(name: string): CustomProvider | undefined {
return getAllProviders().find((p) => p.name === name);
}
function findProvider( function findProvider(
config: Config, config: Config,
name: string name: string
@ -111,42 +158,41 @@ function getAuthHeaders(provider: CustomProvider): Record<string, string> {
function providers() { function providers() {
const config = loadConfig(); const config = loadConfig();
const customProviders = getCustomProviders(config); const allProviders = getAllProviders();
const mainProvider = config.model?.provider || "auto"; const mainProvider = config.model?.provider || "auto";
const mainModel = config.model?.default || "(not set)"; const mainModel = config.model?.default || "(not set)";
console.log(`Main: provider=${mainProvider} model=${mainModel}\n`); console.log(`Main: provider=${mainProvider} model=${mainModel}\n`);
if (customProviders.length === 0) { if (allProviders.length === 0) {
console.log("No custom providers configured."); console.log("No custom providers configured.");
return; return;
} }
console.log("Custom providers:\n"); console.log("Custom providers (from cfg):\n");
for (const p of customProviders) { for (const p of allProviders) {
const active = p.name === mainProvider || `custom:${p.name}` === mainProvider ? " ◀ active" : ""; const active = p.name === mainProvider || `custom:${p.name}` === mainProvider ? " ◀ active" : "";
console.log(` ${p.name}${active}`); console.log(` ${p.name}${active}`);
console.log(` url: ${p.base_url}`); console.log(` url: ${p.base_url}`);
if (p.api_mode) console.log(` mode: ${p.api_mode}`); if (p.api_mode) console.log(` mode: ${p.api_mode}`);
} }
console.log(`\nTotal: ${customProviders.length}`); console.log(`\nTotal: ${allProviders.length}`);
} }
// ── list ──────────────────────────────────────────────────────────────── // ── list ────────────────────────────────────────────────────────────────
async function list(providerName?: string) { async function list(providerName?: string) {
const config = loadConfig(); const allProviders = getAllProviders();
const customProviders = getCustomProviders(config);
const targets: CustomProvider[] = providerName const targets: CustomProvider[] = providerName
? customProviders.filter((p) => p.name === providerName) ? allProviders.filter((p) => p.name === providerName)
: customProviders; : allProviders;
if (targets.length === 0) { if (targets.length === 0) {
if (providerName) { if (providerName) {
console.error(`Provider not found: ${providerName}`); console.error(`Provider not found: ${providerName}`);
console.error( console.error(
`Available: ${customProviders.map((p) => p.name).join(", ")}` `Available: ${allProviders.map((p) => p.name).join(", ")}`
); );
} else { } else {
console.log("No custom providers configured."); console.log("No custom providers configured.");
@ -208,12 +254,11 @@ async function list(providerName?: string) {
// ── test ──────────────────────────────────────────────────────────────── // ── test ────────────────────────────────────────────────────────────────
async function test(providerName?: string) { async function test(providerName?: string) {
const config = loadConfig(); const allProviders = getAllProviders();
const customProviders = getCustomProviders(config);
const targets: CustomProvider[] = providerName const targets: CustomProvider[] = providerName
? customProviders.filter((p) => p.name === providerName) ? allProviders.filter((p) => p.name === providerName)
: customProviders; : allProviders;
if (targets.length === 0) { if (targets.length === 0) {
if (providerName) { if (providerName) {
@ -299,32 +344,74 @@ function switchConfig(opts: SwitchOptions) {
// Switch auxiliary tasks // Switch auxiliary tasks
if (!config.auxiliary) config.auxiliary = {} as Record<string, AuxiliaryEntry>; if (!config.auxiliary) config.auxiliary = {} as Record<string, AuxiliaryEntry>;
// Look up provider from cfg registry to get base_url & api_key
const registryProvider = findProviderFromRegistry(opts.provider);
const providerValue = registryProvider ? `custom:${registryProvider.name}` : opts.provider;
// Ensure custom_providers in config.yaml contains this provider
if (registryProvider) {
if (!config.custom_providers) config.custom_providers = [];
const existing = config.custom_providers.find((p) => p.name === registryProvider.name);
if (!existing) {
config.custom_providers.push(registryProvider);
}
}
for (const task of opts.auxTasks) { for (const task of opts.auxTasks) {
if (!config.auxiliary[task]) { if (!config.auxiliary[task]) {
config.auxiliary[task] = { provider: "", model: "", timeout: 30 }; config.auxiliary[task] = { provider: "", model: "", timeout: 30 };
} }
config.auxiliary[task].provider = opts.provider; config.auxiliary[task].provider = providerValue;
if (registryProvider) {
config.auxiliary[task].base_url = registryProvider.base_url;
const apiKey = getProviderApiKey(registryProvider);
if (apiKey) config.auxiliary[task].api_key = apiKey;
}
if (opts.model) { if (opts.model) {
config.auxiliary[task].model = opts.model; config.auxiliary[task].model = opts.model;
} }
console.log(` auxiliary.${task} → provider=${opts.provider}${opts.model ? ` model=${opts.model}` : ""}`); console.log(` auxiliary.${task} → provider=${providerValue}${opts.model ? ` model=${opts.model}` : ""}`);
} }
} else { } else {
// Switch main provider // Switch main provider — look up from cfg registry
const providerValue = findProvider(config, opts.provider) const registryProvider = findProviderFromRegistry(opts.provider);
? `custom:${opts.provider}`
: opts.provider;
if (registryProvider) {
// Write this provider into config.yaml custom_providers (replace all)
config.custom_providers = [registryProvider];
const providerValue = `custom:${registryProvider.name}`;
if (!config.model) config.model = {}; if (!config.model) config.model = {};
config.model.provider = providerValue; config.model.provider = providerValue;
if (opts.model) { if (opts.model) {
config.model.default = opts.model; config.model.default = opts.model;
} }
console.log(` main → provider=${providerValue}${opts.model ? ` model=${opts.model}` : ""}`); console.log(` main → provider=${providerValue}${opts.model ? ` model=${opts.model}` : ""}`);
console.log(` config.yaml custom_providers updated with ${registryProvider.name}`);
} else {
// Standard provider (openrouter, anthropic, etc.)
if (!config.model) config.model = {};
config.model.provider = opts.provider;
if (opts.model) {
config.model.default = opts.model;
}
// Clear custom_providers since switching to standard
delete config.custom_providers;
console.log(` main → provider=${opts.provider}${opts.model ? ` model=${opts.model}` : ""}`);
}
} }
saveConfig(config); saveConfig(config);
console.log("\nConfig saved. New sessions will use the updated settings.");
// Restart gateway
try {
execSync("systemctl --user restart hermes-gateway 2>/dev/null || true", {
encoding: "utf-8",
timeout: 10000,
});
console.log("\nConfig saved & gateway restarted.");
} catch {
console.log("\nConfig saved. Restart gateway manually to apply.");
}
} }
async function switchTelegram(opts: SwitchOptions) { async function switchTelegram(opts: SwitchOptions) {
@ -386,16 +473,19 @@ async function switchTelegram(opts: SwitchOptions) {
if (result.ok) { if (result.ok) {
console.log(`Sent: ${command}`); console.log(`Sent: ${command}`);
console.log(opts.global ? "Persistent change (--global)." : "Current session only."); console.log(opts.global ? "Persistent change (--global)." : "Current session only.");
// Clean up the command message // Clean up the command message after a delay so hermes can process it
if (result.result?.message_id) { if (result.result?.message_id) {
const msgId = result.result.message_id;
setTimeout(async () => {
await fetch(`https://api.telegram.org/bot${token}/deleteMessage`, { await fetch(`https://api.telegram.org/bot${token}/deleteMessage`, {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify({ body: JSON.stringify({
chat_id: chatId, chat_id: chatId,
message_id: result.result.message_id, message_id: msgId,
}), }),
}).catch(() => {}); }).catch(() => {});
}, 3000);
} }
} else { } else {
console.error(`Telegram API error: ${result.description}`); console.error(`Telegram API error: ${result.description}`);
@ -476,9 +566,10 @@ export async function models(args: string[]) {
i++; i++;
} else if (args[i] === "--aux" && i + 1 < args.length) { } else if (args[i] === "--aux" && i + 1 < args.length) {
const task = args[i + 1]; const task = args[i + 1];
if (!AUXILIARY_TASKS.includes(task)) { const knownTasks = getAuxiliaryTasks();
if (!knownTasks.includes(task)) {
console.error(`Unknown auxiliary task: ${task}`); console.error(`Unknown auxiliary task: ${task}`);
console.error(`Available: ${AUXILIARY_TASKS.join(", ")}`); console.error(`Available: ${knownTasks.join(", ")}`);
process.exit(1); process.exit(1);
} }
auxTasks.push(task); auxTasks.push(task);

View File

@ -3,7 +3,9 @@ import { homedir } from "os";
import { join } from "path"; import { join } from "path";
import { parse, stringify } from "yaml"; import { parse, stringify } from "yaml";
const CONFIG_PATH = join(homedir(), ".hermes", "config.yaml"); const HERMES_HOME = process.env.HERMES_HOME || join(homedir(), ".hermes");
const CONFIG_PATH = join(HERMES_HOME, "config.yaml");
const SOUL_PATH = join(HERMES_HOME, "SOUL.md");
interface Config { interface Config {
agent?: { agent?: {
@ -51,6 +53,30 @@ function getPreview(value: string | PersonalityDict): string {
return value.description || (value.system_prompt || "").slice(0, 60) + "..."; return value.description || (value.system_prompt || "").slice(0, 60) + "...";
} }
// --- SOUL.md management ---
function loadConventions(): string {
try {
const { execSync } = require("child_process");
const result = execSync("cfg get HERMES_AGENT_CONVENTIONS", {
encoding: "utf-8",
timeout: 5000,
stdio: ["pipe", "pipe", "pipe"],
}).trim();
return result || "";
} catch {
return "";
}
}
function writeSoulMd(personalityPrompt: string) {
const conventions = loadConventions();
const parts = [personalityPrompt];
if (conventions) parts.push(conventions);
const content = parts.filter(Boolean).join("\n\n");
writeFileSync(SOUL_PATH, content + "\n", "utf-8");
}
// --- Actions --- // --- Actions ---
function list() { function list() {
@ -126,6 +152,7 @@ function switchLocal(name: string) {
if (!config.agent) config.agent = {}; if (!config.agent) config.agent = {};
config.agent.system_prompt = ""; config.agent.system_prompt = "";
saveConfig(config); saveConfig(config);
writeSoulMd("");
console.log("Personality cleared."); console.log("Personality cleared.");
return; return;
} }
@ -140,7 +167,9 @@ function switchLocal(name: string) {
if (!config.agent) config.agent = {}; if (!config.agent) config.agent = {};
config.agent.system_prompt = prompt; config.agent.system_prompt = prompt;
saveConfig(config); saveConfig(config);
writeSoulMd(prompt);
console.log(`Switched to: ${name}`); console.log(`Switched to: ${name}`);
console.log("SOUL.md updated.");
} }
async function switchTelegram(name: string) { async function switchTelegram(name: string) {