From e15c61e06805fbfdc802a1f4cffa98adce915d53 Mon Sep 17 00:00:00 2001 From: BB-fat <45072480+BB-fat@users.noreply.github.com> Date: Thu, 6 Aug 2026 05:05:06 +0000 Subject: [PATCH 1/2] feat(install-skill): add Kimi Code harness support Add kimi-code to the install-skill harness list: installs the bundled SKILL.md into ~/.kimi/skills (Kimi Code CLI's brand-group user skills directory, highest priority per https://github.com/MoonshotAI/kimi-cli/blob/main/docs/en/customization/skills.md). Detection signals: ~/.kimi directory and kimi/kimi-cli on PATH. Aliases: kimi, kimi-code, kimi-cli, kimicode, kimi_cli. --- crates/bsk-cli/src/skill_install/harness.rs | 37 +++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/crates/bsk-cli/src/skill_install/harness.rs b/crates/bsk-cli/src/skill_install/harness.rs index fe58ff9..324bdce 100644 --- a/crates/bsk-cli/src/skill_install/harness.rs +++ b/crates/bsk-cli/src/skill_install/harness.rs @@ -21,6 +21,7 @@ pub enum HarnessId { Workbuddy, PiAgent, Hermes, + KimiCode, } impl HarnessId { @@ -36,6 +37,7 @@ impl HarnessId { HarnessId::Workbuddy, HarnessId::PiAgent, HarnessId::Hermes, + HarnessId::KimiCode, ]; pub fn index(self) -> usize { @@ -55,6 +57,7 @@ impl HarnessId { HarnessId::Workbuddy => "workbuddy", HarnessId::PiAgent => "pi", HarnessId::Hermes => "hermes", + HarnessId::KimiCode => "kimi-code", } } @@ -71,6 +74,7 @@ impl HarnessId { HarnessId::Workbuddy => "WorkBuddy", HarnessId::PiAgent => "Pi", HarnessId::Hermes => "Hermes Agent", + HarnessId::KimiCode => "Kimi Code", } } @@ -87,6 +91,9 @@ impl HarnessId { HarnessId::Workbuddy => home.join(".workbuddy").join("skills"), HarnessId::PiAgent => home.join(".pi").join("agent").join("skills"), HarnessId::Hermes => hermes_home_for_user_home(home).join("skills"), + // Kimi Code CLI 用户级 skills 目录(brand group 最高优先级),见 + // https://github.com/MoonshotAI/kimi-cli/blob/main/docs/en/customization/skills.md + HarnessId::KimiCode => home.join(".kimi").join("skills"), } } @@ -215,6 +222,16 @@ impl HarnessId { push("`hermes` on PATH"); } } + HarnessId::KimiCode => { + if home.join(".kimi").is_dir() { + push("~/.kimi"); + } + for cmd in ["kimi", "kimi-cli"] { + if command_exists(cmd) { + signals.push(format!("`{cmd}` on PATH")); + } + } + } } let detected = !signals.is_empty(); @@ -265,6 +282,7 @@ pub fn parse_harness_id(raw: &str) -> Result { "workbuddy" | "work-buddy" => Ok(HarnessId::Workbuddy), "pi" | "pi-agent" | "pi_agent" | "piagent" => Ok(HarnessId::PiAgent), "hermes" | "hermes-agent" | "hermes_agent" | "hermesagent" => Ok(HarnessId::Hermes), + "kimi-code" | "kimi" | "kimicode" | "kimi-cli" | "kimi_cli" => Ok(HarnessId::KimiCode), other => bail!( "unknown harness '{other}'; expected one of: {}", HarnessId::ALL @@ -369,6 +387,9 @@ mod tests { ); assert_eq!(parse_harness_id("pi-agent").unwrap(), HarnessId::PiAgent); assert_eq!(parse_harness_id("hermes-agent").unwrap(), HarnessId::Hermes); + assert_eq!(parse_harness_id("kimi").unwrap(), HarnessId::KimiCode); + assert_eq!(parse_harness_id("kimi-code").unwrap(), HarnessId::KimiCode); + assert_eq!(parse_harness_id("kimi-cli").unwrap(), HarnessId::KimiCode); } #[test] @@ -424,6 +445,22 @@ mod tests { HarnessId::Hermes.skills_dir_for_home(home), PathBuf::from("/home/user/.hermes/skills") ); + assert_eq!( + HarnessId::KimiCode.skills_dir_for_home(home), + PathBuf::from("/home/user/.kimi/skills") + ); + } + + #[test] + fn detects_kimi_code_from_home_layout() { + let tmp = tempfile::TempDir::new().unwrap(); + let home = tmp.path(); + std::fs::create_dir_all(home.join(".kimi")).unwrap(); + let report = HarnessId::KimiCode.report_for_home(home); + assert!(report.detected); + assert_eq!(report.skills_dir, home.join(".kimi").join("skills")); + assert_eq!(HarnessId::KimiCode.cli_name(), "kimi-code"); + assert_eq!(HarnessId::KimiCode.display_name(), "Kimi Code"); } #[test] From 0f0d485780ede2c8954b81adac277bd5868f48c1 Mon Sep 17 00:00:00 2001 From: BB-fat <45072480+BB-fat@users.noreply.github.com> Date: Thu, 6 Aug 2026 05:17:52 +0000 Subject: [PATCH 2/2] fix(install-skill): point kimi-code harness at ~/.kimi-code and honor KIMI_CODE_HOME Kimi Code CLI's canonical repo is MoonshotAI/kimi-code: user-level skills live in $KIMI_CODE_HOME/skills (default ~/.kimi-code/skills), not kimi-cli's ~/.kimi/skills. Detection checks the kimi-code home dir (env-aware label) and the kimi binary on PATH. Serialize env-mutating kimi tests with a shared mutex to avoid a parallel-test race on KIMI_CODE_HOME. --- crates/bsk-cli/src/skill_install/harness.rs | 80 +++++++++++++++++---- 1 file changed, 68 insertions(+), 12 deletions(-) diff --git a/crates/bsk-cli/src/skill_install/harness.rs b/crates/bsk-cli/src/skill_install/harness.rs index 324bdce..5c7a0d3 100644 --- a/crates/bsk-cli/src/skill_install/harness.rs +++ b/crates/bsk-cli/src/skill_install/harness.rs @@ -91,9 +91,10 @@ impl HarnessId { HarnessId::Workbuddy => home.join(".workbuddy").join("skills"), HarnessId::PiAgent => home.join(".pi").join("agent").join("skills"), HarnessId::Hermes => hermes_home_for_user_home(home).join("skills"), - // Kimi Code CLI 用户级 skills 目录(brand group 最高优先级),见 - // https://github.com/MoonshotAI/kimi-cli/blob/main/docs/en/customization/skills.md - HarnessId::KimiCode => home.join(".kimi").join("skills"), + // Kimi Code CLI 用户级 skills 目录:`$KIMI_CODE_HOME/skills`, + // 默认 ~/.kimi-code/skills,见 + // https://github.com/MoonshotAI/kimi-code/blob/main/docs/en/customization/skills.md + HarnessId::KimiCode => kimi_code_home_for_user_home(home).join("skills"), } } @@ -223,13 +224,20 @@ impl HarnessId { } } HarnessId::KimiCode => { - if home.join(".kimi").is_dir() { - push("~/.kimi"); + let kimi_home = kimi_code_home_for_user_home(home); + if kimi_home.is_dir() { + let label = if std::env::var("KIMI_CODE_HOME") + .ok() + .is_some_and(|value| !value.trim().is_empty()) + { + format!("$KIMI_CODE_HOME ({})", kimi_home.display()) + } else { + "~/.kimi-code".to_string() + }; + push(&label); } - for cmd in ["kimi", "kimi-cli"] { - if command_exists(cmd) { - signals.push(format!("`{cmd}` on PATH")); - } + if command_exists("kimi") { + push("`kimi` on PATH"); } } } @@ -298,6 +306,18 @@ pub(crate) fn home_dir() -> Result { dirs::home_dir().ok_or_else(|| anyhow::anyhow!("could not resolve home directory")) } +/// Kimi Code CLI 的数据根目录:`KIMI_CODE_HOME` 设置时用之,否则 `~/.kimi-code` +/// (见 kimi-code 官方 skills 文档:用户级 skills 在 `$KIMI_CODE_HOME/skills`)。 +fn kimi_code_home_for_user_home(home: &Path) -> PathBuf { + if let Ok(override_home) = std::env::var("KIMI_CODE_HOME") { + let trimmed = override_home.trim(); + if !trimmed.is_empty() { + return PathBuf::from(trimmed); + } + } + home.join(".kimi-code") +} + /// Mirrors [`get_hermes_home()`](https://github.com/NousResearch/hermes-agent/blob/main/hermes_constants.py): /// `HERMES_HOME` when set, else `%LOCALAPPDATA%\\hermes` on native Windows, else `~/.hermes`. fn hermes_home_for_user_home(home: &Path) -> PathBuf { @@ -378,6 +398,12 @@ fn command_exists(name: &str) -> bool { mod tests { use super::*; + /// 读写 KIMI_CODE_HOME 环境变量的测试共用的互斥锁(Rust 测试默认并行)。 + fn kimi_env_lock() -> &'static std::sync::Mutex<()> { + static LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(()); + &LOCK + } + #[test] fn parse_aliases() { assert_eq!(parse_harness_id("codebody").unwrap(), HarnessId::Codebuddy); @@ -447,22 +473,52 @@ mod tests { ); assert_eq!( HarnessId::KimiCode.skills_dir_for_home(home), - PathBuf::from("/home/user/.kimi/skills") + PathBuf::from("/home/user/.kimi-code/skills") ); } #[test] fn detects_kimi_code_from_home_layout() { + // 与改动 KIMI_CODE_HOME 的测试互斥,避免并行读写环境变量产生竞争。 + let _guard = kimi_env_lock() + .lock() + .unwrap_or_else(|err| err.into_inner()); let tmp = tempfile::TempDir::new().unwrap(); let home = tmp.path(); - std::fs::create_dir_all(home.join(".kimi")).unwrap(); + std::fs::create_dir_all(home.join(".kimi-code")).unwrap(); let report = HarnessId::KimiCode.report_for_home(home); assert!(report.detected); - assert_eq!(report.skills_dir, home.join(".kimi").join("skills")); + assert_eq!(report.skills_dir, home.join(".kimi-code").join("skills")); assert_eq!(HarnessId::KimiCode.cli_name(), "kimi-code"); assert_eq!(HarnessId::KimiCode.display_name(), "Kimi Code"); } + #[test] + fn kimi_code_skills_dir_honors_kimi_code_home_env() { + let _guard = kimi_env_lock() + .lock() + .unwrap_or_else(|err| err.into_inner()); + let tmp = tempfile::TempDir::new().unwrap(); + let custom = tmp.path().join("custom-kimi-code"); + std::fs::create_dir_all(&custom).unwrap(); + let previous = std::env::var("KIMI_CODE_HOME").ok(); + // SAFETY: 持有 kimi_env_lock,覆盖所有读写 KIMI_CODE_HOME 的测试。 + unsafe { + std::env::set_var("KIMI_CODE_HOME", &custom); + } + let home = Path::new("/home/user"); + assert_eq!( + HarnessId::KimiCode.skills_dir_for_home(home), + custom.join("skills") + ); + unsafe { + match previous { + Some(value) => std::env::set_var("KIMI_CODE_HOME", value), + None => std::env::remove_var("KIMI_CODE_HOME"), + } + } + } + #[test] fn detects_workbuddy_from_home_layout() { let tmp = tempfile::TempDir::new().unwrap();