Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 25 additions & 1 deletion src/authorship/agent_detection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,18 +24,21 @@ 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)
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
Expand Down Expand Up @@ -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!(
Expand Down Expand Up @@ -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!(
Expand Down
71 changes: 71 additions & 0 deletions src/commands/checkpoint_agent/agent_presets.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<AgentRunResult, GitAiError> {
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,
})
}
}
20 changes: 18 additions & 2 deletions src/commands/git_ai_handlers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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|stdin> JSON payload required by presets, or 'stdin' to read from stdin"
Expand Down Expand Up @@ -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-{}",
Expand Down
Loading