diff --git a/INSTALL.md b/INSTALL.md index f439d4e74a..c14fbaa7cd 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -210,6 +210,29 @@ rtk init -g --no-patch # Restart Claude Code ``` +### Kimi Code CLI User +```bash +# 1. Install RTK +cargo install --git https://github.com/rtk-ai/rtk +rtk gain # Verify + +# 2. Setup for Kimi +rtk init -g --kimi +# → Answer 'y' when prompted to patch config.toml + +# 3. Restart Kimi CLI +# 4. Test: git status (should use rtk) +``` + +### CI/CD or Automation (Kimi) +```bash +# Non-interactive setup +rtk init -g --kimi --auto-patch + +# Verify in scripts +rtk init --show --kimi | grep "config.toml" +``` + ### Temporary Trial ```bash # Install hook @@ -255,6 +278,17 @@ rtk init -g --uninstall # Restart Claude Code after uninstall ``` +### Kimi Code CLI Uninstall +```bash +rtk init -g --kimi --uninstall + +# What gets removed: +# - Hook entry from ~/.kimi/config.toml +# - Context: ~/.kimi/KIMI.md + +# Restart Kimi CLI after uninstall +``` + **For Local Projects**: Manually remove RTK block from `./CLAUDE.md` ### Binary Removal @@ -391,6 +425,7 @@ Before each session: - [ ] Verify RTK is installed: `rtk --version` - [ ] If not installed → follow "Install from fork" - [ ] If project not initialized → `rtk init` +- [ ] For Kimi users → `rtk init -g --kimi` - [ ] Use `rtk` for ALL git/pnpm/test/vitest commands - [ ] Check savings: `rtk gain` diff --git a/README.md b/README.md index 1452b1ca8a..9cfc5e3bc7 100644 --- a/README.md +++ b/README.md @@ -105,6 +105,7 @@ rtk gain # Should show token savings stats rtk init -g # Claude Code / Copilot (default) rtk init -g --gemini # Gemini CLI rtk init -g --codex # Codex (OpenAI) +rtk init -g --kimi # Kimi Code CLI rtk init -g --agent cursor # Cursor rtk init --agent windsurf # Windsurf rtk init --agent cline # Cline / Roo Code @@ -350,7 +351,7 @@ rtk git status ## Supported AI Tools -RTK supports 12 AI coding tools. Each integration transparently rewrites shell commands to `rtk` equivalents for 60-90% token savings. +RTK supports 13 AI coding tools. Each integration transparently rewrites shell commands to `rtk` equivalents for 60-90% token savings. | Tool | Install | Method | |------|---------|--------| @@ -366,6 +367,7 @@ RTK supports 12 AI coding tools. Each integration transparently rewrites shell c | **OpenClaw** | `openclaw plugins install ./openclaw` | Plugin TS (before_tool_call) | | **Mistral Vibe** | Planned ([#800](https://github.com/rtk-ai/rtk/issues/800)) | Blocked on upstream | | **Kilo Code** | `rtk init --agent kilocode` | .kilocode/rules/rtk-rules.md (project-scoped) | +| **Kimi Code CLI** | `rtk init -g --kimi` | PreToolUse hook (config.toml) | | **Google Antigravity** | `rtk init --agent antigravity` | .agents/rules/antigravity-rtk-rules.md (project-scoped) | For per-agent setup details, override controls, and graceful degradation, see the [Supported Agents guide](https://www.rtk-ai.app/guide/getting-started/supported-agents). diff --git a/hooks/README.md b/hooks/README.md index 62875c0284..871c11a753 100644 --- a/hooks/README.md +++ b/hooks/README.md @@ -4,7 +4,7 @@ **Deployed hook artifacts** — the actual files installed on user machines by `rtk init`. These are shell scripts, TypeScript plugins, and rules files that run outside the Rust binary. They are **thin delegates**: parse agent-specific JSON, call `rtk rewrite` as a subprocess, format agent-specific response. Zero filtering logic lives here. -Owns: per-agent hook scripts and configuration files for 7 supported agents (Claude Code, Copilot, Cursor, Cline, Windsurf, Codex, OpenCode). +Owns: per-agent hook scripts and configuration files for 8 supported agents (Claude Code, Copilot, Cursor, Cline, Windsurf, Codex, OpenCode, Kimi). Does **not** own: hook installation/uninstallation (that's `src/hooks/init.rs`), the rewrite pattern registry (that's `discover/registry`), or integrity verification (that's `src/hooks/integrity.rs`). @@ -40,6 +40,7 @@ Each agent subdirectory has its own README with hook-specific details: - **[`windsurf/`](windsurf/README.md)** — Rules file (prompt-level), `.windsurfrules` workspace-scoped - **[`codex/`](codex/README.md)** — Awareness document, `AGENTS.md` integration, `$CODEX_HOME` or `~/.codex/` location - **[`opencode/`](opencode/README.md)** — TypeScript plugin, `zx` library, `tool.execute.before` event, in-place mutation +- **[`kimi/`](kimi/README.md)** — Native Rust binary hook, `PreToolUse` JSON format, `~/.kimi/config.toml` patching ## Supported Agents @@ -54,6 +55,7 @@ Each agent subdirectory has its own README with hook-specific details: | Windsurf | Custom instructions (rules file) | Prompt-level guidance | N/A | | Codex CLI | AGENTS.md / instructions | Prompt-level guidance | N/A | | OpenCode | TypeScript plugin (`tool.execute.before`) | In-place mutation | Yes | +| Kimi Code CLI | Rust binary (`rtk hook kimi`) | Transparent rewrite | Yes (`hookSpecificOutput`) | ## JSON Formats by Agent @@ -145,6 +147,30 @@ Returns `{}` when no rewrite (Cursor requires JSON for all paths). **No rewrite**: `{"decision": "allow"}` +### Kimi Code CLI (Rust Binary) + +**Input** (stdin): +```json +{ + "tool_name": "Shell", + "tool_input": { "command": "git status" } +} +``` + +**Output** (stdout, when rewritten): +```json +{ + "hookSpecificOutput": { + "hookEventName": "PreToolUse", + "permissionDecision": "allow", + "permissionDecisionReason": "RTK auto-rewrite", + "updatedInput": { "command": "rtk git status" } + } +} +``` + +**No rewrite**: empty stdout (exit 0) + ### OpenCode (TypeScript Plugin) Mutates `args.command` in-place via the zx library: diff --git a/hooks/kimi/README.md b/hooks/kimi/README.md new file mode 100644 index 0000000000..cbd5176dc9 --- /dev/null +++ b/hooks/kimi/README.md @@ -0,0 +1,64 @@ +# Kimi Code CLI Hook + +Native Rust binary hook for Kimi Code CLI. Reuses the Claude hook processor (`process_claude_payload`) since Kimi's `PreToolUse` JSON format and structured output are compatible. + +## Installation + +```bash +rtk init -g --kimi +``` + +This patches `~/.kimi/config.toml` to add a `PreToolUse` hook: + +```toml +[[hooks]] +event = "PreToolUse" +matcher = "Shell" +command = "rtk hook kimi" +timeout = 5 +``` + +## How It Works + +1. Kimi fires `PreToolUse` before executing any `Shell` tool call +2. Kimi passes JSON via stdin to `rtk hook kimi` +3. RTK extracts the command, looks up the rewrite registry +4. If a match is found, RTK outputs structured JSON with the rewritten command +5. Kimi executes the rewritten command (e.g., `rtk git status`) + +## JSON Format + +**Input** (stdin): +```json +{ + "tool_name": "Shell", + "tool_input": { "command": "git status" } +} +``` + +**Output** (stdout, when rewritten): +```json +{ + "hookSpecificOutput": { + "hookEventName": "PreToolUse", + "permissionDecision": "allow", + "permissionDecisionReason": "RTK auto-rewrite", + "updatedInput": { "command": "rtk git status" } + } +} +``` + +**No rewrite**: empty stdout (exit 0) — Kimi runs the original command unchanged. + +## Files + +| File | Purpose | +|------|---------| +| `~/.kimi/config.toml` | Hook registration (TOML `[[hooks]]` array) | +| `~/.kimi/KIMI.md` | Slim RTK awareness instructions | + +## Uninstall + +```bash +rtk init -g --kimi --uninstall +``` diff --git a/src/hooks/constants.rs b/src/hooks/constants.rs index fcacdb3e52..685ef308c4 100644 --- a/src/hooks/constants.rs +++ b/src/hooks/constants.rs @@ -17,3 +17,7 @@ pub const OPENCODE_PLUGIN_PATH: &str = ".config/opencode/plugins/rtk.ts"; pub const CURSOR_DIR: &str = ".cursor"; pub const CODEX_DIR: &str = ".codex"; pub const GEMINI_DIR: &str = ".gemini"; +pub const KIMI_DIR: &str = ".kimi"; + +/// Native Rust hook command for Kimi Code CLI. +pub const KIMI_HOOK_COMMAND: &str = "rtk hook kimi"; diff --git a/src/hooks/hook_cmd.rs b/src/hooks/hook_cmd.rs index cd3c82d1e1..c2e4046e33 100644 --- a/src/hooks/hook_cmd.rs +++ b/src/hooks/hook_cmd.rs @@ -282,6 +282,52 @@ fn audit_log_inner(action: &str, original: &str, rewritten: &str) -> Option<()> .ok() } +// ── Kimi Code CLI native hook ───────────────────────────────── + +/// Run the Kimi Code CLI PreToolUse hook natively. +pub fn run_kimi() -> Result<()> { + let input = read_stdin_limited()?; + + let input = input.trim(); + if input.is_empty() { + return Ok(()); + } + + let v: Value = match serde_json::from_str(input) { + Ok(v) => v, + Err(e) => { + let _ = writeln!(io::stderr(), "[rtk hook] Failed to parse JSON input: {e}"); + return Ok(()); + } + }; + + match process_claude_payload(&v) { + PayloadAction::Rewrite { + cmd, + rewritten, + output, + } => { + audit_log("rewrite", &cmd, &rewritten); + let _ = writeln!(io::stdout(), "{output}"); + } + PayloadAction::Skip { reason, cmd } => { + audit_log(reason, &cmd, ""); + } + PayloadAction::Ignore => {} + } + + Ok(()) +} + +#[cfg(test)] +fn run_kimi_inner(input: &str) -> Option { + let v: Value = serde_json::from_str(input).ok()?; + match process_claude_payload(&v) { + PayloadAction::Rewrite { output, .. } => Some(output.to_string()), + _ => None, + } +} + // ── Claude Code native hook ──────────────────────────────────── enum PayloadAction { @@ -905,4 +951,52 @@ mod tests { "cargo test should be rewritable when not denied" ); } + + // --- Kimi handler --- + + fn kimi_input(cmd: &str) -> String { + json!({ + "tool_name": "Shell", + "tool_input": { "command": cmd } + }) + .to_string() + } + + #[test] + fn test_kimi_rewrite_git_status() { + let result = run_kimi_inner(&kimi_input("git status")).unwrap(); + let v: Value = serde_json::from_str(&result).unwrap(); + let cmd = v + .pointer("/hookSpecificOutput/updatedInput/command") + .and_then(|c| c.as_str()) + .unwrap(); + assert_eq!(cmd, "rtk git status"); + } + + #[test] + fn test_kimi_passthrough_no_output() { + assert!(run_kimi_inner(&kimi_input("htop")).is_none()); + } + + #[test] + fn test_kimi_heredoc_passthrough() { + assert!(run_kimi_inner(&kimi_input("cat < Result { Ok(removed) } -/// Full uninstall for Claude, Gemini, Codex, or Cursor artifacts. -pub fn uninstall(global: bool, gemini: bool, codex: bool, cursor: bool, verbose: u8) -> Result<()> { +/// Full uninstall for Claude, Gemini, Codex, Cursor, or Kimi artifacts. +pub fn uninstall( + global: bool, + gemini: bool, + codex: bool, + cursor: bool, + kimi: bool, + verbose: u8, +) -> Result<()> { if codex { return uninstall_codex(global, verbose); } @@ -558,6 +567,23 @@ pub fn uninstall(global: bool, gemini: bool, codex: bool, cursor: bool, verbose: return Ok(()); } + if kimi { + if !global { + anyhow::bail!("Kimi uninstall only works with --global flag"); + } + let kimi_removed = uninstall_kimi(verbose)?; + if !kimi_removed.is_empty() { + println!("RTK uninstalled (Kimi):"); + for item in &kimi_removed { + println!(" - {}", item); + } + println!("\nRestart Kimi CLI to apply changes."); + } else { + println!("RTK Kimi support was not installed (nothing to remove)"); + } + return Ok(()); + } + if !global { anyhow::bail!("Uninstall only works with --global flag. For local projects, manually remove RTK from CLAUDE.md"); } @@ -2091,11 +2117,15 @@ fn remove_cursor_hook_from_json(root: &mut serde_json::Value) -> bool { } /// Show current rtk configuration -pub fn show_config(codex: bool) -> Result<()> { +pub fn show_config(codex: bool, kimi: bool) -> Result<()> { if codex { return show_codex_config(); } + if kimi { + return show_kimi_config(); + } + show_claude_config() } @@ -2386,6 +2416,55 @@ fn show_codex_config() -> Result<()> { Ok(()) } +fn show_kimi_config() -> Result<()> { + let kimi_dir = resolve_kimi_dir()?; + let kimi_md_path = kimi_dir.join(KIMI_MD); + let config_path = kimi_dir.join("config.toml"); + + println!("rtk Configuration (Kimi Code CLI):\n"); + + // Check KIMI.md + if kimi_md_path.exists() { + println!("[ok] KIMI.md: {} (slim mode)", kimi_md_path.display()); + } else { + println!("[--] KIMI.md: not found"); + } + + // Check config.toml + if config_path.exists() { + let content = fs::read_to_string(&config_path)?; + if let Ok(config) = content.parse::() { + if let Some(hooks) = config.get("hooks").and_then(|v| v.as_array()) { + if hooks.iter().any(|h| { + h.get("command") + .and_then(|v| v.as_str()) + .is_some_and(|c| c.contains("rtk hook kimi")) + }) { + println!("[ok] config.toml: RTK hook configured"); + } else { + println!("[warn] config.toml: exists but RTK hook not configured"); + println!(" Run: rtk init -g --kimi --auto-patch"); + } + } else { + println!("[--] config.toml: exists but no hooks array"); + } + } else { + println!("[warn] config.toml: exists but invalid TOML"); + } + } else { + println!("[--] config.toml: not found"); + } + + println!("\nUsage:"); + println!(" rtk init -g --kimi # Hook + KIMI.md (recommended)"); + println!(" rtk init -g --kimi --auto-patch # Same as above but no prompt"); + println!(" rtk init -g --kimi --no-patch # Skip config.toml (manual setup)"); + println!(" rtk init -g --kimi --uninstall # Remove all Kimi RTK artifacts"); + println!(" rtk init -g --kimi --hook-only # Hook only, no KIMI.md"); + + Ok(()) +} + fn run_opencode_only_mode(verbose: u8) -> Result<()> { let opencode_plugin_path = prepare_opencode_plugin_path()?; ensure_opencode_plugin_installed(&opencode_plugin_path, verbose)?; @@ -2610,6 +2689,176 @@ fn uninstall_gemini(verbose: u8) -> Result> { Ok(removed) } +// ── Kimi Code CLI support ──────────────────────────────────── + +fn resolve_kimi_dir() -> Result { + resolve_home_subdir(KIMI_DIR) +} + +/// Entry point for `rtk init -g --kimi` +pub fn run_kimi(hook_only: bool, patch_mode: PatchMode, verbose: u8) -> Result<()> { + let kimi_dir = resolve_kimi_dir()?; + fs::create_dir_all(&kimi_dir) + .with_context(|| format!("Failed to create Kimi config dir: {}", kimi_dir.display()))?; + + // 1. Install KIMI.md (RTK awareness for Kimi) + if !hook_only { + let kimi_md_path = kimi_dir.join(KIMI_MD); + write_if_changed(&kimi_md_path, RTK_SLIM, KIMI_MD, verbose)?; + } + + // 2. Patch ~/.kimi/config.toml + patch_kimi_config(&kimi_dir, patch_mode, verbose)?; + + println!("\nKimi Code CLI hook installed (global).\n"); + println!(" Hook: {}", KIMI_HOOK_COMMAND); + if !hook_only { + println!(" KIMI.md: {}", kimi_dir.join(KIMI_MD).display()); + } + println!(" Config: {}", kimi_dir.join("config.toml").display()); + println!("\n Restart Kimi CLI. Test with: git status\n"); + Ok(()) +} + +/// Patch ~/.kimi/config.toml with the PreToolUse hook +fn patch_kimi_config(kimi_dir: &Path, patch_mode: PatchMode, verbose: u8) -> Result<()> { + let config_path = kimi_dir.join("config.toml"); + + // Read or create config.toml + let mut config: toml::Value = if config_path.exists() { + let content = fs::read_to_string(&config_path) + .with_context(|| format!("Failed to read {}", config_path.display()))?; + content + .parse() + .unwrap_or(toml::Value::Table(toml::map::Map::new())) + } else { + toml::Value::Table(toml::map::Map::new()) + }; + + // Check if RTK hook already present + if let Some(toml::Value::Array(hooks)) = config.get("hooks") { + if hooks.iter().any(|h| { + h.get("command") + .and_then(|v| v.as_str()) + .is_some_and(|c| c.contains("rtk hook kimi")) + }) { + if verbose > 0 { + eprintln!("Kimi config.toml already has RTK hook"); + } + return Ok(()); + } + } + + // Ask user before patching + if patch_mode == PatchMode::Skip { + println!( + "\nManual setup needed: add RTK hook to {}\n\ + See: https://github.com/rtk-ai/rtk#kimi-code-cli", + config_path.display() + ); + return Ok(()); + } + + if patch_mode == PatchMode::Ask { + print!("Patch {} with RTK hook? [y/N] ", config_path.display()); + std::io::Write::flush(&mut std::io::stdout())?; + let mut answer = String::new(); + std::io::stdin().read_line(&mut answer)?; + if !answer.trim().eq_ignore_ascii_case("y") { + println!("Skipped. Add hook manually later."); + return Ok(()); + } + } + + // Build hook entry + let hook_entry = toml::Value::Table({ + let mut m = toml::map::Map::new(); + m.insert( + "event".to_string(), + toml::Value::String("PreToolUse".to_string()), + ); + m.insert( + "matcher".to_string(), + toml::Value::String("Shell".to_string()), + ); + m.insert( + "command".to_string(), + toml::Value::String(KIMI_HOOK_COMMAND.to_string()), + ); + m.insert("timeout".to_string(), toml::Value::Integer(5)); + m + }); + + // Insert into config + let hooks = config + .as_table_mut() + .context("config.toml is not a table")? + .entry("hooks") + .or_insert(toml::Value::Array(Vec::new())); + + hooks + .as_array_mut() + .context("hooks is not an array")? + .push(hook_entry); + + // Write atomically + let content = toml::to_string_pretty(&config)?; + let tmp = NamedTempFile::new_in(kimi_dir)?; + fs::write(tmp.path(), &content)?; + tmp.persist(&config_path) + .with_context(|| format!("Failed to write {}", config_path.display()))?; + + if verbose > 0 { + eprintln!("Patched {}", config_path.display()); + } + + Ok(()) +} + +/// Remove Kimi artifacts during uninstall +fn uninstall_kimi(verbose: u8) -> Result> { + let mut removed = Vec::new(); + let kimi_dir = match resolve_kimi_dir() { + Ok(d) => d, + Err(_) => return Ok(removed), + }; + + // Remove KIMI.md + let kimi_md = kimi_dir.join(KIMI_MD); + if kimi_md.exists() { + fs::remove_file(&kimi_md) + .with_context(|| format!("Failed to remove {}", kimi_md.display()))?; + removed.push(format!("KIMI.md: {}", kimi_md.display())); + } + + // Remove hook from config.toml + let config_path = kimi_dir.join("config.toml"); + if config_path.exists() { + let content = fs::read_to_string(&config_path)?; + if let Ok(mut config) = content.parse::() { + if let Some(arr) = config.get_mut("hooks").and_then(|v| v.as_array_mut()) { + let before = arr.len(); + arr.retain(|h| { + !h.get("command") + .and_then(|v| v.as_str()) + .is_some_and(|c| c.contains("rtk hook kimi")) + }); + if arr.len() < before { + let new_content = toml::to_string_pretty(&config)?; + fs::write(&config_path, new_content)?; + removed.push("Kimi config.toml: removed RTK hook entry".to_string()); + } + } + } + } + + if verbose > 0 && !removed.is_empty() { + eprintln!("Kimi artifacts removed"); + } + + Ok(removed) +} + // ── Copilot integration ───────────────────────────────────── const COPILOT_HOOK_JSON: &str = r#"{ @@ -3668,4 +3917,139 @@ More notes assert_eq!(arr.len(), 1); assert_eq!(arr[0]["command"].as_str().unwrap(), CURSOR_HOOK_COMMAND); } + + // --- Kimi init tests --- + + #[test] + fn test_patch_kimi_config_creates_new_file() { + let temp = TempDir::new().unwrap(); + let kimi_dir = temp.path(); + + patch_kimi_config(kimi_dir, PatchMode::Auto, 0).unwrap(); + + let config_path = kimi_dir.join("config.toml"); + assert!(config_path.exists()); + + let content = fs::read_to_string(&config_path).unwrap(); + assert!(content.contains("rtk hook kimi")); + assert!(content.contains("PreToolUse")); + assert!(content.contains("Shell")); + } + + #[test] + fn test_patch_kimi_config_is_idempotent() { + let temp = TempDir::new().unwrap(); + let kimi_dir = temp.path(); + + patch_kimi_config(kimi_dir, PatchMode::Auto, 0).unwrap(); + let first = fs::read_to_string(kimi_dir.join("config.toml")).unwrap(); + + patch_kimi_config(kimi_dir, PatchMode::Auto, 0).unwrap(); + let second = fs::read_to_string(kimi_dir.join("config.toml")).unwrap(); + + assert_eq!(first, second); + } + + #[test] + fn test_patch_kimi_config_preserves_existing_hooks() { + let temp = TempDir::new().unwrap(); + let kimi_dir = temp.path(); + let config_path = kimi_dir.join("config.toml"); + + fs::write( + &config_path, + r#"[[hooks]] +event = "PostToolUse" +matcher = "WriteFile" +command = "prettier --write" +"#, + ) + .unwrap(); + + patch_kimi_config(kimi_dir, PatchMode::Auto, 0).unwrap(); + + let content = fs::read_to_string(&config_path).unwrap(); + assert!(content.contains("prettier --write")); + assert!(content.contains("rtk hook kimi")); + } + + #[test] + fn test_uninstall_kimi_removes_artifacts() { + let temp = TempDir::new().unwrap(); + let kimi_dir = temp.path(); + let config_path = kimi_dir.join("config.toml"); + let kimi_md = kimi_dir.join(KIMI_MD); + + fs::write( + &config_path, + r#"[[hooks]] +event = "PreToolUse" +matcher = "Shell" +command = "rtk hook kimi" +timeout = 5 + +[[hooks]] +event = "PostToolUse" +matcher = "WriteFile" +command = "prettier --write" +"#, + ) + .unwrap(); + fs::write(&kimi_md, RTK_SLIM).unwrap(); + + let removed = uninstall_kimi_at(kimi_dir, 0).unwrap(); + assert_eq!(removed.len(), 2); + assert!(!kimi_md.exists()); + + let config_content = fs::read_to_string(&config_path).unwrap(); + assert!(!config_content.contains("rtk hook kimi")); + assert!(config_content.contains("prettier --write")); + } + + #[test] + fn test_uninstall_kimi_is_idempotent() { + let temp = TempDir::new().unwrap(); + let kimi_dir = temp.path(); + + let first = uninstall_kimi_at(kimi_dir, 0).unwrap(); + let second = uninstall_kimi_at(kimi_dir, 0).unwrap(); + + assert!(first.is_empty()); + assert!(second.is_empty()); + } + + // Helper to run uninstall_kimi at a specific path + fn uninstall_kimi_at(kimi_dir: &Path, _verbose: u8) -> Result> { + let mut removed = Vec::new(); + + // Remove KIMI.md + let kimi_md = kimi_dir.join(KIMI_MD); + if kimi_md.exists() { + fs::remove_file(&kimi_md)?; + removed.push(format!("KIMI.md: {}", kimi_md.display())); + } + + // Remove hook from config.toml + let config_path = kimi_dir.join("config.toml"); + if config_path.exists() { + let content = fs::read_to_string(&config_path)?; + if let Ok(mut config) = content.parse::() { + if let Some(arr) = config.get_mut("hooks").and_then(|v| v.as_array_mut()) { + let before = arr.len(); + arr.retain(|h| { + !h.get("command") + .and_then(|v| v.as_str()) + .is_some_and(|c| c.contains("rtk hook kimi")) + }); + if arr.len() < before { + let new_content = toml::to_string_pretty(&config)?; + fs::write(&config_path, new_content)?; + removed.push("Kimi config.toml: removed RTK hook entry".to_string()); + } + } + } + } + + Ok(removed) + } } diff --git a/src/main.rs b/src/main.rs index e8a19c2be2..998a3d5e30 100644 --- a/src/main.rs +++ b/src/main.rs @@ -44,6 +44,8 @@ pub enum AgentTarget { Kilocode, /// Google Antigravity Antigravity, + /// Kimi Code CLI + Kimi, } #[derive(Parser)] @@ -356,6 +358,10 @@ enum Commands { /// Install GitHub Copilot integration (VS Code + CLI) #[arg(long)] copilot: bool, + + /// Initialize for Kimi Code CLI instead of Claude Code + #[arg(long)] + kimi: bool, }, /// Download with compact output (strips progress bars) @@ -738,6 +744,8 @@ enum HookCommands { Gemini, /// Process Copilot preToolUse hook (VS Code + Copilot CLI, reads JSON from stdin) Copilot, + /// Process Kimi Code CLI PreToolUse hook (reads JSON from stdin) + Kimi, /// Check how a command would be rewritten by the hook engine (dry-run) Check { /// Target agent @@ -1716,12 +1724,13 @@ fn run_cli() -> Result { uninstall, codex, copilot, + kimi, } => { if show { - hooks::init::show_config(codex)?; + hooks::init::show_config(codex, kimi)?; } else if uninstall { let cursor = agent == Some(AgentTarget::Cursor); - hooks::init::uninstall(global, gemini, codex, cursor, cli.verbose)?; + hooks::init::uninstall(global, gemini, codex, cursor, kimi, cli.verbose)?; } else if gemini { let patch_mode = if auto_patch { hooks::init::PatchMode::Auto @@ -1733,6 +1742,18 @@ fn run_cli() -> Result { hooks::init::run_gemini(global, hook_only, patch_mode, cli.verbose)?; } else if copilot { hooks::init::run_copilot(cli.verbose)?; + } else if kimi || agent == Some(AgentTarget::Kimi) { + if !global { + anyhow::bail!("Kimi support is global-only. Use: rtk init -g --kimi"); + } + let patch_mode = if auto_patch { + hooks::init::PatchMode::Auto + } else if no_patch { + hooks::init::PatchMode::Skip + } else { + hooks::init::PatchMode::Ask + }; + hooks::init::run_kimi(hook_only, patch_mode, cli.verbose)?; } else if agent == Some(AgentTarget::Kilocode) { if global { anyhow::bail!("Kilo Code is project-scoped. Use: rtk init --agent kilocode"); @@ -2075,6 +2096,10 @@ fn run_cli() -> Result { hooks::hook_cmd::run_copilot()?; 0 } + HookCommands::Kimi => { + hooks::hook_cmd::run_kimi()?; + 0 + } HookCommands::Check { agent: _, command } => { use crate::discover::registry::rewrite_command; let raw = command.join(" ");