From 40c3fec397727180c7e9492a2c1784544ec6e967 Mon Sep 17 00:00:00 2001 From: Viktorplus <36795799+viktorplus@users.noreply.github.com> Date: Sun, 7 Jun 2026 01:27:12 +0200 Subject: [PATCH] fix: align kimi runtime support with review --- .changeset/kimi-runtime-support.md | 2 +- bin/install.js | 135 +++++++++------ docs/ARCHITECTURE.md | 14 +- docs/how-to/install-on-your-runtime.md | 16 +- src/runtime-homes.cts | 11 +- .../bug-kimi-path-layout-local-guard.test.cjs | 161 ++++++++++++++++-- tests/helpers/install-shared.cjs | 2 +- ...ler-migration-install-integration.test.cjs | 2 +- tests/kimi-tool-mapping.test.cjs | 3 + tests/multi-runtime-select.test.cjs | 2 + 10 files changed, 257 insertions(+), 91 deletions(-) diff --git a/.changeset/kimi-runtime-support.md b/.changeset/kimi-runtime-support.md index 29f265ac5..2aa33a6e7 100644 --- a/.changeset/kimi-runtime-support.md +++ b/.changeset/kimi-runtime-support.md @@ -1,5 +1,5 @@ --- type: Added -pr: 817 +pr: 743 --- **Kimi CLI runtime support is now documented and installable** — users can install global GSD Agent Skills with `--kimi --global`, invoke them as `/skill:gsd-*`, and launch the generated custom agent explicitly with `kimi --agent-file`. diff --git a/bin/install.js b/bin/install.js index 17fe3d9b6..fc983ee33 100755 --- a/bin/install.js +++ b/bin/install.js @@ -346,7 +346,7 @@ function getDirName(runtime) { if (runtime === 'trae') return '.trae'; if (runtime === 'qwen') return '.qwen'; if (runtime === 'hermes') return '.hermes'; - if (runtime === 'kimi') return '.kimi'; + if (runtime === 'kimi') return '.kimi-code'; if (runtime === 'codebuddy') return '.codebuddy'; if (runtime === 'cline') return '.cline'; return '.claude'; @@ -393,7 +393,7 @@ function getConfigDirFromHome(runtime, isGlobal) { if (runtime === 'hermes') return "'.hermes'"; if (runtime === 'codebuddy') return "'.codebuddy'"; if (runtime === 'cline') return "'.cline'"; - if (runtime === 'kimi') return "'.config', 'agents'"; + if (runtime === 'kimi') return "'.agents'"; return "'.claude'"; } @@ -684,7 +684,7 @@ if (hasUninstall) { // Show help if requested if (hasHelp) { - console.log(` ${yellow}Usage:${reset} npx ${pkg.name} [options]\n\n ${yellow}Options:${reset}\n ${cyan}-g, --global${reset} Install globally (to config directory)\n ${cyan}-l, --local${reset} Install locally (to current directory)\n ${cyan}--claude${reset} Install for Claude Code only\n ${cyan}--opencode${reset} Install for OpenCode only\n ${cyan}--gemini${reset} Install for Gemini only\n ${cyan}--kilo${reset} Install for Kilo only\n ${cyan}--codex${reset} Install for Codex only\n ${cyan}--kimi${reset} Install for Kimi CLI only\n ${cyan}--copilot${reset} Install for Copilot only\n ${cyan}--antigravity${reset} Install for Antigravity only\n ${cyan}--cursor${reset} Install for Cursor only\n ${cyan}--windsurf${reset} Install for Windsurf only\n ${cyan}--augment${reset} Install for Augment only\n ${cyan}--trae${reset} Install for Trae only\n ${cyan}--qwen${reset} Install for Qwen Code only\n ${cyan}--hermes${reset} Install for Hermes Agent only\n ${cyan}--cline${reset} Install for Cline only\n ${cyan}--codebuddy${reset} Install for CodeBuddy only\n ${cyan}--all${reset} Install for all runtimes\n ${cyan}-u, --uninstall${reset} Uninstall GSD (remove all GSD files)\n ${cyan}-c, --config-dir ${reset} Specify custom config directory\n ${cyan}-h, --help${reset} Show this help message\n ${cyan}--force-statusline${reset} Replace existing statusline config\n ${cyan}--portable-hooks${reset} Emit \$HOME-relative hook paths in settings.json\n (for WSL/Docker bind-mount setups; also GSD_PORTABLE_HOOKS=1)\n ${cyan}--profile=${reset} Install a named skill profile. Profiles:\n core — 7 main-loop skills incl. phase (~130 desc tokens)\n standard — ~13 skills incl. phase, review, config (~700)\n full — all 66 skills (default)\n Composable: --profile=core,audit installs union of closures.\n Profile is persisted and respected by \`gsd update\`.\n ${cyan}--minimal${reset} Alias for --profile=core (back-compat).\n Cuts cold-start overhead from ~12k tokens to ~700.\n Alias: --core-only.\n\n ${yellow}Examples:${reset}\n ${dim}# Interactive install (prompts for runtime and location)${reset}\n npx ${pkg.name}\n\n ${dim}# Install for Claude Code globally${reset}\n npx ${pkg.name} --claude --global\n\n ${dim}# Install for Gemini globally${reset}\n npx ${pkg.name} --gemini --global\n\n ${dim}# Install for Kilo globally${reset}\n npx ${pkg.name} --kilo --global\n\n ${dim}# Install for Codex globally${reset}\n npx ${pkg.name} --codex --global\n\n ${dim}# Install for Kimi CLI globally${reset}\n npx ${pkg.name} --kimi --global\n\n ${dim}# Install for Kimi CLI under ~/.kimi${reset}\n npx ${pkg.name} --kimi --global --config-dir ~/.kimi\n\n ${dim}# Install for Copilot globally${reset}\n npx ${pkg.name} --copilot --global\n\n ${dim}# Install for Copilot locally${reset}\n npx ${pkg.name} --copilot --local\n\n ${dim}# Install for Antigravity globally${reset}\n npx ${pkg.name} --antigravity --global\n\n ${dim}# Install for Antigravity locally${reset}\n npx ${pkg.name} --antigravity --local\n\n ${dim}# Install for Cursor globally${reset}\n npx ${pkg.name} --cursor --global\n\n ${dim}# Install for Cursor locally${reset}\n npx ${pkg.name} --cursor --local\n\n ${dim}# Install for Windsurf globally${reset}\n npx ${pkg.name} --windsurf --global\n\n ${dim}# Install for Windsurf locally${reset}\n npx ${pkg.name} --windsurf --local\n\n ${dim}# Install for Augment globally${reset}\n npx ${pkg.name} --augment --global\n\n ${dim}# Install for Augment locally${reset}\n npx ${pkg.name} --augment --local\n\n ${dim}# Install for Trae globally${reset}\n npx ${pkg.name} --trae --global\n\n ${dim}# Install for Trae locally${reset}\n npx ${pkg.name} --trae --local\n\n ${dim}# Install for Hermes Agent globally${reset}\n npx ${pkg.name} --hermes --global\n\n ${dim}# Install for Hermes Agent locally${reset}\n npx ${pkg.name} --hermes --local\n\n ${dim}# Install for Cline locally${reset}\n npx ${pkg.name} --cline --local\n\n ${dim}# Install for CodeBuddy globally${reset}\n npx ${pkg.name} --codebuddy --global\n\n ${dim}# Install for CodeBuddy locally${reset}\n npx ${pkg.name} --codebuddy --local\n\n ${dim}# Install for all runtimes globally${reset}\n npx ${pkg.name} --all --global\n\n ${dim}# Install to custom config directory${reset}\n npx ${pkg.name} --kilo --global --config-dir ~/.kilo-work\n\n ${dim}# Install to current project only${reset}\n npx ${pkg.name} --claude --local\n\n ${dim}# Uninstall GSD from Cursor globally${reset}\n npx ${pkg.name} --cursor --global --uninstall\n\n ${yellow}Notes:${reset}\n The --config-dir option is useful when you have multiple configurations.\n It takes priority over CLAUDE_CONFIG_DIR / OPENCODE_CONFIG_DIR / GEMINI_CONFIG_DIR / KILO_CONFIG_DIR / CODEX_HOME / KIMI_CONFIG_DIR / COPILOT_CONFIG_DIR / ANTIGRAVITY_CONFIG_DIR / CURSOR_CONFIG_DIR / WINDSURF_CONFIG_DIR / AUGMENT_CONFIG_DIR / TRAE_CONFIG_DIR / QWEN_CONFIG_DIR / HERMES_HOME / CLINE_CONFIG_DIR / CODEBUDDY_CONFIG_DIR environment variables.\n Kimi CLI defaults to ${cyan}~/.config/agents${reset}; use ${cyan}--config-dir ~/.kimi${reset} or ${cyan}KIMI_CONFIG_DIR=~/.kimi${reset} for brand-specific Kimi installs.\n`); + console.log(` ${yellow}Usage:${reset} npx ${pkg.name} [options]\n\n ${yellow}Options:${reset}\n ${cyan}-g, --global${reset} Install globally (to config directory)\n ${cyan}-l, --local${reset} Install locally (to current directory)\n ${cyan}--claude${reset} Install for Claude Code only\n ${cyan}--opencode${reset} Install for OpenCode only\n ${cyan}--gemini${reset} Install for Gemini only\n ${cyan}--kilo${reset} Install for Kilo only\n ${cyan}--codex${reset} Install for Codex only\n ${cyan}--kimi${reset} Install for Kimi CLI only\n ${cyan}--copilot${reset} Install for Copilot only\n ${cyan}--antigravity${reset} Install for Antigravity only\n ${cyan}--cursor${reset} Install for Cursor only\n ${cyan}--windsurf${reset} Install for Windsurf only\n ${cyan}--augment${reset} Install for Augment only\n ${cyan}--trae${reset} Install for Trae only\n ${cyan}--qwen${reset} Install for Qwen Code only\n ${cyan}--hermes${reset} Install for Hermes Agent only\n ${cyan}--cline${reset} Install for Cline only\n ${cyan}--codebuddy${reset} Install for CodeBuddy only\n ${cyan}--all${reset} Install for all runtimes\n ${cyan}-u, --uninstall${reset} Uninstall GSD (remove all GSD files)\n ${cyan}-c, --config-dir ${reset} Specify custom config directory\n ${cyan}-h, --help${reset} Show this help message\n ${cyan}--force-statusline${reset} Replace existing statusline config\n ${cyan}--portable-hooks${reset} Emit \$HOME-relative hook paths in settings.json\n (for WSL/Docker bind-mount setups; also GSD_PORTABLE_HOOKS=1)\n ${cyan}--profile=${reset} Install a named skill profile. Profiles:\n core — 7 main-loop skills incl. phase (~130 desc tokens)\n standard — ~13 skills incl. phase, review, config (~700)\n full — all 66 skills (default)\n Composable: --profile=core,audit installs union of closures.\n Profile is persisted and respected by \`gsd update\`.\n ${cyan}--minimal${reset} Alias for --profile=core (back-compat).\n Cuts cold-start overhead from ~12k tokens to ~700.\n Alias: --core-only.\n\n ${yellow}Examples:${reset}\n ${dim}# Interactive install (prompts for runtime and location)${reset}\n npx ${pkg.name}\n\n ${dim}# Install for Claude Code globally${reset}\n npx ${pkg.name} --claude --global\n\n ${dim}# Install for Gemini globally${reset}\n npx ${pkg.name} --gemini --global\n\n ${dim}# Install for Kilo globally${reset}\n npx ${pkg.name} --kilo --global\n\n ${dim}# Install for Codex globally${reset}\n npx ${pkg.name} --codex --global\n\n ${dim}# Install for Kimi CLI globally${reset}\n npx ${pkg.name} --kimi --global\n\n ${dim}# Install for Kimi CLI under ~/.kimi-code${reset}\n npx ${pkg.name} --kimi --global --config-dir ~/.kimi-code\n\n ${dim}# Install for Copilot globally${reset}\n npx ${pkg.name} --copilot --global\n\n ${dim}# Install for Copilot locally${reset}\n npx ${pkg.name} --copilot --local\n\n ${dim}# Install for Antigravity globally${reset}\n npx ${pkg.name} --antigravity --global\n\n ${dim}# Install for Antigravity locally${reset}\n npx ${pkg.name} --antigravity --local\n\n ${dim}# Install for Cursor globally${reset}\n npx ${pkg.name} --cursor --global\n\n ${dim}# Install for Cursor locally${reset}\n npx ${pkg.name} --cursor --local\n\n ${dim}# Install for Windsurf globally${reset}\n npx ${pkg.name} --windsurf --global\n\n ${dim}# Install for Windsurf locally${reset}\n npx ${pkg.name} --windsurf --local\n\n ${dim}# Install for Augment globally${reset}\n npx ${pkg.name} --augment --global\n\n ${dim}# Install for Augment locally${reset}\n npx ${pkg.name} --augment --local\n\n ${dim}# Install for Trae globally${reset}\n npx ${pkg.name} --trae --global\n\n ${dim}# Install for Trae locally${reset}\n npx ${pkg.name} --trae --local\n\n ${dim}# Install for Hermes Agent globally${reset}\n npx ${pkg.name} --hermes --global\n\n ${dim}# Install for Hermes Agent locally${reset}\n npx ${pkg.name} --hermes --local\n\n ${dim}# Install for Cline locally${reset}\n npx ${pkg.name} --cline --local\n\n ${dim}# Install for CodeBuddy globally${reset}\n npx ${pkg.name} --codebuddy --global\n\n ${dim}# Install for CodeBuddy locally${reset}\n npx ${pkg.name} --codebuddy --local\n\n ${dim}# Install for all runtimes globally${reset}\n npx ${pkg.name} --all --global\n\n ${dim}# Install to custom config directory${reset}\n npx ${pkg.name} --kilo --global --config-dir ~/.kilo-work\n\n ${dim}# Install to current project only${reset}\n npx ${pkg.name} --claude --local\n\n ${dim}# Uninstall GSD from Cursor globally${reset}\n npx ${pkg.name} --cursor --global --uninstall\n\n ${yellow}Notes:${reset}\n The --config-dir option is useful when you have multiple configurations.\n It takes priority over CLAUDE_CONFIG_DIR / OPENCODE_CONFIG_DIR / GEMINI_CONFIG_DIR / KILO_CONFIG_DIR / CODEX_HOME / KIMI_CONFIG_DIR / COPILOT_CONFIG_DIR / ANTIGRAVITY_CONFIG_DIR / CURSOR_CONFIG_DIR / WINDSURF_CONFIG_DIR / AUGMENT_CONFIG_DIR / TRAE_CONFIG_DIR / QWEN_CONFIG_DIR / HERMES_HOME / CLINE_CONFIG_DIR / CODEBUDDY_CONFIG_DIR environment variables.\n Kimi CLI defaults to ${cyan}~/.agents${reset}; use ${cyan}--config-dir ~/.kimi-code${reset} or ${cyan}KIMI_CONFIG_DIR=~/.kimi-code${reset} for brand-specific Kimi installs.\n`); process.exit(0); } @@ -1892,9 +1892,9 @@ const claudeToKimiTools = { WebFetch: 'kimi_cli.tools.web:FetchURL', FetchURL: 'kimi_cli.tools.web:FetchURL', ReadMediaFile: 'kimi_cli.tools.file:ReadMediaFile', - TaskList: 'kimi_cli.tools.task:TaskList', - TaskOutput: 'kimi_cli.tools.task:TaskOutput', - TaskStop: 'kimi_cli.tools.task:TaskStop', + TaskList: 'kimi_cli.tools.background:TaskList', + TaskOutput: 'kimi_cli.tools.background:TaskOutput', + TaskStop: 'kimi_cli.tools.background:TaskStop', }; /** @@ -6539,7 +6539,7 @@ function applyRuntimeContentRewritesInPlace(stagedDir, runtime, pathPrefix) { const fullPath = path.join(dir, entry.name); if (entry.isDirectory()) { walkAndRewrite(fullPath); - } else if (entry.name === 'SKILL.md') { + } else if (entry.name.endsWith('.md')) { let content = fs.readFileSync(fullPath, 'utf8'); content = _applyRuntimeRewrites(content, runtime, pathPrefix); fs.writeFileSync(fullPath, content); @@ -6667,6 +6667,16 @@ function _applyRuntimeRewrites(content, runtime, pathPrefix) { content = processAttribution(content, getCommitAttribution(runtime)); break; + case 'kimi': + content = content.replace(/~\/\.claude\//g, pathPrefix); + content = content.replace(/\$HOME\/\.claude\//g, pathPrefix); + content = content.replace(/\.\/\.claude\//g, `./${dirName}/`); + content = content.replace(/~\/\.claude\b/g, normalizedPathPrefix); + content = content.replace(/\$HOME\/\.claude\b/g, normalizedPathPrefix); + content = content.replace(/\.\/\.claude\b/g, `./${dirName}`); + content = processAttribution(content, getCommitAttribution(runtime)); + break; + default: // Unknown runtime — no rewrites break; @@ -6952,7 +6962,7 @@ function installRuntimeArtifacts(runtime, configDir, scope, resolvedProfile) { for (const kind of layout.kinds) { const staged = kind.stage(resolvedProfile); - if (kind.kind === 'skills') { + if (kind.kind === 'skills' || kind.kind === 'kimi-agents') { applyRuntimeContentRewritesInPlace(staged, runtime, pathPrefix); } const dest = path.join(layout.configDir, kind.destSubpath); @@ -7093,6 +7103,9 @@ function copyWithPathReplacement(srcDir, destDir, pathPrefix, runtime, isCommand content = content.replace(globalClaudeRegex, pathPrefix); content = content.replace(globalClaudeHomeRegex, pathPrefix); content = content.replace(localClaudeRegex, `./${dirName}/`); + content = content.replace(/~\/\.claude\b/g, pathPrefix.replace(/\/$/, '')); + content = content.replace(/\$HOME\/\.claude\b/g, pathPrefix.replace(/\/$/, '')); + content = content.replace(/\.\/\.claude\b/g, `./${dirName}`); content = content.replace(/~\/\.qwen\//g, pathPrefix); content = content.replace(/\$HOME\/\.qwen\//g, pathPrefix); content = content.replace(/\.\/\.qwen\//g, `./${dirName}/`); @@ -7418,6 +7431,7 @@ function uninstall(isGlobal, runtime = 'claude') { if (runtime === 'trae') runtimeLabel = 'Trae'; if (runtime === 'qwen') runtimeLabel = 'Qwen Code'; if (runtime === 'hermes') runtimeLabel = 'Hermes Agent'; + if (runtime === 'kimi') runtimeLabel = 'Kimi CLI'; if (runtime === 'codebuddy') runtimeLabel = 'CodeBuddy'; console.log(` Uninstalling GSD from ${cyan}${runtimeLabel}${reset} at ${cyan}${locationLabel}${reset}\n`); @@ -8194,6 +8208,7 @@ function writeManifest(configDir, runtime = 'claude', options = {}) { const isWindsurf = runtime === 'windsurf'; const isTrae = runtime === 'trae'; const isCline = runtime === 'cline'; + const isKimi = runtime === 'kimi'; const isHermes = runtime === 'hermes'; const gsdDir = path.join(configDir, 'gsd-core'); const commandsDir = path.join(configDir, 'commands', 'gsd'); @@ -8258,7 +8273,16 @@ function writeManifest(configDir, runtime = 'claude', options = {}) { } } } - if (fs.existsSync(agentsDir)) { + if (isKimi && fs.existsSync(agentsDir)) { + const agentHashes = generateManifest(agentsDir); + for (const [rel, hash] of Object.entries(agentHashes)) { + const isRootAgent = rel === 'gsd.yaml' || rel === 'gsd.md'; + const isSubagent = /^subagents\/gsd-[^/]+\.(yaml|md)$/.test(rel); + if (isRootAgent || isSubagent) { + manifest.files['agents/' + rel] = hash; + } + } + } else if (fs.existsSync(agentsDir)) { for (const file of fs.readdirSync(agentsDir)) { if (file.startsWith('gsd-') && (file.endsWith('.md') || file.endsWith('.toml'))) { manifest.files['agents/' + file] = fileHash(path.join(agentsDir, file)); @@ -8275,7 +8299,7 @@ function writeManifest(configDir, runtime = 'claude', options = {}) { // Track hook files so saveLocalPatches() can detect user modifications // Hooks are only installed for runtimes that use settings.json (not Codex/Copilot/Cline) - if (!isCodex && !isCopilot && !isCline) { + if (!isCodex && !isCopilot && !isCline && !isKimi) { const hooksDir = path.join(configDir, 'hooks'); if (fs.existsSync(hooksDir)) { for (const file of fs.readdirSync(hooksDir)) { @@ -8640,7 +8664,7 @@ function install(isGlobal, runtime = 'claude', options = {}) { if (isKimi && !isGlobal) { console.log(` ${yellow}⚠${reset} Kimi local install is deferred for Phase 2.`); - console.log(` No .kimi/skills or .agents/skills project artifacts were written.`); + console.log(` No .kimi-code/skills or .agents/skills project artifacts were written.`); console.log(` Project-level Kimi install semantics remain deferred.`); return { runtime, @@ -9044,40 +9068,6 @@ function install(isGlobal, runtime = 'claude', options = {}) { reportInstallerMigrationResult(installerMigrationResult); assertInstallerMigrationsUnblocked(installerMigrationResult); - if (isKimi && isGlobal) { - installRuntimeArtifacts(runtime, targetDir, 'global', _resolvedProfile); - const skillsDir = path.join(targetDir, 'skills'); - const rootAgentPath = path.join(targetDir, 'agents', 'gsd.yaml'); - const count = fs.existsSync(skillsDir) - ? fs.readdirSync(skillsDir, { withFileTypes: true }) - .filter(e => e.isDirectory() && e.name.startsWith('gsd-')).length - : 0; - if (count > 0) { - console.log(` ${green}✓${reset} Installed ${count} skills to skills/`); - } else { - throw new Error('Kimi global install produced no skills/gsd-* entries'); - } - if (fs.existsSync(rootAgentPath)) { - console.log(` ${green}✓${reset} Generated Kimi root agent: ${rootAgentPath}`); - console.log(` Launch with: kimi --agent-file ${rootAgentPath}`); - } else { - throw new Error('Kimi global install produced no agents/gsd.yaml'); - } - console.log(`\n ${green}Done!${reset} Launch Kimi with ${cyan}kimi --agent-file ${rootAgentPath}${reset}.`); - return { - runtime, - skipped: true, - reason: 'kimi_global_skills_and_agents', - configDir: targetDir, - agentPath: rootAgentPath, - settingsPath: null, - settings: null, - statuslineCommand: null, - updateBannerCommand: null, - rollbackInstallerMigrations, - }; - } - // Artifact install dispatcher — routes to layout-driven path for all // skills-based runtimes (both full and minimal/core profiles); keeps // back-compat paths for commands-based runtimes (OpenCode/Kilo/Gemini/ @@ -9127,7 +9117,25 @@ function install(isGlobal, runtime = 'claude', options = {}) { failures.push('skills/gsd/*'); } } else if (isKimi) { - console.log(` ${yellow}⚠${reset} Kimi SKILL.md conversion is deferred for Phase 2; no skills were written.`); + const skillsDir = path.join(targetDir, 'skills'); + const rootAgentPath = path.join(targetDir, 'agents', 'gsd.yaml'); + if (fs.existsSync(skillsDir)) { + const count = fs.readdirSync(skillsDir, { withFileTypes: true }) + .filter(e => e.isDirectory() && e.name.startsWith('gsd-')).length; + if (count > 0) { + console.log(` ${green}✓${reset} Installed ${count} Kimi skills to skills/`); + } else { + failures.push('skills/gsd-*'); + } + } else { + failures.push('skills/gsd-*'); + } + if (fs.existsSync(rootAgentPath)) { + console.log(` ${green}✓${reset} Generated Kimi root agent: ${rootAgentPath}`); + console.log(` Launch with: kimi --agent-file ${rootAgentPath}`); + } else { + failures.push('agents/gsd.yaml'); + } } else { const skillsDir = path.join(targetDir, 'skills'); if (fs.existsSync(skillsDir)) { @@ -9305,7 +9313,9 @@ function install(isGlobal, runtime = 'claude', options = {}) { } } - if (isMinimalMode(_effectiveInstallMode)) { + if (isKimi) { + console.log(` ${dim}↳${reset} Kimi custom agent YAML/prompt artifacts were installed via runtime artifact layout`); + } else if (isMinimalMode(_effectiveInstallMode)) { // Codex registers agents in `config.toml` via `[agents.gsd-*]` sections. // Without stripping them here, a full → minimal reinstall would leave the // runtime advertising the old full agent surface even though the agent @@ -9448,7 +9458,7 @@ function install(isGlobal, runtime = 'claude', options = {}) { failures.push('VERSION'); } - if (!isCodex && !isCopilot && !isCursor && !isWindsurf && !isTrae && !isCline) { + if (!isCodex && !isCopilot && !isCursor && !isWindsurf && !isTrae && !isCline && !isKimi) { // Write package.json to force CommonJS mode for GSD scripts // Prevents "require is not defined" errors when project has "type": "module" // Node.js walks up looking for package.json - this stops inheritance from project @@ -9542,7 +9552,7 @@ function install(isGlobal, runtime = 'claude', options = {}) { // receive the hooks/lib/ helpers either — otherwise the Codex comment downstream // ("we deliberately do *not* copy hooks/lib/ for Codex") is contradicted in practice. const hooksLibSrc = path.join(src, 'hooks', 'lib'); - if (!isCodex && !isCopilot && !isCursor && !isWindsurf && !isTrae && !isCline && fs.existsSync(hooksLibSrc)) { + if (!isCodex && !isCopilot && !isCursor && !isWindsurf && !isTrae && !isCline && !isKimi && fs.existsSync(hooksLibSrc)) { const hooksLibDest = path.join(targetDir, 'hooks', 'lib'); fs.mkdirSync(hooksLibDest, { recursive: true }); copyLibDir(hooksLibSrc, hooksLibDest, GSD_HOOK_LIB_FILES); @@ -10023,6 +10033,13 @@ function install(isGlobal, runtime = 'claude', options = {}) { return { settingsPath: null, settings: null, statuslineCommand: null, updateBannerCommand: null, runtime, configDir: targetDir }; } + if (isKimi) { + // Kimi uses Agent Skills plus explicit custom agent YAML files. It does + // not own settings.json, hooks, rules, or update-banner/statusline config. + persistActiveProfileMarker(); + return { settingsPath: null, settings: null, statuslineCommand: null, updateBannerCommand: null, runtime, configDir: targetDir }; + } + if (isCline) { // Cline uses .clinerules — generate a rules file with GSD system instructions const clinerulesDest = path.join(targetDir, '.clinerules'); @@ -10579,8 +10596,9 @@ function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallS const isWindsurf = runtime === 'windsurf'; const isTrae = runtime === 'trae'; const isCline = runtime === 'cline'; + const isKimi = runtime === 'kimi'; - if (shouldInstallStatusline && !isOpencode && !isKilo && !isCodex && !isCopilot && !isCursor && !isWindsurf && !isTrae) { + if (shouldInstallStatusline && !isOpencode && !isKilo && !isCodex && !isCopilot && !isCursor && !isWindsurf && !isTrae && !isKimi) { if (!isGlobal && !forceStatusline) { // Local installs skip statusLine by default: repo settings.json takes precedence over // profile-level settings.json in Claude Code, so writing here would silently clobber @@ -10638,7 +10656,7 @@ function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallS // {type: 'command', command: null} items that the runtime hook schema // rejects at parse time. validateHookFields filters those out so the file // we write is always schema-valid. - if (!isCodex && !isCopilot && !isKilo && !isCursor && !isWindsurf && !isTrae && !isCline) { + if (settingsPath && settings && !isCodex && !isCopilot && !isKilo && !isCursor && !isWindsurf && !isTrae && !isCline && !isKimi) { writeSettings(settingsPath, validateHookFields(settings)); } @@ -10687,6 +10705,7 @@ function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallS if (runtime === 'cline') program = 'Cline'; if (runtime === 'qwen') program = 'Qwen Code'; if (runtime === 'hermes') program = 'Hermes Agent'; + if (runtime === 'kimi') program = 'Kimi CLI'; let command = '/gsd-new-project'; if (runtime === 'opencode') command = '/gsd-new-project'; @@ -10702,6 +10721,7 @@ function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallS if (runtime === 'cline') command = '/gsd-new-project'; if (runtime === 'qwen') command = '/gsd-new-project'; if (runtime === 'hermes') command = '/gsd-new-project'; + if (runtime === 'kimi') command = '/skill:gsd-new-project'; // Claude Code global installs use the skills/ format (CC 2.1.88+). // Restart is required for CC to pick up newly-installed skills, and the @@ -10716,6 +10736,16 @@ function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallS return; } + if (runtime === 'kimi') { + const agentPath = configDir ? path.join(configDir, 'agents', 'gsd.yaml') : 'agents/gsd.yaml'; + console.log(` + ${green}Done!${reset} Start ${program} with ${cyan}kimi --agent-file ${agentPath}${reset}, then run ${cyan}${command}${reset}. + + ${cyan}Join the community:${reset} https://discord.gg/mYgfVNfA2r +`); + return; + } + console.log(` ${green}Done!${reset} Open a blank directory in ${program} and run ${cyan}${command}${reset}. @@ -10818,7 +10848,7 @@ function buildRuntimePromptText() { ${cyan}8${reset}) Cursor ${dim}(~/.cursor)${reset} ${cyan}9${reset}) Gemini ${dim}(~/.gemini)${reset} ${cyan}10${reset}) Hermes Agent ${dim}(~/.hermes)${reset} - ${cyan}11${reset}) Kimi ${dim}(~/.config/agents)${reset} + ${cyan}11${reset}) Kimi ${dim}(~/.agents)${reset} ${cyan}12${reset}) Kilo ${dim}(~/.config/kilo)${reset} ${cyan}13${reset}) OpenCode ${dim}(~/.config/opencode)${reset} ${cyan}14${reset}) Qwen Code ${dim}(~/.qwen)${reset} @@ -11288,6 +11318,7 @@ function installAllRuntimes(runtimes, isGlobal, isInteractive) { const printSummaries = () => { for (const result of results) { if (result && result.skipped) continue; + if (!result) continue; const useStatusline = statuslineRuntimes.includes(result.runtime) && shouldInstallStatusline; finishInstall( result.settingsPath, diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 0d9defddc..f16a02572 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -563,7 +563,7 @@ Equivalent paths for other runtimes: - **OpenCode:** `~/.config/opencode/` global or `./.opencode/` local - **Kilo:** `~/.config/kilo/` global or `./.kilo/` local - **Gemini CLI:** `~/.gemini/` global or `./.gemini/` local -- **Kimi CLI:** `~/.config/agents/` global; local install is deferred and guarded +- **Kimi CLI:** `~/.agents/` global; local install is deferred and guarded - **Codex:** `~/.codex/` global or `./.codex/` local - **Copilot:** `~/.copilot/` global or `./.github/` local - **Antigravity:** auto-detected global root (`~/.gemini/antigravity/`, `~/.gemini/antigravity-ide/`, or `~/.gemini/antigravity-cli/`) or `./.agent/` local @@ -802,7 +802,7 @@ The migration-specific ownership and source snapshots live in | OpenCode | `~/.config/opencode` | `./.opencode` | `command/gsd-*.md` | `agents/gsd-*.md` | `opencode.json` or `opencode.jsonc`; no GSD hooks | | Kilo | `~/.config/kilo` | `./.kilo` | `command/gsd-*.md` | `agents/gsd-*.md` | `kilo.json` or `kilo.jsonc`; no GSD hooks | | Gemini CLI | `~/.gemini` | `./.gemini` | `commands/gsd/*.toml` | `agents/gsd-*.md` | `settings.json` feature flag, hooks, and statusline | -| Kimi CLI | `~/.config/agents` | Deferred and guarded | `skills/gsd-*/SKILL.md` invoked as `/skill:gsd-*` | `agents/gsd.yaml`, `agents/gsd.md`, and `agents/subagents/gsd-*` YAML/prompt pairs | Explicit `kimi --agent-file ~/.config/agents/agents/gsd.yaml`; no GSD hooks or statusline | +| Kimi CLI | `~/.agents` | Deferred and guarded | `skills/gsd-*/SKILL.md` invoked as `/skill:gsd-*` | `agents/gsd.yaml`, `agents/gsd.md`, and `agents/subagents/gsd-*` YAML/prompt pairs | Explicit `kimi --agent-file ~/.agents/agents/gsd.yaml`; no GSD hooks or statusline | | Codex | `~/.codex` | `./.codex` | `skills/gsd-*/SKILL.md` | `agents/` source markdown plus per-agent TOML | `config.toml` `[agents.gsd-*]`, `[features].hooks` (canonical; legacy alias `codex_hooks` is recognized and migrated forward on reinstall, #3566), and hook tables | | GitHub Copilot | `~/.copilot` | `./.github` | `skills/gsd-*/SKILL.md` and `copilot-instructions.md` | `.agent.md` files | No GSD hooks or statusline | | Antigravity | auto-detected: `~/.gemini/antigravity`, `~/.gemini/antigravity-ide`, or `~/.gemini/antigravity-cli` | `./.agent` | `skills/gsd-*/SKILL.md` | `agents/gsd-*.md` | Gemini-style `settings.json` hook entries when installed by GSD | @@ -818,15 +818,17 @@ The migration-specific ownership and source snapshots live in ### Upstream Contract Sources Runtime install expectations are checked against primary documentation where -available. The current source snapshot is 2026-05-11: +available. The current source snapshot is 2026-05-11, with Kimi CLI rechecked +on 2026-06-06: - Claude Code: Anthropic slash commands, settings, hooks, and subagents docs. - OpenCode and Kilo: OpenCode config docs and Kilo custom subagent docs. - Gemini CLI and Qwen Code: command/config docs; Qwen command docs were last updated 2026-05-06. -- Kimi CLI: Agent Skills docs for `~/.config/agents/skills/` discovery and - Agents docs for YAML files, `system_prompt_path`, `kimi_cli.tools.*` module - paths, and explicit `kimi --agent-file` launch. +- Kimi CLI: Agent Skills docs for `~/.kimi-code/skills/` and + `~/.agents/skills/` user-level discovery, plus Agents docs for YAML files, + `system_prompt_path`, `kimi_cli.tools.*` module paths, and explicit + `kimi --agent-file` launch. - Codex: OpenAI Codex docs and `config-schema.json`; the installer also carries Codex 0.124.0 compatibility for agent table shape. - Copilot, Cursor, Cline, Augment, Hermes, and CodeBuddy: vendor docs for diff --git a/docs/how-to/install-on-your-runtime.md b/docs/how-to/install-on-your-runtime.md index 6edd6f2cb..cfad88a00 100644 --- a/docs/how-to/install-on-your-runtime.md +++ b/docs/how-to/install-on-your-runtime.md @@ -110,42 +110,42 @@ Skills land in `~/.codex/skills/gsd-*/SKILL.md`. Agents are written with per-age npx @opengsd/gsd-core@latest --kimi --global ``` -Skills land in `~/.config/agents/skills/gsd-*/SKILL.md`. Start a new Kimi CLI session after install, then invoke GSD skills with `/skill:gsd-*`, for example: +Skills land in `~/.agents/skills/gsd-*/SKILL.md`. Start a new Kimi CLI session after install, then invoke GSD skills with `/skill:gsd-*`, for example: ```text /skill:gsd-new-project ``` -The installer also writes the GSD custom agent definition to `~/.config/agents/agents/gsd.yaml` with its prompt at `~/.config/agents/agents/gsd.md`; subagents land under `~/.config/agents/agents/subagents/gsd-*.yaml` and `~/.config/agents/agents/subagents/gsd-*.md`. +The installer also writes the GSD custom agent definition to `~/.agents/agents/gsd.yaml` with its prompt at `~/.agents/agents/gsd.md`; subagents land under `~/.agents/agents/subagents/gsd-*.yaml` and `~/.agents/agents/subagents/gsd-*.md`. Kimi custom agents do not auto-activate just because the files exist. Launch Kimi with the generated agent file when you want the GSD agent surface: ```bash -kimi --agent-file ~/.config/agents/agents/gsd.yaml +kimi --agent-file ~/.agents/agents/gsd.yaml ``` -GSD uses Kimi's generic Agent Skills root (`~/.config/agents`) as the default so the install follows the shared agents discovery convention. Kimi also discovers user skills from the brand-specific `~/.kimi` directory. If your Kimi setup is already centered on `~/.kimi`, install there explicitly: +GSD uses Kimi's generic Agent Skills root (`~/.agents`) as the default so the install follows the shared agents discovery convention. Kimi also discovers user skills from the brand-specific `~/.kimi-code` directory. If your Kimi setup is already centered on `~/.kimi-code`, install there explicitly: ```bash -npx @opengsd/gsd-core@latest --kimi --global --config-dir ~/.kimi +npx @opengsd/gsd-core@latest --kimi --global --config-dir ~/.kimi-code ``` Then launch the generated agent from that directory: ```bash -kimi --agent-file ~/.kimi/agents/gsd.yaml +kimi --agent-file ~/.kimi-code/agents/gsd.yaml ``` **Override the install directory:** ```bash -KIMI_CONFIG_DIR=~/.config/agents-alt npx @opengsd/gsd-core@latest --kimi --global +KIMI_CONFIG_DIR=~/.agents-alt npx @opengsd/gsd-core@latest --kimi --global ``` For brand-specific scripted installs, use: ```bash -KIMI_CONFIG_DIR=~/.kimi npx @opengsd/gsd-core@latest --kimi --global +KIMI_CONFIG_DIR=~/.kimi-code npx @opengsd/gsd-core@latest --kimi --global ``` `--kimi --local` is intentionally deferred and guarded in v1; use the global install path above for Kimi CLI. diff --git a/src/runtime-homes.cts b/src/runtime-homes.cts index a775726cf..30ac2cd26 100644 --- a/src/runtime-homes.cts +++ b/src/runtime-homes.cts @@ -14,9 +14,9 @@ * cline — Rules-based; commands are embedded in .clinerules. Cline does * not use a skills/ directory. getGlobalSkillDir() returns null * for cline so the caller can emit an appropriate warning. - * kimi — Agent Skills are discovered from the generic agents root: - * ~/.config/agents/skills. ~/.kimi/skills is compatible with - * Kimi CLI, but is not GSD's canonical Phase 1 install target. + * kimi — Agent Skills are discovered from Kimi's generic user root: + * ~/.agents/skills. ~/.kimi-code/skills is compatible with + * Kimi Code CLI and can be selected with KIMI_CONFIG_DIR. */ import os from 'node:os'; @@ -128,11 +128,10 @@ export function getGlobalConfigDir(runtime: string): string { case 'cline': return env['CLINE_CONFIG_DIR'] ? expandTilde(env['CLINE_CONFIG_DIR']) : path.join(home, '.cline'); - // ── Kimi CLI (generic agents XDG root) ───────────────────────────────── + // ── Kimi CLI (generic agents user root) ──────────────────────────────── case 'kimi': { if (env['KIMI_CONFIG_DIR']) return expandTilde(env['KIMI_CONFIG_DIR']); - if (env['XDG_CONFIG_HOME']) return path.join(expandTilde(env['XDG_CONFIG_HOME']), 'agents'); - return path.join(home, '.config', 'agents'); + return path.join(home, '.agents'); } // ── OpenCode (XDG) ─────────────────────────────────────────────────────── diff --git a/tests/bug-kimi-path-layout-local-guard.test.cjs b/tests/bug-kimi-path-layout-local-guard.test.cjs index 40483a4d9..0d6fe1668 100644 --- a/tests/bug-kimi-path-layout-local-guard.test.cjs +++ b/tests/bug-kimi-path-layout-local-guard.test.cjs @@ -47,47 +47,47 @@ function withEnv(updates, fn) { } describe('Kimi runtime homes', () => { - test('canonical global skills base is ~/.config/agents/skills, not ~/.kimi/skills', () => { + test('canonical global skills base is ~/.agents/skills, not ~/.kimi-code/skills', () => { withEnv({ KIMI_CONFIG_DIR: undefined, XDG_CONFIG_HOME: undefined }, () => { assert.strictEqual( getGlobalConfigDir('kimi'), - path.join(os.homedir(), '.config', 'agents'), + path.join(os.homedir(), '.agents'), ); assert.strictEqual( getGlobalSkillsBase('kimi'), - path.join(os.homedir(), '.config', 'agents', 'skills'), + path.join(os.homedir(), '.agents', 'skills'), ); assert.strictEqual( getGlobalSkillDir('kimi', 'gsd-help'), - path.join(os.homedir(), '.config', 'agents', 'skills', 'gsd-help'), + path.join(os.homedir(), '.agents', 'skills', 'gsd-help'), ); assert.notStrictEqual( getGlobalSkillsBase('kimi'), - path.join(os.homedir(), '.kimi', 'skills'), + path.join(os.homedir(), '.kimi-code', 'skills'), ); }); }); - test('KIMI_CONFIG_DIR overrides the Kimi generic agents config root', () => { - withEnv({ KIMI_CONFIG_DIR: '/tmp/custom-kimi-agents', XDG_CONFIG_HOME: undefined }, () => { - assert.strictEqual(getGlobalConfigDir('kimi'), '/tmp/custom-kimi-agents'); + test('KIMI_CONFIG_DIR can select the brand-specific ~/.kimi-code root', () => { + withEnv({ KIMI_CONFIG_DIR: '/tmp/custom-kimi-code', XDG_CONFIG_HOME: undefined }, () => { + assert.strictEqual(getGlobalConfigDir('kimi'), '/tmp/custom-kimi-code'); assert.strictEqual( getGlobalSkillsBase('kimi'), - path.join('/tmp/custom-kimi-agents', 'skills'), + path.join('/tmp/custom-kimi-code', 'skills'), ); - assert.strictEqual(getGlobalDir('kimi'), '/tmp/custom-kimi-agents'); + assert.strictEqual(getGlobalDir('kimi'), '/tmp/custom-kimi-code'); }); }); - test('XDG_CONFIG_HOME participates in the default Kimi generic agents path', () => { + test('XDG_CONFIG_HOME does not change Kimi default root', () => { withEnv({ KIMI_CONFIG_DIR: undefined, XDG_CONFIG_HOME: '/tmp/xdg-home' }, () => { assert.strictEqual( getGlobalConfigDir('kimi'), - path.join('/tmp/xdg-home', 'agents'), + path.join(os.homedir(), '.agents'), ); assert.strictEqual( getConfigDirFromHome('kimi', true), - "'.config', 'agents'", + "'.agents'", ); }); }); @@ -138,7 +138,8 @@ describe('Kimi local install guard', () => { assert.match(combined, /Kimi local install/i); assert.match(combined, /deferred/i); - assert.ok(!fs.existsSync(path.join(tmpProject, '.kimi')), 'must not create .kimi/'); + assert.ok(!fs.existsSync(path.join(tmpProject, '.kimi')), 'must not create legacy .kimi/'); + assert.ok(!fs.existsSync(path.join(tmpProject, '.kimi-code')), 'must not create .kimi-code/'); assert.ok(!fs.existsSync(path.join(tmpProject, '.agents')), 'must not create .agents/'); assert.ok(!fs.existsSync(path.join(tmpProject, '.claude')), 'must not fall back to Claude local install'); } finally { @@ -169,15 +170,19 @@ describe('Kimi local install guard', () => { ); const combined = `${result.stdout}\n${result.stderr}`; assert.match(combined, /Installing for .*Kimi/i); - assert.match(combined, /Installed \d+ skills to skills\//i); + assert.match(combined, /Installed \d+ Kimi skills to skills\//i); assert.match(combined, /Generated Kimi root agent: .*agents.*gsd\.yaml/i); assert.match(combined, /kimi --agent-file/i); + assert.match(combined, /Wrote file manifest/i); const skillFile = path.join(tmpConfig, 'skills', 'gsd-new-project', 'SKILL.md'); assert.ok(fs.existsSync(skillFile), 'must write gsd-new-project/SKILL.md'); const skillContent = fs.readFileSync(skillFile, 'utf8'); assert.match(skillContent, /^name: gsd-new-project$/m); assert.match(skillContent, /\/skill:gsd-new-project/); + assert.match(skillContent, /gsd-core\/workflows\/new-project\.md/); + assert.doesNotMatch(skillContent, /@~\/\.claude\/gsd-core|@\$HOME\/\.claude\/gsd-core/); + assert.doesNotMatch(skillContent, /@[^\r\n]*\\/, 'serialized Kimi payload references must use forward slashes'); assert.doesNotMatch(skillContent, /kimi_cli\.tools|system_prompt_path|^version: 1$/m); const rootYaml = path.join(tmpConfig, 'agents', 'gsd.yaml'); @@ -204,8 +209,132 @@ describe('Kimi local install guard', () => { assert.match(executorYamlContent, /kimi_cli\.tools\./); assert.doesNotMatch(executorYamlContent, /mcp__/); - assert.ok(!fs.existsSync(path.join(tmpConfig, 'gsd-core')), 'must not write workflow payloads as Kimi artifacts'); + assert.ok(fs.existsSync(path.join(tmpConfig, 'gsd-core', 'workflows', 'new-project.md')), 'must write workflow payloads used by Kimi skills'); + const manifestPath = path.join(tmpConfig, 'gsd-file-manifest.json'); + assert.ok(fs.existsSync(manifestPath), 'must write gsd-file-manifest.json for Kimi installs'); + const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8')); + assert.ok(manifest.files['skills/gsd-new-project/SKILL.md'], 'manifest tracks generated Kimi skill'); + assert.ok(manifest.files['agents/gsd.yaml'], 'manifest tracks Kimi root agent YAML'); + assert.ok(manifest.files['agents/gsd.md'], 'manifest tracks Kimi root agent prompt'); + assert.ok(manifest.files['agents/subagents/gsd-executor.yaml'], 'manifest tracks Kimi subagent YAML'); + assert.ok(manifest.files['agents/subagents/gsd-executor.md'], 'manifest tracks Kimi subagent prompt'); + assert.ok(manifest.files['gsd-core/workflows/new-project.md'], 'manifest tracks installed workflow payload'); assert.ok(!fs.existsSync(path.join(tmpConfig, 'hooks')), 'must not write hooks under the Kimi root'); + assert.ok(!fs.existsSync(path.join(tmpConfig, 'settings.json')), 'must not write settings.json under the Kimi root'); + assert.ok(!fs.existsSync(path.join(tmpConfig, '.clinerules')), 'must not write rules under the Kimi root'); + } finally { + cleanup(tmpProject); + cleanup(tmpConfig); + cleanup(tmpHome); + } + }); + + test('--kimi --global backs up local edits to generated skills and agent artifacts on reinstall', () => { + const tmpProject = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-kimi-reinstall-project-')); + const tmpConfig = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-kimi-reinstall-config-')); + const tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-kimi-reinstall-home-')); + const env = installerEnv({ HOME: tmpHome, USERPROFILE: tmpHome }); + + try { + const installArgs = [INSTALL_SCRIPT, '--kimi', '--global', '--config-dir', tmpConfig, '--no-sdk']; + const first = spawnSync(process.execPath, installArgs, { + cwd: tmpProject, + encoding: 'utf8', + env, + }); + assert.strictEqual( + first.status, + 0, + `first install failed\nstdout: ${first.stdout}\nstderr: ${first.stderr}`, + ); + + const skillFile = path.join(tmpConfig, 'skills', 'gsd-new-project', 'SKILL.md'); + const agentPrompt = path.join(tmpConfig, 'agents', 'subagents', 'gsd-executor.md'); + fs.appendFileSync(skillFile, '\nUSER LOCAL KIMI SKILL EDIT\n'); + fs.appendFileSync(agentPrompt, '\nUSER LOCAL KIMI AGENT EDIT\n'); + + const second = spawnSync(process.execPath, installArgs, { + cwd: tmpProject, + encoding: 'utf8', + env, + }); + assert.strictEqual( + second.status, + 0, + `second install failed\nstdout: ${second.stdout}\nstderr: ${second.stderr}`, + ); + assert.match(`${second.stdout}\n${second.stderr}`, /locally modified GSD file/i); + + const skillBackup = path.join(tmpConfig, 'gsd-local-patches', 'skills', 'gsd-new-project', 'SKILL.md'); + const agentBackup = path.join(tmpConfig, 'gsd-local-patches', 'agents', 'subagents', 'gsd-executor.md'); + assert.match(fs.readFileSync(skillBackup, 'utf8'), /USER LOCAL KIMI SKILL EDIT/); + assert.match(fs.readFileSync(agentBackup, 'utf8'), /USER LOCAL KIMI AGENT EDIT/); + + const meta = JSON.parse(fs.readFileSync(path.join(tmpConfig, 'gsd-local-patches', 'backup-meta.json'), 'utf8')); + assert.ok(meta.files.includes('skills/gsd-new-project/SKILL.md')); + assert.ok(meta.files.includes('agents/subagents/gsd-executor.md')); + } finally { + cleanup(tmpProject); + cleanup(tmpConfig); + cleanup(tmpHome); + } + }); + + test('--kimi --global --uninstall removes GSD artifacts and preserves non-GSD Kimi content', () => { + const tmpProject = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-kimi-uninstall-project-')); + const tmpConfig = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-kimi-uninstall-config-')); + const tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-kimi-uninstall-home-')); + const env = installerEnv({ HOME: tmpHome, USERPROFILE: tmpHome }); + + try { + const installArgs = [INSTALL_SCRIPT, '--kimi', '--global', '--config-dir', tmpConfig, '--no-sdk']; + const installResult = spawnSync(process.execPath, installArgs, { + cwd: tmpProject, + encoding: 'utf8', + env, + }); + assert.strictEqual( + installResult.status, + 0, + `install failed\nstdout: ${installResult.stdout}\nstderr: ${installResult.stderr}`, + ); + + const foreignSkill = path.join(tmpConfig, 'skills', 'user-custom-skill', 'SKILL.md'); + const foreignRootAgent = path.join(tmpConfig, 'agents', 'user-agent.yaml'); + const foreignSubagent = path.join(tmpConfig, 'agents', 'subagents', 'user-agent.yaml'); + fs.mkdirSync(path.dirname(foreignSkill), { recursive: true }); + fs.mkdirSync(path.dirname(foreignSubagent), { recursive: true }); + fs.writeFileSync(foreignSkill, '# user skill\n', 'utf8'); + fs.writeFileSync(foreignRootAgent, 'version: 1\n', 'utf8'); + fs.writeFileSync(foreignSubagent, 'version: 1\n', 'utf8'); + + const uninstallResult = spawnSync( + process.execPath, + [INSTALL_SCRIPT, '--kimi', '--global', '--config-dir', tmpConfig, '--uninstall'], + { + cwd: tmpProject, + encoding: 'utf8', + env, + }, + ); + assert.strictEqual( + uninstallResult.status, + 0, + `uninstall failed\nstdout: ${uninstallResult.stdout}\nstderr: ${uninstallResult.stderr}`, + ); + assert.match(`${uninstallResult.stdout}\n${uninstallResult.stderr}`, /Kimi CLI/); + + assert.ok(!fs.existsSync(path.join(tmpConfig, 'skills', 'gsd-new-project')), 'must remove generated Kimi skills'); + assert.ok(!fs.existsSync(path.join(tmpConfig, 'agents', 'gsd.yaml')), 'must remove Kimi root agent YAML'); + assert.ok(!fs.existsSync(path.join(tmpConfig, 'agents', 'gsd.md')), 'must remove Kimi root agent prompt'); + assert.ok(!fs.existsSync(path.join(tmpConfig, 'agents', 'subagents', 'gsd-executor.yaml')), 'must remove generated Kimi subagent YAML'); + assert.ok(!fs.existsSync(path.join(tmpConfig, 'agents', 'subagents', 'gsd-executor.md')), 'must remove generated Kimi subagent prompt'); + assert.ok(!fs.existsSync(path.join(tmpConfig, 'gsd-core')), 'must remove installed workflow payload'); + assert.ok(!fs.existsSync(path.join(tmpConfig, 'gsd-file-manifest.json')), 'must remove the manifest'); + + assert.ok(fs.existsSync(foreignSkill), 'must preserve non-GSD Kimi skills'); + assert.ok(fs.existsSync(foreignRootAgent), 'must preserve non-GSD root agents'); + assert.ok(fs.existsSync(foreignSubagent), 'must preserve non-GSD subagents'); } finally { cleanup(tmpProject); cleanup(tmpConfig); diff --git a/tests/helpers/install-shared.cjs b/tests/helpers/install-shared.cjs index 12c580d91..fb499452b 100644 --- a/tests/helpers/install-shared.cjs +++ b/tests/helpers/install-shared.cjs @@ -48,7 +48,7 @@ const RUNTIME_META = { cursor: { localDir: '.cursor', globalSuffix: '.cursor' }, gemini: { localDir: '.gemini', globalSuffix: '.gemini' }, hermes: { localDir: '.hermes', globalSuffix: '.hermes' }, - kimi: { localDir: '.kimi', globalSuffix: path.join('.config', 'agents') }, + kimi: { localDir: '.kimi-code', globalSuffix: '.agents' }, kilo: { localDir: '.kilo', globalSuffix: path.join('.config', 'kilo') }, opencode: { localDir: '.opencode', globalSuffix: path.join('.config', 'opencode') }, qwen: { localDir: '.qwen', globalSuffix: '.qwen' }, diff --git a/tests/installer-migration-install-integration.test.cjs b/tests/installer-migration-install-integration.test.cjs index 304d416b0..ae047122c 100644 --- a/tests/installer-migration-install-integration.test.cjs +++ b/tests/installer-migration-install-integration.test.cjs @@ -34,7 +34,7 @@ const RUNTIME_INSTALL_CONTRACTS = { cursor: { surface: 'flat-skills', settings: false, packageJson: false }, gemini: { surface: 'commands-gsd', settings: true, packageJson: true }, hermes: { surface: 'hermes-skills', settings: true, packageJson: true }, - kimi: { surface: 'kimi-skills-agents', settings: false, packageJson: false, workflowPayload: false }, + kimi: { surface: 'kimi-skills-agents', settings: false, packageJson: false }, kilo: { surface: 'flat-command', settings: false, packageJson: true }, opencode: { surface: 'flat-command', settings: true, packageJson: true }, qwen: { surface: 'flat-skills', settings: true, packageJson: true }, diff --git a/tests/kimi-tool-mapping.test.cjs b/tests/kimi-tool-mapping.test.cjs index 324dfc4d3..ef1692f0e 100644 --- a/tests/kimi-tool-mapping.test.cjs +++ b/tests/kimi-tool-mapping.test.cjs @@ -32,6 +32,9 @@ describe('convertKimiToolName', () => { ['TodoWrite', 'kimi_cli.tools.todo:SetTodoList'], ['WebSearch', 'kimi_cli.tools.web:SearchWeb'], ['WebFetch', 'kimi_cli.tools.web:FetchURL'], + ['TaskList', 'kimi_cli.tools.background:TaskList'], + ['TaskOutput', 'kimi_cli.tools.background:TaskOutput'], + ['TaskStop', 'kimi_cli.tools.background:TaskStop'], ]); for (const [claudeTool, kimiTool] of expectedMappings) { diff --git a/tests/multi-runtime-select.test.cjs b/tests/multi-runtime-select.test.cjs index 8b2efa530..771440a52 100644 --- a/tests/multi-runtime-select.test.cjs +++ b/tests/multi-runtime-select.test.cjs @@ -187,6 +187,8 @@ describe('install.js exports multi-select runtime metadata', () => { 'prompt lists Hermes Agent as option 10'); assert.ok(/\b11\)\s*Kimi\b/.test(prompt), 'prompt lists Kimi as option 11'); + assert.ok(/Kimi\s+\(~\/\.agents\)/.test(prompt), + 'prompt shows the canonical Kimi global root'); assert.ok(/\b14\)\s*Qwen Code\b/.test(prompt), 'prompt lists Qwen Code as option 14'); assert.ok(/\b15\)\s*Trae\b/.test(prompt),