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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ fe2c4c8 (claude [session_id] 2025-12-02 19:25:13 -0500 142) let fro

### Supported Agents

<img src="assets/docs/badges/claude_code.svg" alt="Claude Code" height="30" /> <img src="assets/docs/badges/codex-black.svg" alt="Codex" height="30" /> <img src="assets/docs/badges/cursor.svg" alt="Cursor" height="30" /> <img src="assets/docs/badges/opencode.svg" alt="OpenCode" height="30" /> <img src="assets/docs/badges/windsurf.svg" alt="Windsurf" height="30" /> <img src="assets/docs/badges/amp.svg" alt="Amp" height="30" /> <img src="assets/docs/badges/gemini.svg" alt="Gemini" height="30" /> <img src="assets/docs/badges/copilot.svg" alt="GitHub Copilot" height="30" /> <img src="assets/docs/badges/continue.svg" alt="Continue" height="30" /> <img src="assets/docs/badges/droid.svg" alt="Droid" height="30" /> <img src="assets/docs/badges/junie_white.svg" alt="Junie" height="30" /> <img src="assets/docs/badges/rovodev.svg" alt="Rovo Dev" height="30" />
<img src="assets/docs/badges/claude_code.svg" alt="Claude Code" height="30" /> <img src="assets/docs/badges/codex-black.svg" alt="Codex" height="30" /> <img src="assets/docs/badges/cursor.svg" alt="Cursor" height="30" /> <img src="assets/docs/badges/opencode.svg" alt="OpenCode" height="30" /> <img src="assets/docs/badges/windsurf.svg" alt="Windsurf" height="30" /> <img src="assets/docs/badges/amp.svg" alt="Amp" height="30" /> <img src="assets/docs/badges/gemini.svg" alt="Gemini" height="30" /> <img src="assets/docs/badges/copilot.svg" alt="GitHub Copilot" height="30" /> <img src="assets/docs/badges/continue.svg" alt="Continue" height="30" /> <img src="assets/docs/badges/droid.svg" alt="Droid" height="30" /> <img src="assets/docs/badges/junie_white.svg" alt="Junie" height="30" /> <img src="assets/docs/badges/rovodev.svg" alt="Rovo Dev" height="30" /> <img src="assets/docs/badges/firebender.svg" alt="Firebender" height="30" />

> [+ Add support for another agent](https://usegitai.com/docs/cli/add-your-agent)

Expand Down
21 changes: 21 additions & 0 deletions assets/docs/badges/firebender.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
106 changes: 106 additions & 0 deletions src/commands/checkpoint_agent/agent_presets.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3590,3 +3590,109 @@ impl AgentCheckpointPreset for AiTabPreset {
})
}
}

// Firebender to checkpoint preset
pub struct FirebenderPreset;

#[derive(Debug, Deserialize)]
struct FirebenderHookInput {
hook_event_name: String,
model: String,
repo_working_dir: Option<String>,
workspace_roots: Option<Vec<String>>,
will_edit_filepaths: Option<Vec<String>>,
edited_filepaths: Option<Vec<String>>,
file_path: Option<String>,
completion_id: Option<String>,
dirty_files: Option<HashMap<String, String>>,
}

impl AgentCheckpointPreset for FirebenderPreset {
fn run(&self, flags: AgentCheckpointFlags) -> Result<AgentRunResult, GitAiError> {
let hook_input_json = flags.hook_input.ok_or_else(|| {
GitAiError::PresetError("hook_input is required for firebender preset".to_string())
})?;

let hook_input: FirebenderHookInput = serde_json::from_str(&hook_input_json)
.map_err(|e| GitAiError::PresetError(format!("Invalid JSON in hook_input: {}", e)))?;

let FirebenderHookInput {
hook_event_name,
model,
repo_working_dir,
workspace_roots,
will_edit_filepaths,
edited_filepaths,
file_path,
completion_id,
dirty_files,
} = hook_input;

let normalized_event = match hook_event_name.as_str() {
"before_edit" | "beforeSubmitPrompt" => "before_edit",
"after_edit" | "afterFileEdit" => "after_edit",
_ => {
return Err(GitAiError::PresetError(format!(
"Unsupported hook_event_name '{}' for firebender preset (expected 'before_edit' or 'after_edit')",
hook_event_name
)));
}
};

let mut resolved_will_edit = will_edit_filepaths;
let mut resolved_edited = edited_filepaths;
if let Some(path) = file_path.filter(|p| !p.trim().is_empty()) {
if normalized_event == "before_edit" && resolved_will_edit.is_none() {
resolved_will_edit = Some(vec![path.clone()]);
}
if normalized_event == "after_edit" && resolved_edited.is_none() {
resolved_edited = Some(vec![path]);
}
}

let repo_working_dir = repo_working_dir
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.or_else(|| workspace_roots.and_then(|roots| roots.into_iter().next()));

let model = model.trim().to_string();
if model.is_empty() {
return Err(GitAiError::PresetError(
"model must be a non-empty string for firebender preset".to_string(),
));
}

let agent_id = AgentId {
tool: "firebender".to_string(),
id: format!(
"firebender-{}",
completion_id.unwrap_or_else(|| Utc::now().timestamp_millis().to_string())
),
model,
};

if normalized_event == "before_edit" {
return Ok(AgentRunResult {
agent_id,
agent_metadata: None,
checkpoint_kind: CheckpointKind::Human,
transcript: None,
repo_working_dir,
edited_filepaths: None,
will_edit_filepaths: resolved_will_edit,
dirty_files,
});
}

Ok(AgentRunResult {
agent_id,
agent_metadata: None,
checkpoint_kind: CheckpointKind::AiAgent,
transcript: None,
repo_working_dir,
edited_filepaths: resolved_edited,
will_edit_filepaths: None,
dirty_files,
})
}
}
22 changes: 19 additions & 3 deletions src/commands/git_ai_handlers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ use crate::authorship::working_log::{AgentId, CheckpointKind};
use crate::commands;
use crate::commands::checkpoint_agent::agent_presets::{
AgentCheckpointFlags, AgentCheckpointPreset, AgentRunResult, AiTabPreset, ClaudePreset,
CodexPreset, ContinueCliPreset, CursorPreset, DroidPreset, GeminiPreset, GithubCopilotPreset,
WindsurfPreset,
CodexPreset, ContinueCliPreset, CursorPreset, DroidPreset, FirebenderPreset, GeminiPreset,
GithubCopilotPreset, WindsurfPreset,
};
use crate::commands::checkpoint_agent::agent_v1_preset::AgentV1Preset;
use crate::commands::checkpoint_agent::amp_preset::AmpPreset;
Expand Down Expand Up @@ -252,7 +252,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, ai_tab, firebender, 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]) {
}
}
}
"firebender" => {
match FirebenderPreset.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!("Firebender preset error: {}", e);
std::process::exit(0);
}
}
}
"agent-v1" => {
match AgentV1Preset.run(AgentCheckpointFlags {
hook_input: hook_input.clone(),
Expand Down
Loading