Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions CHANGELOG.zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

### 变更
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
2 changes: 1 addition & 1 deletion packages/commands/package.json
Original file line number Diff line number Diff line change
@@ -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": {
Expand Down
6 changes: 4 additions & 2 deletions packages/commands/src/commands/config/agent/writers/hermes.ts
Original file line number Diff line number Diff line change
@@ -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);

Expand Down
9 changes: 5 additions & 4 deletions packages/commands/src/commands/config/inventory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

/**
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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<string, unknown> | undefined;
try {
Expand Down Expand Up @@ -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<string, unknown> | undefined;
if (text) {
try {
Expand Down
38 changes: 37 additions & 1 deletion packages/commands/tests/config-agent-writers.test.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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 });
Expand Down
32 changes: 32 additions & 0 deletions packages/commands/tests/inventory.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
});
});
2 changes: 1 addition & 1 deletion packages/core/package.json
Original file line number Diff line number Diff line change
@@ -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": {
Expand Down
5 changes: 4 additions & 1 deletion packages/core/src/skills/agents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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/<name>),
Expand Down Expand Up @@ -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"),
{
Expand Down
39 changes: 39 additions & 0 deletions packages/core/src/utils/agent-home.ts
Original file line number Diff line number Diff line change
@@ -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;
}
1 change: 1 addition & 0 deletions packages/core/src/utils/index.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down
73 changes: 73 additions & 0 deletions packages/core/tests/agent-home.test.ts
Original file line number Diff line number Diff line change
@@ -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 <home>/.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 <home>/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");
});
});
2 changes: 1 addition & 1 deletion packages/kscli/package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
2 changes: 1 addition & 1 deletion packages/runtime/package.json
Original file line number Diff line number Diff line change
@@ -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": {
Expand Down
2 changes: 1 addition & 1 deletion skills/bailian-cli/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
name: bailian-cli
metadata:
version: "2.2.0"
version: "2.2.1"
requires:
bins: ["bl"]
description: >-
Expand Down
2 changes: 1 addition & 1 deletion skills/bailian-finetune/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
name: bailian-finetune
metadata:
version: "2.2.0"
version: "2.2.1"
requires:
bins: ["bl"]
description: >-
Expand Down
2 changes: 1 addition & 1 deletion skills/bailian-gen/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
name: bailian-gen
metadata:
version: "2.2.0"
version: "2.2.1"
requires:
bins: ["bl"]
description: >-
Expand Down
2 changes: 1 addition & 1 deletion skills/bailian-managed-agent/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
name: bailian-managed-agent
metadata:
version: "2.2.0"
version: "2.2.1"
requires:
bins: ["bl"]
description: >-
Expand Down
2 changes: 1 addition & 1 deletion skills/bailian-protocol/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
name: bailian-protocol
metadata:
version: "2.2.0"
version: "2.2.1"
requires:
bins: ["bl"]
description: >-
Expand Down
2 changes: 1 addition & 1 deletion skills/bailian-sandbox/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
name: bailian-sandbox
metadata:
version: "2.2.0"
version: "2.2.1"
requires:
bins: ["bl"]
description: >-
Expand Down
Loading
Loading