pub mod claude;
pub mod codex;
pub mod gemini;
use crate::core::state::ForgeState;
use crate::core::task::Task;
use std::path::Path;
use std::process::Command;
pub(crate) fn pct_after(haystack: &str, prefix: &str) -> Option<u8> {
let idx = haystack.rfind(prefix)?;
let rest = &haystack[idx + prefix.len()..];
let digits: String = rest.chars().take_while(|c| c.is_ascii_digit()).collect();
digits.parse::<u32>().ok().map(|n| n.min(100) as u8)
}
pub(crate) fn pct_after_word_percent(haystack: &str, word: &str) -> Option<u8> {
let idx = haystack.rfind(word)? + word.len();
let rest = haystack[idx..].trim_start();
let digits: String = rest.chars().take_while(|c| c.is_ascii_digit()).collect();
if digits.is_empty() {
return None;
}
let after = rest[digits.len()..].trim_start();
if !after.starts_with('%') {
return None;
}
digits.parse::<u32>().ok().map(|n| n.min(100) as u8)
}
pub(crate) fn bracket_contents(line: &str) -> Vec<&str> {
let mut out = Vec::new();
let bytes = line.as_bytes();
let mut start = None;
for (i, &b) in bytes.iter().enumerate() {
if b == b'[' {
start = Some(i + 1);
} else if b == b']'
&& let Some(s) = start.take()
&& s <= i
{
out.push(&line[s..i]);
}
}
out
}
pub trait ToolAdapter {
fn name(&self) -> &str;
fn parse_ctx_pct(&self, _statusline: &str) -> Option<u8> {
None
}
fn render_config(
&self,
state: &ForgeState,
tasks: &[Task],
project_root: &Path,
) -> anyhow::Result<()>;
fn build_command(
&self,
task: &Task,
project_root: &Path,
auth_mode: &str,
permissions: &str,
) -> Command;
fn build_command_interactive(
&self,
task: &Task,
project_root: &Path,
auth_mode: &str,
permissions: &str,
) -> Command {
self.build_command(task, project_root, auth_mode, permissions)
}
fn initial_input(&self, _task: &Task) -> Option<String> {
None
}
fn initial_input_delay_ms(&self) -> u64 {
10000
}
fn ready_pattern(&self) -> Option<&str> {
None
}
fn execute_headless(
&self,
task: &Task,
project_root: &Path,
auth_mode: &str,
permissions: &str,
) -> anyhow::Result<ExecutionResult> {
let mut cmd = self.build_command(task, project_root, auth_mode, permissions);
let output = cmd.output()?;
Ok(process_output(output))
}
}
pub async fn execute_command_async(cmd: Command) -> anyhow::Result<ExecutionResult> {
let mut tokio_cmd = tokio::process::Command::from(cmd);
let output = tokio_cmd.output().await?;
Ok(process_output(output))
}
pub fn process_output(output: std::process::Output) -> ExecutionResult {
let exit_code = output.status.code().unwrap_or(-1);
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
let combined = if stderr.is_empty() {
stdout
} else {
format!("{stdout}\n\nSTDERR:\n{stderr}")
};
ExecutionResult {
success: output.status.success(),
output: combined,
exit_code,
}
}
#[derive(Debug)]
pub struct ExecutionResult {
pub success: bool,
pub output: String,
pub exit_code: i32,
}