From 0731563d1a73d94e6fc6b2e74fff4b3e50b5364e Mon Sep 17 00:00:00 2001 From: liruifengv Date: Mon, 8 Jun 2026 17:15:17 +0800 Subject: [PATCH] fix: honor KIMI_CODE_HOME for global agent resources --- .changeset/follow-kimi-home-resources.md | 6 +++ docs/AGENTS.md | 6 +-- docs/en/configuration/data-locations.md | 17 ++++--- docs/en/customization/agents.md | 4 ++ docs/en/customization/skills.md | 6 ++- docs/zh/configuration/data-locations.md | 19 +++++--- docs/zh/customization/agents.md | 4 ++ docs/zh/customization/skills.md | 6 ++- packages/agent-core/src/profile/context.ts | 14 ++++-- packages/agent-core/src/rpc/core-impl.ts | 1 + packages/agent-core/src/session/index.ts | 7 ++- .../agent-core/src/session/subagent-host.ts | 2 +- packages/agent-core/src/skill/scanner.ts | 13 ++++- .../test/harness/skill-session.test.ts | 39 ++++++++++----- .../agent-core/test/profile/context.test.ts | 43 +++++++++++++++++ .../test/session/subagent-host.test.ts | 43 +++++++++++++++++ .../agent-core/test/skill/scanner.test.ts | 48 +++++++++++++++++++ packages/node-sdk/test/session-skills.test.ts | 29 ++++++++--- 18 files changed, 259 insertions(+), 48 deletions(-) create mode 100644 .changeset/follow-kimi-home-resources.md diff --git a/.changeset/follow-kimi-home-resources.md b/.changeset/follow-kimi-home-resources.md new file mode 100644 index 0000000000..0189791bfb --- /dev/null +++ b/.changeset/follow-kimi-home-resources.md @@ -0,0 +1,6 @@ +--- +"@moonshot-ai/agent-core": patch +"@moonshot-ai/kimi-code": patch +--- + +Load Kimi-specific user Skills and global agent instructions from `KIMI_CODE_HOME` when it is set. diff --git a/docs/AGENTS.md b/docs/AGENTS.md index 764a7e580a..e3bd7affc0 100644 --- a/docs/AGENTS.md +++ b/docs/AGENTS.md @@ -97,9 +97,9 @@ Term mapping (Chinese <-> English, and proper noun handling): - Chinese: use `提示` for tip, `注意` for warning, `说明` for info, `警告` for danger. - English: use no title or short words like `Note` for warning. - ✓ `::: tip 提示` + content starting with the key point - - ✓ `::: warning 注意` + content `\`KIMI_CODE_HOME\` 不影响 Skills 的搜索路径。...` - - ✗ `::: warning 不影响 Skills` (title too long, should be in content) - - ✗ `::: tip Skills 路径独立于 KIMI_CODE_HOME` (title too long) + - ✓ `::: warning 注意` + content `部分 \`.agents\` 资源不受 \`KIMI_CODE_HOME\` 影响。...` + - ✗ `::: warning 不影响 .agents` (title too long, should be in content) + - ✗ `::: tip .agents 路径独立于 KIMI_CODE_HOME` (title too long) - **Version info blocks**: For version change callouts, use `::: info` with a category title (Added/Changed/Removed in English; 新增/变更/移除 in Chinese). The content should be a complete sentence. - ✓ `::: info 新增` + content `新增于 0.2.0。` - ✗ `::: info 新增于 0.2.0` (title too long) diff --git a/docs/en/configuration/data-locations.md b/docs/en/configuration/data-locations.md index 2f08ca0c18..9122609697 100644 --- a/docs/en/configuration/data-locations.md +++ b/docs/en/configuration/data-locations.md @@ -16,13 +16,13 @@ If you need to move the data directory elsewhere (for example, to isolate config export KIMI_CODE_HOME="$HOME/.config/kimi-code" ``` -Once set, **all** data — config, sessions, logs, OAuth credentials, and more — lands under the new path. For the full reference on `KIMI_CODE_HOME`, see [Environment variables](./env-vars.md). +Once set, **all** Kimi Code data — config, sessions, logs, OAuth credentials, Kimi-specific user Skills, global `AGENTS.md`, and more — lands under the new path. For the full reference on `KIMI_CODE_HOME`, see [Environment variables](./env-vars.md). -::: tip Two types of data are not affected by `KIMI_CODE_HOME` +::: tip Note **Built-in tool cache** (ripgrep binary) uses `KIMI_CODE_CACHE_DIR` instead. When that is unset, the platform cache directory is used: `~/Library/Caches/kimi-code` on macOS, `~/.cache/kimi-code` on Linux, and `%LOCALAPPDATA%\kimi-code` on Windows. -**Agent Skills** search paths are `~/.kimi-code/skills` and `~/.agents/skills` (user level), and `.kimi-code/skills` and `.agents/skills` under the working directory (project level). See [Agent Skills](../customization/skills.md). +**Generic `.agents` resources** stay under the real OS home so they can be shared across tools. For example, user-level generic Skills remain at `~/.agents/skills/`, while Kimi-specific user Skills move with `KIMI_CODE_HOME` as `$KIMI_CODE_HOME/skills/`. ::: ## Directory layout @@ -31,7 +31,9 @@ Once set, **all** data — config, sessions, logs, OAuth credentials, and more $KIMI_CODE_HOME (default: ~/.kimi-code) ├── config.toml # User configuration ├── tui.toml # Terminal UI preferences (including auto-update toggle) +├── AGENTS.md # Global Kimi-specific agent instructions (optional) ├── mcp.json # User-level MCP server declarations (optional) +├── skills/ # Kimi-specific user-level Skills (optional) ├── plugins/ │ ├── installed.json # Installed plugin records and enabled state │ └── managed/ # Plugin copies installed from zip/local paths @@ -60,7 +62,9 @@ Each top-level file under the data root serves a specific purpose; most are mana - **`config.toml`**: the main runtime configuration file, storing user-level settings such as providers, models, and loop control. See [Configuration files](./config-files.md). - **`tui.toml`**: terminal UI client preferences, including `[upgrade].auto_install` (auto-update, on by default). You can disable it in `/settings` or by manually setting `auto_install = false`. +- **`AGENTS.md`**: global Kimi-specific agent instructions. This file moves with `KIMI_CODE_HOME`; generic cross-tool instructions can still live under `~/.agents/AGENTS.md`. - **`mcp.json`**: user-level MCP server declarations, merged with the project-local `.kimi-code/mcp.json` on startup. See [MCP](../customization/mcp.md). +- **`skills/`**: Kimi-specific user-level Skills. This directory moves with `KIMI_CODE_HOME`; generic cross-tool Skills can still live under `~/.agents/skills/`. See [Agent Skills](../customization/skills.md). - **`plugins/installed.json`**: records installed plugins, each plugin's enabled state, and MCP server capability state changes made via `/plugins` or `/plugins mcp disable|enable`. Files installed from local paths or zip URLs are copied to `plugins/managed//`. See [Plugins](../customization/plugins.md). - **`credentials/`**: OAuth credential directory, with permissions `0o700` (directory) / `0o600` (files), readable and writable only by the current user. Managed provider credentials are stored as `credentials/.json`; MCP server credentials are stored under `credentials/mcp/`. Credentials are written using an atomic flow (tmp → fsync → rename) to prevent corruption. @@ -111,9 +115,10 @@ Deleting the data root directory (`~/.kimi-code/` or the path set by `KIMI_CODE_ | Force re-download of ripgrep | Delete `~/.kimi-code/bin/` | | Clear provider OAuth login state | Run `/logout`, or delete the corresponding `credentials/.json` | | Clear MCP server OAuth login state | Delete `credentials/mcp/` (`/logout` does not clear MCP credentials) | -| Remove user-level MCP declarations | Delete `~/.kimi-code/mcp.json` | -| Clear plugin install records | Delete `~/.kimi-code/plugins/` (local plugin source directories are not affected) | -| Clear user-level Skills | Delete `~/.kimi-code/skills/` | +| Remove user-level MCP declarations | Delete `$KIMI_CODE_HOME/mcp.json` (default `~/.kimi-code/mcp.json`) | +| Clear global Kimi-specific agent instructions | Delete `$KIMI_CODE_HOME/AGENTS.md` (default `~/.kimi-code/AGENTS.md`) | +| Clear plugin install records | Delete `$KIMI_CODE_HOME/plugins/` (local plugin source directories are not affected) | +| Clear Kimi-specific user-level Skills | Delete `$KIMI_CODE_HOME/skills/` (default `~/.kimi-code/skills/`) | ## Next steps diff --git a/docs/en/customization/agents.md b/docs/en/customization/agents.md index d6bd373b05..4eb0b753bd 100644 --- a/docs/en/customization/agents.md +++ b/docs/en/customization/agents.md @@ -37,6 +37,10 @@ Sub-agent permission rules are inherited from the main Agent: "always allow" rul If you need a particular type of tool to be permanently unavailable inside sub-agents, tighten the corresponding permission rule on the main Agent. +## Instruction Files + +Global Kimi-specific instructions can live at `$KIMI_CODE_HOME/AGENTS.md` (default: `~/.kimi-code/AGENTS.md`). When you relocate the data root with `KIMI_CODE_HOME`, this global instruction file moves with it. Generic cross-tool instructions can still live under `~/.agents/AGENTS.md` in the real OS home, and project-level instructions remain under the project tree, for example `.kimi-code/AGENTS.md` or `AGENTS.md`. + ## Storage Location in the Session Directory Sub-agent runtime state is persisted to the `agents/` subdirectory of the current session directory. Each sub-agent instance has its own directory, which contains a `wire.jsonl` file that records prompts, message history, and final state in chronological order. Background sub-agents also expose their lifecycle status through a `tasks/` subdirectory. diff --git a/docs/en/customization/skills.md b/docs/en/customization/skills.md index 1f96b85eef..3bb35fb546 100644 --- a/docs/en/customization/skills.md +++ b/docs/en/customization/skills.md @@ -66,9 +66,11 @@ Positional arguments support single and double quoting, so in `/skill:commit "fi Kimi Code CLI scans four tiers by scope; more specific scopes take higher priority: **Project > User > Extra > Built-in** **User level** (applies to all projects): -- `~/.kimi-code/skills/` +- `$KIMI_CODE_HOME/skills/` (default: `~/.kimi-code/skills/`) - `~/.agents/skills/` +The Kimi-specific user Skill directory moves with `KIMI_CODE_HOME`, so isolated data roots also get isolated Kimi-specific Skills. The generic `~/.agents/skills/` directory stays under the real OS home so it can be shared across tools. + **Project level** (project root = the nearest directory containing `.git`, searching upward from the working directory): - `.kimi-code/skills/` - `.agents/skills/` @@ -120,7 +122,7 @@ Please review the PR the user specified: $pr_ref - Noteworthy positives ``` -Save this as `~/.kimi-code/skills/review-pr/SKILL.md`, place the checklist at `references/checklist.md` in the same directory, and after starting a new session you can invoke it with `/skill:review-pr #1234`, where `#1234` is expanded into `$pr_ref`. +Save this as `$KIMI_CODE_HOME/skills/review-pr/SKILL.md` (or `~/.kimi-code/skills/review-pr/SKILL.md` when `KIMI_CODE_HOME` is unset), place the checklist at `references/checklist.md` in the same directory, and after starting a new session you can invoke it with `/skill:review-pr #1234`, where `#1234` is expanded into `$pr_ref`. ## Next steps diff --git a/docs/zh/configuration/data-locations.md b/docs/zh/configuration/data-locations.md index 1325a74513..701460b011 100644 --- a/docs/zh/configuration/data-locations.md +++ b/docs/zh/configuration/data-locations.md @@ -16,13 +16,15 @@ Kimi Code CLI 把所有运行时数据——配置文件、会话历史、登录 export KIMI_CODE_HOME="$HOME/.config/kimi-code" ``` -设置后,配置、会话、日志、OAuth 凭据等**全部**数据都会落到新路径下。`KIMI_CODE_HOME` 的完整说明见[环境变量](./env-vars.md)。 +设置后,配置、会话、日志、OAuth 凭据、Kimi 专属用户级 Skills、全局 `AGENTS.md` 等 **Kimi Code 数据**都会落到新路径下。`KIMI_CODE_HOME` 的完整说明见[环境变量](./env-vars.md)。 -::: tip 两类数据不受 `KIMI_CODE_HOME` 影响 +::: tip 提示 + +部分路径不受 `KIMI_CODE_HOME` 影响。 **内置工具缓存**(ripgrep 二进制)走的是 `KIMI_CODE_CACHE_DIR`,未设时使用平台缓存目录:macOS 的 `~/Library/Caches/kimi-code`、Linux 的 `~/.cache/kimi-code`、Windows 的 `%LOCALAPPDATA%\kimi-code`。 -**Agent Skills** 的搜索路径是 `~/.kimi-code/skills` 和 `~/.agents/skills`(用户级),以及工作目录下的 `.kimi-code/skills` 和 `.agents/skills`(项目级)。详见 [Agent Skills](../customization/skills.md)。 +**通用 `.agents` 资源**仍放在真实 OS home 下,以便跨工具共享。例如,用户级通用 Skills 仍位于 `~/.agents/skills/`,而 Kimi 专属用户级 Skills 会随 `KIMI_CODE_HOME` 移动到 `$KIMI_CODE_HOME/skills/`。 ::: ## 目录结构 @@ -31,7 +33,9 @@ export KIMI_CODE_HOME="$HOME/.config/kimi-code" $KIMI_CODE_HOME (默认 ~/.kimi-code) ├── config.toml # 用户配置 ├── tui.toml # 终端界面偏好(含自动更新开关) +├── AGENTS.md # 全局 Kimi 专属 Agent 指令(可选) ├── mcp.json # 用户级 MCP server 声明(可选) +├── skills/ # Kimi 专属用户级 Skills(可选) ├── plugins/ │ ├── installed.json # 已安装 plugin 记录与启用状态 │ └── managed/ # zip/本地路径安装的 plugin 副本 @@ -60,7 +64,9 @@ $KIMI_CODE_HOME (默认 ~/.kimi-code) - **`config.toml`**:主运行时配置,存放供应商、模型、循环控制等用户级设置。详见[配置文件](./config-files.md)。 - **`tui.toml`**:终端界面客户端偏好,包括 `[upgrade].auto_install`(自动更新,默认开启)。可在 `/settings` 关闭,或手动设为 `auto_install = false`。 +- **`AGENTS.md`**:全局 Kimi 专属 Agent 指令。该文件会随 `KIMI_CODE_HOME` 移动;跨工具通用指令仍可放在 `~/.agents/AGENTS.md`。 - **`mcp.json`**:用户级 MCP server 声明,启动时与项目内的 `.kimi-code/mcp.json` 合并加载。详见 [MCP](../customization/mcp.md)。 +- **`skills/`**:Kimi 专属用户级 Skills。该目录会随 `KIMI_CODE_HOME` 移动;跨工具通用 Skills 仍可放在 `~/.agents/skills/`。详见 [Agent Skills](../customization/skills.md)。 - **`plugins/installed.json`**:记录已安装的 plugin、每个 plugin 的启用状态,以及通过 `/plugins` 或 `/plugins mcp disable|enable` 修改的 MCP server 能力状态。本地路径和 zip URL 安装的文件会复制到 `plugins/managed//`。详见 [Plugins](../customization/plugins.md)。 - **`credentials/`**:OAuth 凭据目录,权限 `0o700`(目录)/ `0o600`(文件),仅当前用户可读写。托管供应商凭据存为 `credentials/.json`,MCP server 凭据存在 `credentials/mcp/` 子目录下。凭据写入使用原子流程(tmp → fsync → rename)防止写损。 @@ -111,9 +117,10 @@ CLI 第一次需要 ripgrep 时会自动下载并缓存到 `bin/rg`(Windows | 强制重新下载 ripgrep | 删除 `~/.kimi-code/bin/` | | 清除供应商 OAuth 登录态 | 运行 `/logout`,或删除对应的 `credentials/.json` | | 清除 MCP server OAuth 登录态 | 删除 `credentials/mcp/`(`/logout` 不会清理 MCP 凭据) | -| 移除用户级 MCP 声明 | 删除 `~/.kimi-code/mcp.json` | -| 清理 plugin 安装记录 | 删除 `~/.kimi-code/plugins/`(本地 plugin 源码不受影响) | -| 清空用户级 Skills | 删除 `~/.kimi-code/skills/` | +| 移除用户级 MCP 声明 | 删除 `$KIMI_CODE_HOME/mcp.json`(默认为 `~/.kimi-code/mcp.json`) | +| 清理全局 Kimi 专属 Agent 指令 | 删除 `$KIMI_CODE_HOME/AGENTS.md`(默认为 `~/.kimi-code/AGENTS.md`) | +| 清理 plugin 安装记录 | 删除 `$KIMI_CODE_HOME/plugins/`(本地 plugin 源码不受影响) | +| 清空 Kimi 专属用户级 Skills | 删除 `$KIMI_CODE_HOME/skills/`(默认为 `~/.kimi-code/skills/`) | ## 下一步 diff --git a/docs/zh/customization/agents.md b/docs/zh/customization/agents.md index f6af376955..5b754ada1b 100644 --- a/docs/zh/customization/agents.md +++ b/docs/zh/customization/agents.md @@ -37,6 +37,10 @@ Kimi Code CLI 内置三种子 Agent,开箱即用,分别面向不同任务形 如果需要某类工具在子 Agent 中始终不可用,应收紧主 Agent 的权限规则。 +## 指令文件 + +全局 Kimi 专属指令可放在 `$KIMI_CODE_HOME/AGENTS.md`(默认:`~/.kimi-code/AGENTS.md`)。当你用 `KIMI_CODE_HOME` 移动数据根时,这份全局指令文件也会一起移动。跨工具通用指令仍可放在真实 OS home 下的 `~/.agents/AGENTS.md`,项目级指令仍放在项目目录中,例如 `.kimi-code/AGENTS.md` 或 `AGENTS.md`。 + ## 会话目录中的存储位置 子 Agent 的运行状态持久化到当前会话目录的 `agents/` 子目录下,每个子 Agent 实例对应一个独立目录,其中包含按时间顺序记录提示词、消息历史与最终状态的 `wire.jsonl` 文件。后台子 Agent 还会通过 `tasks/` 子目录暴露生命周期状态。 diff --git a/docs/zh/customization/skills.md b/docs/zh/customization/skills.md index 1b0829e9f6..c26d9a1b2d 100644 --- a/docs/zh/customization/skills.md +++ b/docs/zh/customization/skills.md @@ -66,9 +66,11 @@ arguments: Kimi Code CLI 按作用域分四档扫描,越具体的作用域优先级越高:**Project > User > Extra > Built-in** **用户级**(对所有项目生效): -- `~/.kimi-code/skills/` +- `$KIMI_CODE_HOME/skills/`(默认:`~/.kimi-code/skills/`) - `~/.agents/skills/` +Kimi 专属用户级 Skill 目录会随 `KIMI_CODE_HOME` 移动,因此隔离数据根时也会隔离 Kimi 专属 Skills。通用 `~/.agents/skills/` 目录仍放在真实 OS home 下,以便跨工具共享。 + **项目级**(项目根 = 工作目录向上最近的含 `.git` 的目录): - `.kimi-code/skills/` - `.agents/skills/` @@ -120,7 +122,7 @@ arguments: - 值得肯定的地方 ``` -保存为 `~/.kimi-code/skills/review-pr/SKILL.md`,检查清单放在同目录的 `references/checklist.md`,重开会话后即可通过 `/skill:review-pr #1234` 调用,其中 `#1234` 会展开到 `$pr_ref`。 +保存为 `$KIMI_CODE_HOME/skills/review-pr/SKILL.md`(未设置 `KIMI_CODE_HOME` 时为 `~/.kimi-code/skills/review-pr/SKILL.md`),检查清单放在同目录的 `references/checklist.md`,重开会话后即可通过 `/skill:review-pr #1234` 调用,其中 `#1234` 会展开到 `$pr_ref`。 ## 下一步 diff --git a/packages/agent-core/src/profile/context.ts b/packages/agent-core/src/profile/context.ts index 430e4f7082..e03b8469c2 100644 --- a/packages/agent-core/src/profile/context.ts +++ b/packages/agent-core/src/profile/context.ts @@ -13,15 +13,16 @@ export type PreparedSystemPromptContext = Pick { const [cwdListing, agentsMd] = await Promise.all([ listDirectory(kaos), - loadAgentsMd(kaos), + loadAgentsMd(kaos, brandHome), ]); return { cwdListing, agentsMd }; } -export async function loadAgentsMd(kaos: Kaos): Promise { +export async function loadAgentsMd(kaos: Kaos, brandHome?: string): Promise { const workDir = kaos.getcwd(); const projectRoot = await findProjectRoot(kaos, workDir); const dirs = dirsRootToLeaf(kaos, workDir, projectRoot); @@ -39,11 +40,14 @@ export async function loadAgentsMd(kaos: Kaos): Promise { }; // User-level files come first so any project-level AGENTS.md overrides them. - const home = kaos.gethome(); - await collect(join(home, '.kimi-code', 'AGENTS.md')); + // The brand dir follows KIMI_CODE_HOME (default ~/.kimi-code); the generic + // .agents dir stays under the real OS home so it can be shared across tools. + const realHome = kaos.gethome(); + const brandDir = brandHome ?? join(realHome, '.kimi-code'); + await collect(join(brandDir, 'AGENTS.md')); // Generic user-level dir (.agents) matches skill discovery. - const genericDirs = [join(home, '.agents')]; + const genericDirs = [join(realHome, '.agents')]; const genericFiles = genericDirs.flatMap((dir) => ['AGENTS.md', 'agents.md'].map((name) => join(dir, name)), ); diff --git a/packages/agent-core/src/rpc/core-impl.ts b/packages/agent-core/src/rpc/core-impl.ts index 56207ebfd3..77640aef55 100644 --- a/packages/agent-core/src/rpc/core-impl.ts +++ b/packages/agent-core/src/rpc/core-impl.ts @@ -778,6 +778,7 @@ export class KimiCore implements PromisableMethods { const explicitDirs = this.skillDirs.length > 0 ? this.skillDirs : undefined; return { userHomeDir: this.userHomeDir, + brandHomeDir: this.homeDir, explicitDirs, extraDirs: config.extraSkillDirs, pluginSkillRoots: this.plugins.pluginSkillRoots(), diff --git a/packages/agent-core/src/session/index.ts b/packages/agent-core/src/session/index.ts index 5a0ee234af..b1867a0892 100644 --- a/packages/agent-core/src/session/index.ts +++ b/packages/agent-core/src/session/index.ts @@ -65,6 +65,8 @@ export interface SessionOptions { export interface SessionSkillConfig { readonly userHomeDir?: string; + /** Brand data dir (KIMI_CODE_HOME); user brand skills live under `/skills`. */ + readonly brandHomeDir?: string; readonly explicitDirs?: readonly string[]; readonly extraDirs?: readonly string[]; readonly pluginSkillRoots?: readonly SkillRoot[]; @@ -323,7 +325,7 @@ export class Session { agent: Agent, profile: ResolvedAgentProfile, ): Promise { - const context = await prepareSystemPromptContext(agent.kaos); + const context = await prepareSystemPromptContext(agent.kaos, this.options.kimiHomeDir); agent.useProfile(profile, context); } @@ -342,7 +344,7 @@ export class Session { }); await handle.completion; - const agentsMd = await loadAgentsMd(mainAgent.kaos); + const agentsMd = await loadAgentsMd(mainAgent.kaos, this.options.kimiHomeDir); mainAgent.context.appendSystemReminder(initCompletionReminder(agentsMd), { kind: 'injection', variant: 'init', @@ -399,6 +401,7 @@ export class Session { const roots = await resolveSkillRoots({ paths: { userHomeDir: this.options.skills?.userHomeDir ?? homedir(), + brandHomeDir: this.options.skills?.brandHomeDir ?? this.options.kimiHomeDir, workDir: this.options.kaos.getcwd(), }, explicitDirs: this.options.skills?.explicitDirs, diff --git a/packages/agent-core/src/session/subagent-host.ts b/packages/agent-core/src/session/subagent-host.ts index 38e68b947a..09437ff7a3 100644 --- a/packages/agent-core/src/session/subagent-host.ts +++ b/packages/agent-core/src/session/subagent-host.ts @@ -362,7 +362,7 @@ export class SessionSubagentHost { thinkingLevel: parent.config.thinkingLevel, }); - const context = await prepareSystemPromptContext(child.kaos); + const context = await prepareSystemPromptContext(child.kaos, this.session.options.kimiHomeDir); child.useProfile(profile, context); child.tools.inheritUserTools(parent.tools); } diff --git a/packages/agent-core/src/skill/scanner.ts b/packages/agent-core/src/skill/scanner.ts index c133faf6df..b0e02a9ba5 100644 --- a/packages/agent-core/src/skill/scanner.ts +++ b/packages/agent-core/src/skill/scanner.ts @@ -6,7 +6,9 @@ import { SkillParseError, UnsupportedSkillTypeError, parseSkillFromFile } from ' import type { SkillDefinition, SkillRoot, SkillSource, SkippedSkill } from './types'; import { normalizeSkillName } from './types'; -const USER_BRAND_DIRS = ['.kimi-code/skills'] as const; +// Relative to brandHomeDir, which already IS the brand data dir (~/.kimi-code or +// $KIMI_CODE_HOME) — no '.kimi-code' segment here, or it would nest twice. +const USER_BRAND_DIRS = ['skills'] as const; const USER_GENERIC_DIRS = ['.agents/skills'] as const; const PROJECT_BRAND_DIRS = ['.kimi-code/skills'] as const; const PROJECT_GENERIC_DIRS = ['.agents/skills'] as const; @@ -21,6 +23,12 @@ const MAX_SKILL_SCAN_DEPTH = 8; export interface SkillPathContext { readonly userHomeDir: string; + /** + * Brand data dir — `KIMI_CODE_HOME`, or `/.kimi-code` by default. + * User brand skills live directly under here as `skills/`, so this path + * carries no `.kimi-code` segment of its own (that would double the prefix). + */ + readonly brandHomeDir?: string; readonly workDir: string; } @@ -66,6 +74,7 @@ export async function resolveSkillRoots( const roots: SkillRoot[] = []; const mergeAllAvailableSkills = options.mergeAllAvailableSkills ?? true; const { userHomeDir, workDir } = options.paths; + const brandHomeDir = options.paths.brandHomeDir ?? path.join(userHomeDir, '.kimi-code'); const projectRoot = await findProjectRoot(workDir); if (options.explicitDirs !== undefined && options.explicitDirs.length > 0) { @@ -92,7 +101,7 @@ export async function resolveSkillRoots( await pushBrandGroup( roots, USER_BRAND_DIRS, - userHomeDir, + brandHomeDir, 'user', mergeAllAvailableSkills, isDir, diff --git a/packages/agent-core/test/harness/skill-session.test.ts b/packages/agent-core/test/harness/skill-session.test.ts index 76366049e8..75cd553cdf 100644 --- a/packages/agent-core/test/harness/skill-session.test.ts +++ b/packages/agent-core/test/harness/skill-session.test.ts @@ -100,33 +100,33 @@ describe('HarnessAPI session skills', () => { expect(JSON.stringify(skills)).not.toContain('Your tool list contains one synthetic tool'); }); - it('resolves user skills from the OS home directory, not from the kimi home', async () => { + it('resolves user brand skills from the kimi home, not the OS home', async () => { const processHome = join(tmp, 'process-home'); vi.stubEnv('HOME', processHome); - await writeUserSkill(processHome, 'real-home-only', 'Real home skill'); - await writeUserSkill(homeDir, 'sandbox-only', 'Sandbox skill'); + await writeLegacyUserSkill(processHome, 'real-home-only', 'Real home skill'); + await writeBrandUserSkill(homeDir, 'sandbox-only', 'Sandbox skill'); const { rpc } = await createTestRpc(); const created = await rpc.createSession({ id: 'ses_skill_sandbox_home', workDir }); const names = new Set((await rpc.listSkills({ sessionId: created.id })).map((skill) => skill.name)); - expect(names.has('real-home-only')).toBe(true); - expect(names.has('sandbox-only')).toBe(false); + expect(names.has('real-home-only')).toBe(false); + expect(names.has('sandbox-only')).toBe(true); }); - it('resolves user skills from the OS home directory even when KIMI_CODE_HOME is set', async () => { + it('resolves user brand skills from KIMI_CODE_HOME when no explicit home is set', async () => { const processHome = join(tmp, 'env-process-home'); vi.stubEnv('HOME', processHome); vi.stubEnv('KIMI_CODE_HOME', homeDir); - await writeUserSkill(processHome, 'env-real-home-only', 'Env real home skill'); - await writeUserSkill(homeDir, 'env-sandbox-only', 'Env sandbox skill'); + await writeLegacyUserSkill(processHome, 'env-real-home-only', 'Env real home skill'); + await writeBrandUserSkill(homeDir, 'env-sandbox-only', 'Env sandbox skill'); const { rpc } = await createTestRpc({}); const created = await rpc.createSession({ id: 'ses_skill_env_home', workDir }); const names = new Set((await rpc.listSkills({ sessionId: created.id })).map((skill) => skill.name)); - expect(names.has('env-real-home-only')).toBe(true); - expect(names.has('env-sandbox-only')).toBe(false); + expect(names.has('env-real-home-only')).toBe(false); + expect(names.has('env-sandbox-only')).toBe(true); }); it('activates an inline skill through core and records display origin metadata', async () => { @@ -496,8 +496,23 @@ describe('HarnessAPI session skills', () => { await writeFile(join(dir, 'SKILL.md'), lines.join('\n')); } - async function writeUserSkill(userHomeDir: string, name: string, description: string): Promise { - const dir = join(userHomeDir, '.kimi-code', 'skills', name); + async function writeLegacyUserSkill( + userHomeDir: string, + name: string, + description: string, + ): Promise { + await writeSkillFile(join(userHomeDir, '.kimi-code', 'skills', name), name, description); + } + + async function writeBrandUserSkill( + brandHomeDir: string, + name: string, + description: string, + ): Promise { + await writeSkillFile(join(brandHomeDir, 'skills', name), name, description); + } + + async function writeSkillFile(dir: string, name: string, description: string): Promise { await mkdir(dir, { recursive: true }); await writeFile( join(dir, 'SKILL.md'), diff --git a/packages/agent-core/test/profile/context.test.ts b/packages/agent-core/test/profile/context.test.ts index 9e4f3e00e8..dded000963 100644 --- a/packages/agent-core/test/profile/context.test.ts +++ b/packages/agent-core/test/profile/context.test.ts @@ -68,3 +68,46 @@ describe('loadAgentsMd user-level discovery', () => { expect(result.split('home branded').length - 1).toBe(1); }); }); + +describe('loadAgentsMd brand home (KIMI_CODE_HOME)', () => { + let brandHome: string; + + beforeEach(async () => { + brandHome = await mkdtemp(join(tmpdir(), 'kimi-agents-brand-')); + }); + + afterEach(async () => { + await rm(brandHome, { recursive: true, force: true }); + }); + + it('loads the branded AGENTS.md from the brand home and generic from the real home', async () => { + await writeFile(join(brandHome, 'AGENTS.md'), 'brand home instructions', 'utf-8'); + await mkdir(join(homeDir, '.agents'), { recursive: true }); + await writeFile(join(homeDir, '.agents', 'AGENTS.md'), 'real home generic', 'utf-8'); + + const result = await loadAgentsMd(testKaos, brandHome); + + expect(result).toContain('brand home instructions'); + expect(result).toContain('real home generic'); + }); + + it('ignores the real-home .kimi-code/AGENTS.md when the brand home is elsewhere', async () => { + await writeFile(join(brandHome, 'AGENTS.md'), 'brand wins', 'utf-8'); + await mkdir(join(homeDir, '.kimi-code'), { recursive: true }); + await writeFile(join(homeDir, '.kimi-code', 'AGENTS.md'), 'stale real-home brand', 'utf-8'); + + const result = await loadAgentsMd(testKaos, brandHome); + + expect(result).toContain('brand wins'); + expect(result).not.toContain('stale real-home brand'); + }); + + it('falls back to the real-home .kimi-code/AGENTS.md when no brand home is given', async () => { + await mkdir(join(homeDir, '.kimi-code'), { recursive: true }); + await writeFile(join(homeDir, '.kimi-code', 'AGENTS.md'), 'fallback branded', 'utf-8'); + + const result = await loadAgentsMd(testKaos); + + expect(result).toContain('fallback branded'); + }); +}); diff --git a/packages/agent-core/test/session/subagent-host.test.ts b/packages/agent-core/test/session/subagent-host.test.ts index 8437cff8fc..97d86773e9 100644 --- a/packages/agent-core/test/session/subagent-host.test.ts +++ b/packages/agent-core/test/session/subagent-host.test.ts @@ -1306,6 +1306,48 @@ describe('Session.createAgent', () => { expect(created.agent.config.systemPrompt).toContain('leaf instructions'); }); + it('uses the kimi home for global branded AGENTS.md files', async () => { + const realHome = '/real-home'; + const kimiHome = '/kimi-home'; + const workDir = '/repo/packages/app'; + const kaos = createFakeKaos({ + gethome: () => realHome, + mkdir: vi.fn(async () => {}), + writeText: vi.fn().mockResolvedValue(0), + stat: vi.fn(async (path: string) => { + if (['/repo', '/repo/.git', '/repo/packages', workDir].includes(path)) { + return stat('dir'); + } + if ([`${kimiHome}/AGENTS.md`, `${realHome}/.kimi-code/AGENTS.md`].includes(path)) { + return stat('file'); + } + throw new Error(`ENOENT ${path}`); + }), + // oxlint-disable-next-line require-yield + iterdir: async function* () { + return; + }, + readText: vi.fn(async (path: string) => { + if (path === `${kimiHome}/AGENTS.md`) return 'kimi home instructions'; + if (path === `${realHome}/.kimi-code/AGENTS.md`) return 'stale real-home instructions'; + throw new Error(`ENOENT ${path}`); + }), + }); + const session = new Session({ + id: 'test-kimi-home-agents-md', + kaos: kaos.withCwd(workDir), + homedir: '/tmp/kimi-session', + kimiHomeDir: kimiHome, + rpc: createSessionRpc(), + initializeMainAgent: false, + }); + + const created = await session.createAgent({ type: 'main' }, { profile: contextProfile() }); + + expect(created.agent.config.systemPrompt).toContain('kimi home instructions'); + expect(created.agent.config.systemPrompt).not.toContain('stale real-home instructions'); + }); + it('inherits the parent agent cwd when creating a subagent', async () => { const sessionWorkDir = '/session/work'; const parentWorkDir = '/parent/work'; @@ -1409,6 +1451,7 @@ function fakeSession( } return { agents, + options: { kimiHomeDir: undefined }, metadata: { createdAt: '2026-01-01T00:00:00.000Z', updatedAt: '2026-01-01T00:00:00.000Z', diff --git a/packages/agent-core/test/skill/scanner.test.ts b/packages/agent-core/test/skill/scanner.test.ts index 08eab3b427..be89e1b1b8 100644 --- a/packages/agent-core/test/skill/scanner.test.ts +++ b/packages/agent-core/test/skill/scanner.test.ts @@ -701,6 +701,54 @@ describe('resolveSkillRoots ordering and priority', () => { }); }); +describe('resolveSkillRoots brand home (KIMI_CODE_HOME)', () => { + it('resolves the user brand skills root under brandHomeDir while keeping generic under the real home', async () => { + const { homeDir, workDir } = await makeWorkspace(); + const brandHomeDir = path.join(homeDir, '..', 'brand-home'); + await mkdir(path.join(brandHomeDir, 'skills'), { recursive: true }); + await mkdir(path.join(homeDir, '.agents', 'skills'), { recursive: true }); + + const roots = await resolveSkillRoots({ + paths: { userHomeDir: homeDir, brandHomeDir, workDir }, + }); + + const userRoots = roots.filter((r) => r.source === 'user').map((r) => r.path); + expect(userRoots).toContain(await realpath(path.join(brandHomeDir, 'skills'))); + expect(userRoots).toContain(await realpath(path.join(homeDir, '.agents', 'skills'))); + }); + + it('never nests a second .kimi-code under the brand home', async () => { + const { homeDir, workDir } = await makeWorkspace(); + // Mirrors the default case where brandHomeDir already IS the ~/.kimi-code dir. + const brandHomeDir = path.join(homeDir, '.kimi-code'); + await mkdir(path.join(brandHomeDir, 'skills'), { recursive: true }); + // The doubled-prefix path that must never be selected. + await mkdir(path.join(brandHomeDir, '.kimi-code', 'skills'), { recursive: true }); + + const roots = await resolveSkillRoots({ + paths: { userHomeDir: homeDir, brandHomeDir, workDir }, + }); + + const userRoots = roots.filter((r) => r.source === 'user').map((r) => r.path); + expect(userRoots).toContain(await realpath(path.join(brandHomeDir, 'skills'))); + expect(userRoots).not.toContain( + await realpath(path.join(brandHomeDir, '.kimi-code', 'skills')), + ); + }); + + it('falls back to /.kimi-code/skills when brandHomeDir is omitted', async () => { + const { homeDir, workDir } = await makeWorkspace(); + await mkdir(path.join(homeDir, '.kimi-code', 'skills'), { recursive: true }); + + const roots = await resolveSkillRoots({ + paths: { userHomeDir: homeDir, workDir }, + }); + + const userRoots = roots.filter((r) => r.source === 'user').map((r) => r.path); + expect(userRoots).toContain(await realpath(path.join(homeDir, '.kimi-code', 'skills'))); + }); +}); + describe('resolveSkillRoots extra dirs', () => { it('appends extra dirs to the resolved roots', async () => { const { homeDir, repoDir, workDir } = await makeWorkspace(); diff --git a/packages/node-sdk/test/session-skills.test.ts b/packages/node-sdk/test/session-skills.test.ts index e007d431d4..1cdefabacb 100644 --- a/packages/node-sdk/test/session-skills.test.ts +++ b/packages/node-sdk/test/session-skills.test.ts @@ -198,22 +198,22 @@ describe('Session skills', () => { } }); - it('resolves user skills from the OS home directory, independently of KIMI_CODE_HOME', async () => { + it('resolves user brand skills from KIMI_CODE_HOME, not the OS home', async () => { const homeDir = await makeTempDir(tempDirs, 'kimi-sdk-skills-home-'); const processHome = await makeTempDir(tempDirs, 'kimi-sdk-skills-process-home-'); const workDir = await makeTempDir(tempDirs, 'kimi-sdk-skills-work-'); vi.stubEnv('HOME', processHome); vi.stubEnv('KIMI_CODE_HOME', homeDir); - await writeUserSkill(processHome, 'sdk-real-home-only', 'SDK real home skill'); - await writeUserSkill(homeDir, 'sdk-sandbox-only', 'SDK sandbox skill'); + await writeLegacyUserSkill(processHome, 'sdk-real-home-only', 'SDK real home skill'); + await writeBrandUserSkill(homeDir, 'sdk-sandbox-only', 'SDK sandbox skill'); const harness = createKimiHarness({ identity: TEST_IDENTITY }); try { const session = await harness.createSession({ id: 'ses_sdk_skill_env_home', workDir }); const names = new Set((await session.listSkills()).map((skill) => skill.name)); - expect(names.has('sdk-real-home-only')).toBe(true); - expect(names.has('sdk-sandbox-only')).toBe(false); + expect(names.has('sdk-real-home-only')).toBe(false); + expect(names.has('sdk-sandbox-only')).toBe(true); } finally { await harness.close(); } @@ -294,8 +294,23 @@ async function writeSkill(workDir: string, name: string, lines: readonly string[ await writeFile(join(dir, 'SKILL.md'), lines.join('\n')); } -async function writeUserSkill(userHomeDir: string, name: string, description: string): Promise { - const dir = join(userHomeDir, '.kimi-code', 'skills', name); +async function writeLegacyUserSkill( + userHomeDir: string, + name: string, + description: string, +): Promise { + await writeSkillFile(join(userHomeDir, '.kimi-code', 'skills', name), name, description); +} + +async function writeBrandUserSkill( + brandHomeDir: string, + name: string, + description: string, +): Promise { + await writeSkillFile(join(brandHomeDir, 'skills', name), name, description); +} + +async function writeSkillFile(dir: string, name: string, description: string): Promise { await mkdir(dir, { recursive: true }); await writeFile( join(dir, 'SKILL.md'),