diff --git a/src/authorship/agent_detection.rs b/src/authorship/agent_detection.rs index 5980d15fd..d34d1c6f4 100644 --- a/src/authorship/agent_detection.rs +++ b/src/authorship/agent_detection.rs @@ -24,6 +24,8 @@ const AGENT_EMAIL_MAPPINGS: &[(&str, &str)] = &[ ), ("noreply@anthropic.com", "claude"), ("noreply@openai.com", "codex"), + ("noreply@moonshot.cn", "kimi-code"), + ("noreply@kimi.ai", "kimi-code"), ]; /// Known GitHub username mappings: (username, platform) @@ -31,11 +33,12 @@ const AGENT_USERNAME_MAPPINGS: &[(&str, &str)] = &[ ("copilot-swe-agent[bot]", "github-copilot"), ("devin-ai-integration[bot]", "devin"), ("cursor[bot]", "cursor"), + ("kimi-code[bot]", "kimi-code"), ]; /// Match a commit author email to a known AI agent tool name. /// -/// Returns the tool name (e.g. "cursor", "github-copilot", "devin", "claude", "codex") +/// Returns the tool name (e.g. "cursor", "github-copilot", "devin", "claude", "codex", "kimi-code") /// if the email matches a known agent pattern, or `None` otherwise. /// /// # Examples @@ -193,6 +196,19 @@ mod tests { assert_eq!(match_email_to_agent("noreply@openai.com"), Some("codex")); } + #[test] + fn test_match_email_kimi_code_moonshot() { + assert_eq!( + match_email_to_agent("noreply@moonshot.cn"), + Some("kimi-code") + ); + } + + #[test] + fn test_match_email_kimi_code_kimi() { + assert_eq!(match_email_to_agent("noreply@kimi.ai"), Some("kimi-code")); + } + #[test] fn test_match_email_case_insensitive() { assert_eq!( @@ -237,6 +253,14 @@ mod tests { assert_eq!(match_username_to_platform("cursor[bot]"), Some("cursor")); } + #[test] + fn test_match_username_kimi_code() { + assert_eq!( + match_username_to_platform("kimi-code[bot]"), + Some("kimi-code") + ); + } + #[test] fn test_match_username_case_insensitive() { assert_eq!( diff --git a/src/commands/checkpoint_agent/agent_presets.rs b/src/commands/checkpoint_agent/agent_presets.rs index dd5eba4f2..d62c76385 100644 --- a/src/commands/checkpoint_agent/agent_presets.rs +++ b/src/commands/checkpoint_agent/agent_presets.rs @@ -3585,3 +3585,74 @@ impl AgentCheckpointPreset for AiTabPreset { }) } } + +// Kimi Code (Moonshot AI) to checkpoint preset +pub struct KimiCodePreset; + +impl AgentCheckpointPreset for KimiCodePreset { + fn run(&self, flags: AgentCheckpointFlags) -> Result { + let stdin_json = flags.hook_input.ok_or_else(|| { + GitAiError::PresetError("hook_input is required for Kimi Code preset".to_string()) + })?; + + let hook_data: serde_json::Value = serde_json::from_str(&stdin_json) + .map_err(|e| GitAiError::PresetError(format!("Invalid JSON in hook_input: {}", e)))?; + + let session_id = hook_data + .get("session_id") + .and_then(|v| v.as_str()) + .ok_or_else(|| { + GitAiError::PresetError("session_id not found in hook_input".to_string()) + })?; + + let cwd = hook_data + .get("cwd") + .and_then(|v| v.as_str()) + .ok_or_else(|| GitAiError::PresetError("cwd not found in hook_input".to_string()))?; + + let model = hook_data + .get("model") + .and_then(|v| v.as_str()) + .unwrap_or("unknown"); + + let agent_id = AgentId { + tool: "kimi-code".to_string(), + id: session_id.to_string(), + model: model.to_string(), + }; + + // Extract file_path from tool_input if present + let file_path_as_vec = hook_data + .get("tool_input") + .and_then(|ti| ti.get("file_path")) + .and_then(|v| v.as_str()) + .map(|path| vec![path.to_string()]); + + // Check if this is a PreToolUse event (human checkpoint) + let hook_event_name = hook_data.get("hook_event_name").and_then(|v| v.as_str()); + + if hook_event_name == Some("PreToolUse") { + return Ok(AgentRunResult { + agent_id, + agent_metadata: None, + checkpoint_kind: CheckpointKind::Human, + transcript: None, + repo_working_dir: Some(cwd.to_string()), + edited_filepaths: None, + will_edit_filepaths: file_path_as_vec, + dirty_files: None, + }); + } + + Ok(AgentRunResult { + agent_id, + agent_metadata: None, + checkpoint_kind: CheckpointKind::AiAgent, + transcript: None, + repo_working_dir: Some(cwd.to_string()), + edited_filepaths: file_path_as_vec, + will_edit_filepaths: None, + dirty_files: None, + }) + } +} diff --git a/src/commands/git_ai_handlers.rs b/src/commands/git_ai_handlers.rs index f0b07e3e7..c65e94311 100644 --- a/src/commands/git_ai_handlers.rs +++ b/src/commands/git_ai_handlers.rs @@ -8,7 +8,7 @@ use crate::commands; use crate::commands::checkpoint_agent::agent_presets::{ AgentCheckpointFlags, AgentCheckpointPreset, AgentRunResult, AiTabPreset, ClaudePreset, CodexPreset, ContinueCliPreset, CursorPreset, DroidPreset, GeminiPreset, GithubCopilotPreset, - WindsurfPreset, + KimiCodePreset, WindsurfPreset, }; use crate::commands::checkpoint_agent::agent_v1_preset::AgentV1Preset; use crate::commands::checkpoint_agent::amp_preset::AmpPreset; @@ -202,7 +202,7 @@ fn print_help() { eprintln!("Commands:"); eprintln!(" checkpoint Checkpoint working changes and attribute author"); eprintln!( - " Presets: claude, codex, continue-cli, cursor, gemini, github-copilot, amp, windsurf, opencode, ai_tab, mock_ai" + " Presets: claude, codex, continue-cli, cursor, gemini, github-copilot, amp, windsurf, opencode, kimi-code, ai_tab, mock_ai" ); eprintln!( " --hook-input JSON payload required by presets, or 'stdin' to read from stdin" @@ -546,6 +546,22 @@ fn handle_checkpoint(args: &[String]) { } } } + "kimi-code" => { + match KimiCodePreset.run(AgentCheckpointFlags { + hook_input: hook_input.clone(), + }) { + Ok(agent_run) => { + if agent_run.repo_working_dir.is_some() { + repository_working_dir = agent_run.repo_working_dir.clone().unwrap(); + } + agent_run_result = Some(agent_run); + } + Err(e) => { + eprintln!("Kimi Code preset error: {}", e); + std::process::exit(0); + } + } + } "mock_ai" => { let mock_agent_id = format!( "ai-thread-{}",