From 26c834dab6506098d10c3ac05ad7a54160ca8890 Mon Sep 17 00:00:00 2001 From: "lisheng.lisheng" Date: Mon, 12 Oct 2026 17:28:28 +0800 Subject: [PATCH 1/2] fix(config): resolve Hermes home via HERMES_HOME and platform default Hermes resolves its home as HERMES_HOME -> platform-native default (%LOCALAPPDATA%\hermes on Windows, ~/.hermes elsewhere). The config writer, inventory probe, detail extraction and skill roots all hard-coded ~/.hermes, so on Windows or any customized HERMES_HOME the config was written where Hermes never reads it and the command silently no-oped. Add a shared core helper getHermesHome() mirroring hermes-agent resolution and use it uniformly across the writer, inventory and skill fan-out. Fixes #131 --- .../commands/config/agent/writers/hermes.ts | 6 +- .../commands/src/commands/config/inventory.ts | 9 ++- .../tests/config-agent-writers.test.ts | 38 +++++++++- packages/commands/tests/inventory.test.ts | 32 ++++++++ packages/core/src/skills/agents.ts | 5 +- packages/core/src/utils/agent-home.ts | 39 ++++++++++ packages/core/src/utils/index.ts | 1 + packages/core/tests/agent-home.test.ts | 73 +++++++++++++++++++ 8 files changed, 195 insertions(+), 8 deletions(-) create mode 100644 packages/core/src/utils/agent-home.ts create mode 100644 packages/core/tests/agent-home.test.ts diff --git a/packages/commands/src/commands/config/agent/writers/hermes.ts b/packages/commands/src/commands/config/agent/writers/hermes.ts index 73ae5194a..2b60908cb 100644 --- a/packages/commands/src/commands/config/agent/writers/hermes.ts +++ b/packages/commands/src/commands/config/agent/writers/hermes.ts @@ -1,13 +1,15 @@ -import { homedir } from "os"; import { join } from "path"; import { existsSync, readFileSync } from "fs"; import yaml from "yaml"; +import { getHermesHome } from "bailian-cli-core"; import { backup, writeTextAtomic, isAnthropicEndpoint, type AgentDef } from "./utils.ts"; export default { label: "Hermes Agent", write({ baseUrl, apiKey, model }) { - const configPath = join(homedir(), ".hermes", "config.yaml"); + // Honor HERMES_HOME / the platform-native default so the config lands where + // Hermes actually reads it (see core getHermesHome); never hard-code ~/.hermes. + const configPath = join(getHermesHome(), "config.yaml"); backup(configPath); diff --git a/packages/commands/src/commands/config/inventory.ts b/packages/commands/src/commands/config/inventory.ts index 3c771d2be..97013d0de 100644 --- a/packages/commands/src/commands/config/inventory.ts +++ b/packages/commands/src/commands/config/inventory.ts @@ -19,6 +19,7 @@ import { import { inflateRawSync } from "node:zlib"; import yaml from "yaml"; import { parse as parseToml } from "smol-toml"; +import { getHermesHome } from "bailian-cli-core"; import { qwenworkMcpPath, workbuddyMcpPaths } from "../mcp/agent-config.ts"; /** @@ -143,7 +144,7 @@ function skillRoots(home: string): Array<{ source: string; dir: string }> { { source: "opencode", dir: join(home, ".config", "opencode", "skills") }, { source: "openclaw", dir: join(home, ".openclaw", "skills") }, { source: "openclaw", dir: join(home, ".openclaw", "workspace", "skills") }, - { source: "hermes", dir: join(home, ".hermes", "skills") }, + { source: "hermes", dir: join(getHermesHome(home), "skills") }, { source: "gemini", dir: join(home, ".gemini", "skills") }, { source: "antigravity", @@ -857,9 +858,9 @@ const AGENT_PROBES: AgentProbe[] = [ { id: "hermes", label: "Hermes Agent", - paths: (h) => [join(h, ".hermes", "config.yaml")], + paths: (h) => [join(getHermesHome(h), "config.yaml")], detect: (h) => { - const text = readText(join(h, ".hermes", "config.yaml")); + const text = readText(join(getHermesHome(h), "config.yaml")); if (!text) return { configured: false }; let config: Record | undefined; try { @@ -1000,7 +1001,7 @@ function agentDetailFields(id: string, home: string): AgentField[] { pushField(out, "API", bailian?.api); pushField(out, "API Key", bailian?.apiKey, true); } else if (id === "hermes") { - const text = readText(join(home, ".hermes", "config.yaml")); + const text = readText(join(getHermesHome(home), "config.yaml")); let config: Record | undefined; if (text) { try { diff --git a/packages/commands/tests/config-agent-writers.test.ts b/packages/commands/tests/config-agent-writers.test.ts index a08becd4a..52eae9eef 100644 --- a/packages/commands/tests/config-agent-writers.test.ts +++ b/packages/commands/tests/config-agent-writers.test.ts @@ -1,4 +1,12 @@ -import { mkdtempSync, rmSync, readFileSync, writeFileSync, mkdirSync, readdirSync } from "fs"; +import { + mkdtempSync, + rmSync, + readFileSync, + writeFileSync, + mkdirSync, + readdirSync, + existsSync, +} from "fs"; import { tmpdir, homedir } from "os"; import { join } from "path"; import { afterEach, beforeEach, describe, expect, test } from "vite-plus/test"; @@ -500,6 +508,34 @@ describe("config agent writers", () => { }); }); + test("hermes 尊重 HERMES_HOME 环境变量,写入自定义 home 而非 ~/.hermes", () => { + const customHome = join(home, "custom-hermes-home"); + mkdirSync(customHome, { recursive: true }); + const prevHermesHome = process.env.HERMES_HOME; + process.env.HERMES_HOME = customHome; + try { + const summary = hermes.write({ + baseUrl: OAI_URL, + apiKey: "sk-h", + model: "qwen3-coder-plus", + }); + const expectedPath = join(customHome, "config.yaml"); + // 写入位置与返回的 paths 都必须是 $HERMES_HOME/config.yaml + expect(summary.paths).toEqual([expectedPath]); + const config = yaml.parse(readFileSync(expectedPath, "utf8")); + expect(config.model).toMatchObject({ + default: "qwen3-coder-plus", + provider: "custom", + base_url: OAI_URL, + }); + // 不再写入硬编码的 ~/.hermes/config.yaml(否则 Hermes 读不到,静默失效) + expect(existsSync(join(home, ".hermes", "config.yaml"))).toBe(false); + } finally { + if (prevHermesHome === undefined) delete process.env.HERMES_HOME; + else process.env.HERMES_HOME = prevHermesHome; + } + }); + test("codex 写入 config.toml 与 auth.json(官方 env_key 结构,合并保留)", () => { // 预置 config.toml 无关顶层键与另一个 provider,验证非破坏性合并 mkdirSync(join(home, ".codex"), { recursive: true }); diff --git a/packages/commands/tests/inventory.test.ts b/packages/commands/tests/inventory.test.ts index 0fa28a783..ed37168eb 100644 --- a/packages/commands/tests/inventory.test.ts +++ b/packages/commands/tests/inventory.test.ts @@ -240,3 +240,35 @@ test("listAgents 报告安装与已连接 bailian-cli 的状态", () => { expect(agents).toHaveLength(6); }); }); + +test("listAgents 通过 HERMES_HOME 探测 relocated 的 hermes 配置", () => { + const prevHermesHome = process.env.HERMES_HOME; + withHome((home) => { + // Hermes home relocated away from ~/.hermes via HERMES_HOME. + const hermesHome = join(home, "relocated-hermes"); + process.env.HERMES_HOME = hermesHome; + write( + hermesHome, + "config.yaml", + [ + "model:", + " default: qwen3-max", + " provider: custom", + " base_url: https://dashscope.aliyuncs.com/compatible-mode/v1", + " api_key: sk-test", + ].join("\n"), + ); + try { + const byId = Object.fromEntries(listAgents(home).map((a) => [a.id, a])); + // Probe must follow HERMES_HOME, not the hard-coded ~/.hermes (issue #131). + expect(byId.hermes).toMatchObject({ + installed: true, + configured: true, + model: "qwen3-max", + }); + } finally { + if (prevHermesHome === undefined) delete process.env.HERMES_HOME; + else process.env.HERMES_HOME = prevHermesHome; + } + }); +}); diff --git a/packages/core/src/skills/agents.ts b/packages/core/src/skills/agents.ts index 79ebe392f..393938324 100644 --- a/packages/core/src/skills/agents.ts +++ b/packages/core/src/skills/agents.ts @@ -10,6 +10,7 @@ import { import { homedir } from "node:os"; import { dirname, isAbsolute, join, resolve, sep } from "node:path"; import { getSkillsDir } from "./lock.ts"; +import { getHermesHome } from "../utils/agent-home.ts"; /** * Agent fan-out: after a skill lands in the canonical dir (~/.bailian/skills/), @@ -164,7 +165,9 @@ export function getAgentTargets(): AgentTarget[] { detectDirs: [join(xdgConfig, "goose")], }, fromBase("grok", "Grok Build", envBase(process.env.GROK_HOME, ".grok")), - fromBase("hermes", "Hermes Agent", envBase(process.env.HERMES_HOME, ".hermes")), + // Hermes resolves its home via HERMES_HOME → platform-native default; share the + // canonical resolver so skill fan-out, the config writer and the inventory probe agree. + fromBase("hermes", "Hermes Agent", getHermesHome(home)), simple("iflow-cli", "iFlow CLI", ".iflow"), simple("inference-sh", "inference.sh", ".inferencesh"), { diff --git a/packages/core/src/utils/agent-home.ts b/packages/core/src/utils/agent-home.ts new file mode 100644 index 000000000..b2306ad4c --- /dev/null +++ b/packages/core/src/utils/agent-home.ts @@ -0,0 +1,39 @@ +import { homedir } from "os"; +import { join } from "path"; + +/** + * Resolve the Hermes Agent home directory the same way Hermes itself does + * (hermes-agent `hermes_constants.get_hermes_home` → `get_process_hermes_home`): + * + * 1. `HERMES_HOME` env var (trimmed, leading `~` expanded), when set; + * 2. otherwise the platform-native default: + * - Windows: `%LOCALAPPDATA%\hermes` (falling back to `~/AppData/Local/hermes`); + * - Linux/macOS: `~/.hermes`. + * + * Every code path that touches Hermes config — the writer, the inventory probe, + * detail extraction and skill roots — must resolve through this helper. + * Hard-coding `~/.hermes` silently writes config to a place Hermes never reads + * when `HERMES_HOME` is customized or on native Windows, so the command reports + * success while the model/provider change never takes effect. + * + * `home` overrides the user home used for the platform fallback (defaults to + * `homedir()`), so callers and tests can inject a temporary home. + */ +export function getHermesHome(home: string = homedir()): string { + const fromEnv = process.env.HERMES_HOME?.trim(); + if (fromEnv) return expandUserHome(fromEnv); + if (process.platform === "win32") { + const localAppData = process.env.LOCALAPPDATA?.trim(); + return localAppData ? join(localAppData, "hermes") : join(home, "AppData", "Local", "hermes"); + } + return join(home, ".hermes"); +} + +/** Expand a leading `~` (bare, or `~/` / `~\`) into the user's home directory. */ +function expandUserHome(value: string): string { + if (value === "~") return homedir(); + if (value.startsWith("~/") || value.startsWith("~\\")) { + return join(homedir(), value.slice(2)); + } + return value; +} diff --git a/packages/core/src/utils/index.ts b/packages/core/src/utils/index.ts index 38943a060..d487d9fbb 100644 --- a/packages/core/src/utils/index.ts +++ b/packages/core/src/utils/index.ts @@ -1,4 +1,5 @@ export { generateFilename } from "./filename.ts"; +export { getHermesHome } from "./agent-home.ts"; export { resolveOutputDir } from "./output-dir.ts"; export { maskToken } from "./token.ts"; export { stripUndefined } from "./object.ts"; diff --git a/packages/core/tests/agent-home.test.ts b/packages/core/tests/agent-home.test.ts new file mode 100644 index 000000000..9425805cf --- /dev/null +++ b/packages/core/tests/agent-home.test.ts @@ -0,0 +1,73 @@ +import { afterEach, describe, expect, test } from "vite-plus/test"; +import { join } from "node:path"; +import { getHermesHome } from "../src/utils/agent-home.ts"; + +/** + * getHermesHome must mirror hermes-agent's own resolution: + * HERMES_HOME env var (expanded) → platform-native default. + * A wrong fallback makes `bl config agent --agent hermes` write config Hermes + * never reads (see issue #131). + */ + +const originalPlatform = process.platform; +const savedEnv = { + HERMES_HOME: process.env.HERMES_HOME, + LOCALAPPDATA: process.env.LOCALAPPDATA, +}; + +function setPlatform(value: NodeJS.Platform): void { + Object.defineProperty(process, "platform", { value, configurable: true }); +} + +afterEach(() => { + setPlatform(originalPlatform); + for (const [key, value] of Object.entries(savedEnv)) { + if (value === undefined) delete process.env[key]; + else process.env[key] = value; + } +}); + +describe("getHermesHome", () => { + test("HERMES_HOME wins over the platform default (absolute path)", () => { + setPlatform("linux"); + delete process.env.LOCALAPPDATA; + process.env.HERMES_HOME = "/opt/custom/hermes"; + expect(getHermesHome("/home/u")).toBe("/opt/custom/hermes"); + }); + + test("HERMES_HOME is trimmed and a leading ~ is expanded", () => { + setPlatform("darwin"); + process.env.HERMES_HOME = " ~/profiles/coder "; + // The injected home arg is only the fallback base; ~ expands via homedir(). + expect(getHermesHome("/home/u")).toMatch(/profiles[/\\]coder$/); + expect(getHermesHome("/home/u").startsWith("~")).toBe(false); + }); + + test("falls back to /.hermes on Linux/macOS when HERMES_HOME is unset", () => { + setPlatform("darwin"); + delete process.env.HERMES_HOME; + expect(getHermesHome("/home/u")).toBe("/home/u/.hermes"); + }); + + test("falls back to %LOCALAPPDATA%\\hermes on Windows when HERMES_HOME is unset", () => { + setPlatform("win32"); + delete process.env.HERMES_HOME; + process.env.LOCALAPPDATA = "C:\\Users\\u\\AppData\\Local"; + // Build the expectation with join() so the assertion is separator-agnostic + // (path.join uses the runner's separator, not the stubbed process.platform). + expect(getHermesHome("C:\\Users\\u")).toBe(join("C:\\Users\\u\\AppData\\Local", "hermes")); + }); + + test("Windows without LOCALAPPDATA falls back to /AppData/Local/hermes", () => { + setPlatform("win32"); + delete process.env.HERMES_HOME; + delete process.env.LOCALAPPDATA; + expect(getHermesHome("C:\\Users\\u")).toBe(join("C:\\Users\\u", "AppData", "Local", "hermes")); + }); + + test("an empty/whitespace HERMES_HOME is ignored", () => { + setPlatform("linux"); + process.env.HERMES_HOME = " "; + expect(getHermesHome("/home/u")).toBe("/home/u/.hermes"); + }); +}); From be407fd3466bf6a5e02276ed73797a188e8f46bf Mon Sep 17 00:00:00 2001 From: "lisheng.lisheng" Date: Mon, 12 Oct 2026 20:09:15 +0800 Subject: [PATCH 2/2] chore(release): prepare 2.2.1 --- CHANGELOG.md | 6 ++++++ CHANGELOG.zh.md | 6 ++++++ packages/cli/package.json | 2 +- packages/commands/package.json | 2 +- packages/core/package.json | 2 +- packages/kscli/package.json | 2 +- packages/runtime/package.json | 2 +- skills/bailian-cli/SKILL.md | 2 +- skills/bailian-finetune/SKILL.md | 2 +- skills/bailian-gen/SKILL.md | 2 +- skills/bailian-managed-agent/SKILL.md | 2 +- skills/bailian-protocol/SKILL.md | 2 +- skills/bailian-sandbox/SKILL.md | 2 +- skills/bailian-web-search/SKILL.md | 2 +- 14 files changed, 24 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a71ff88d0..464622462 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,12 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and [中文版](CHANGELOG.zh.md) · [README](README.md) · [Contributing](CONTRIBUTING.md) +## [2.2.1] - 2026-10-12 + +### Fixed + +- **Hermes agent config path** — `bl config agent --agent hermes` now writes to the location Hermes actually reads (`$HERMES_HOME/config.yaml`, or the platform-native default: `%LOCALAPPDATA%\hermes` on Windows, `~/.hermes` elsewhere) instead of always `~/.hermes/config.yaml`. Previously on Windows or with a custom `HERMES_HOME`, config landed where Hermes never read it and the change silently took no effect; agent detection could also falsely report Hermes as unconfigured. + ## [2.2.0] - 2026-10-12 ### Changed diff --git a/CHANGELOG.zh.md b/CHANGELOG.zh.md index 7ed339ef8..f8d527b4e 100644 --- a/CHANGELOG.zh.md +++ b/CHANGELOG.zh.md @@ -6,6 +6,12 @@ [English](CHANGELOG.md) · [README](README.zh.md) · [参与贡献](CONTRIBUTING.zh.md) +## [2.2.1] - 2026-10-12 + +### 修复 + +- **Hermes Agent 配置路径** —— `bl config agent --agent hermes` 现在会写入 Hermes 实际读取的位置(`$HERMES_HOME/config.yaml`,或平台原生默认路径:Windows 为 `%LOCALAPPDATA%\hermes`,其它平台为 `~/.hermes`),不再固定写 `~/.hermes/config.yaml`。此前在 Windows 或自定义 `HERMES_HOME` 环境下,配置被写到 Hermes 不读取的文件、变更静默失效,Agent 探测也可能误报 Hermes 为「未配置」。 + ## [2.2.0] - 2026-10-12 ### 变更 diff --git a/packages/cli/package.json b/packages/cli/package.json index 162bbba0f..b2440d178 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "bailian-cli", - "version": "2.2.0", + "version": "2.2.1", "description": "CLI for Aliyun Model Studio (DashScope) AI Platform.", "keywords": [ "agent", diff --git a/packages/commands/package.json b/packages/commands/package.json index 644b67861..194503991 100644 --- a/packages/commands/package.json +++ b/packages/commands/package.json @@ -1,6 +1,6 @@ { "name": "bailian-cli-commands", - "version": "2.2.0", + "version": "2.2.1", "description": "Command library for bailian-cli products (knowledge, memory, media, …). See https://www.npmjs.com/package/bailian-cli for usage.", "homepage": "https://bailian.console.aliyun.com/cli", "bugs": { diff --git a/packages/core/package.json b/packages/core/package.json index af542b123..9c6a3675c 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "bailian-cli-core", - "version": "2.2.0", + "version": "2.2.1", "description": "Core SDK for bailian-cli. See https://www.npmjs.com/package/bailian-cli for usage.", "homepage": "https://bailian.console.aliyun.com/cli", "bugs": { diff --git a/packages/kscli/package.json b/packages/kscli/package.json index fb4ad05d7..3d14d68c2 100644 --- a/packages/kscli/package.json +++ b/packages/kscli/package.json @@ -1,6 +1,6 @@ { "name": "knowledge-studio-cli", - "version": "2.2.0", + "version": "2.2.1", "description": "Lightweight RAG CLI for Aliyun Model Studio — focused on knowledge-base retrieval.", "keywords": [ "alibaba-cloud", diff --git a/packages/runtime/package.json b/packages/runtime/package.json index 6402e51df..b9daf0633 100644 --- a/packages/runtime/package.json +++ b/packages/runtime/package.json @@ -1,6 +1,6 @@ { "name": "bailian-cli-runtime", - "version": "2.2.0", + "version": "2.2.1", "description": "Runtime framework for bailian-cli (createCli, registry, args, output, pipeline). See https://www.npmjs.com/package/bailian-cli for usage.", "homepage": "https://bailian.console.aliyun.com/cli", "bugs": { diff --git a/skills/bailian-cli/SKILL.md b/skills/bailian-cli/SKILL.md index da206b307..6604a7e2d 100644 --- a/skills/bailian-cli/SKILL.md +++ b/skills/bailian-cli/SKILL.md @@ -1,7 +1,7 @@ --- name: bailian-cli metadata: - version: "2.2.0" + version: "2.2.1" requires: bins: ["bl"] description: >- diff --git a/skills/bailian-finetune/SKILL.md b/skills/bailian-finetune/SKILL.md index dd4991e34..f79498a76 100644 --- a/skills/bailian-finetune/SKILL.md +++ b/skills/bailian-finetune/SKILL.md @@ -1,7 +1,7 @@ --- name: bailian-finetune metadata: - version: "2.2.0" + version: "2.2.1" requires: bins: ["bl"] description: >- diff --git a/skills/bailian-gen/SKILL.md b/skills/bailian-gen/SKILL.md index 451629250..71e212555 100644 --- a/skills/bailian-gen/SKILL.md +++ b/skills/bailian-gen/SKILL.md @@ -1,7 +1,7 @@ --- name: bailian-gen metadata: - version: "2.2.0" + version: "2.2.1" requires: bins: ["bl"] description: >- diff --git a/skills/bailian-managed-agent/SKILL.md b/skills/bailian-managed-agent/SKILL.md index a9f2b6ed1..0c1461c7e 100644 --- a/skills/bailian-managed-agent/SKILL.md +++ b/skills/bailian-managed-agent/SKILL.md @@ -1,7 +1,7 @@ --- name: bailian-managed-agent metadata: - version: "2.2.0" + version: "2.2.1" requires: bins: ["bl"] description: >- diff --git a/skills/bailian-protocol/SKILL.md b/skills/bailian-protocol/SKILL.md index 46c00253e..186cda166 100644 --- a/skills/bailian-protocol/SKILL.md +++ b/skills/bailian-protocol/SKILL.md @@ -1,7 +1,7 @@ --- name: bailian-protocol metadata: - version: "2.2.0" + version: "2.2.1" requires: bins: ["bl"] description: >- diff --git a/skills/bailian-sandbox/SKILL.md b/skills/bailian-sandbox/SKILL.md index 33d9e570a..58fd85bda 100644 --- a/skills/bailian-sandbox/SKILL.md +++ b/skills/bailian-sandbox/SKILL.md @@ -1,7 +1,7 @@ --- name: bailian-sandbox metadata: - version: "2.2.0" + version: "2.2.1" requires: bins: ["bl"] description: >- diff --git a/skills/bailian-web-search/SKILL.md b/skills/bailian-web-search/SKILL.md index 4948b5126..051a984ba 100644 --- a/skills/bailian-web-search/SKILL.md +++ b/skills/bailian-web-search/SKILL.md @@ -1,7 +1,7 @@ --- name: bailian-web-search metadata: - version: "2.2.0" + version: "2.2.1" requires: bins: ["bl"] description: >-