use crate::Harness;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PromptMode {
Flag(&'static str),
KeystrokeInjection,
Unsupported,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ProcessMatcher {
Exact(&'static str),
Suffix(&'static str),
Contains(&'static str),
}
impl ProcessMatcher {
#[must_use]
pub fn matches(&self, candidate: &str) -> bool {
match self {
Self::Exact(expected) => candidate == *expected,
Self::Suffix(suffix) => candidate.ends_with(suffix),
Self::Contains(needle) => candidate.contains(needle),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct HarnessDefinition {
pub id: &'static str,
pub aliases: &'static [&'static str],
pub display_name: &'static str,
pub harness: Harness,
pub binary: &'static str,
pub default_args: &'static [&'static str],
pub model_flag: Option<&'static str>,
pub prompt_mode: PromptMode,
pub version_args: &'static [&'static str],
pub process_matchers: &'static [ProcessMatcher],
pub discovery_enabled: bool,
pub allow_bare_cmdline_match: bool,
}
const CLAUDE_MATCHERS: &[ProcessMatcher] = &[
ProcessMatcher::Exact("claude"),
ProcessMatcher::Suffix("/claude"),
ProcessMatcher::Contains("claude/versions/"),
];
const PI_MATCHERS: &[ProcessMatcher] = &[
ProcessMatcher::Suffix("/bin/pi"),
ProcessMatcher::Suffix("/pi"),
ProcessMatcher::Contains("pi-coding-agent"),
];
const CODEX_MATCHERS: &[ProcessMatcher] = &[
ProcessMatcher::Exact("codex"),
ProcessMatcher::Suffix("/codex"),
ProcessMatcher::Contains("codex-cli"),
];
const AMP_MATCHERS: &[ProcessMatcher] =
&[ProcessMatcher::Exact("amp"), ProcessMatcher::Suffix("/amp")];
const QWEN_MATCHERS: &[ProcessMatcher] = &[
ProcessMatcher::Exact("qwen"),
ProcessMatcher::Suffix("/qwen"),
ProcessMatcher::Exact("qwen-code"),
ProcessMatcher::Suffix("/qwen-code"),
];
const GEMINI_MATCHERS: &[ProcessMatcher] = &[
ProcessMatcher::Exact("gemini"),
ProcessMatcher::Suffix("/gemini"),
ProcessMatcher::Contains("gemini-cli"),
];
pub const BUILTIN_HARNESSES: &[HarnessDefinition] = &[
HarnessDefinition {
id: "claude",
aliases: &["claude_code", "claude-code", "cc"],
display_name: "Claude Code",
harness: Harness::ClaudeCode,
binary: "claude",
default_args: &[],
model_flag: Some("--model"),
prompt_mode: PromptMode::KeystrokeInjection,
version_args: &["--version"],
process_matchers: CLAUDE_MATCHERS,
discovery_enabled: true,
allow_bare_cmdline_match: true,
},
HarnessDefinition {
id: "pi",
aliases: &[],
display_name: "pi",
harness: Harness::Pi,
binary: "pi",
default_args: &[],
model_flag: Some("--model"),
prompt_mode: PromptMode::KeystrokeInjection,
version_args: &["--version"],
process_matchers: PI_MATCHERS,
discovery_enabled: true,
allow_bare_cmdline_match: false,
},
HarnessDefinition {
id: "codex",
aliases: &["codex-cli"],
display_name: "Codex CLI",
harness: Harness::Codex,
binary: "codex",
default_args: &[],
model_flag: None,
prompt_mode: PromptMode::KeystrokeInjection,
version_args: &["--version"],
process_matchers: CODEX_MATCHERS,
discovery_enabled: false,
allow_bare_cmdline_match: true,
},
HarnessDefinition {
id: "amp",
aliases: &[],
display_name: "Amp",
harness: Harness::Amp,
binary: "amp",
default_args: &[],
model_flag: None,
prompt_mode: PromptMode::KeystrokeInjection,
version_args: &["--version"],
process_matchers: AMP_MATCHERS,
discovery_enabled: false,
allow_bare_cmdline_match: true,
},
HarnessDefinition {
id: "qwen",
aliases: &["qwen-code"],
display_name: "Qwen Code",
harness: Harness::Qwen,
binary: "qwen",
default_args: &[],
model_flag: None,
prompt_mode: PromptMode::KeystrokeInjection,
version_args: &["--version"],
process_matchers: QWEN_MATCHERS,
discovery_enabled: false,
allow_bare_cmdline_match: true,
},
HarnessDefinition {
id: "gemini",
aliases: &["gemini-cli"],
display_name: "Gemini CLI",
harness: Harness::Gemini,
binary: "gemini",
default_args: &[],
model_flag: None,
prompt_mode: PromptMode::KeystrokeInjection,
version_args: &["--version"],
process_matchers: GEMINI_MATCHERS,
discovery_enabled: false,
allow_bare_cmdline_match: true,
},
];
#[must_use]
pub fn default_harness_definition() -> &'static HarnessDefinition {
if let Some(definition) = find_harness_definition("claude") {
definition
} else {
BUILTIN_HARNESSES
.iter()
.next()
.unwrap_or(&FALLBACK_HARNESS_DEFINITION)
}
}
const FALLBACK_HARNESS_DEFINITION: HarnessDefinition = HarnessDefinition {
id: "unknown",
aliases: &[],
display_name: "unknown",
harness: Harness::Unknown,
binary: "claude",
default_args: &[],
model_flag: None,
prompt_mode: PromptMode::Unsupported,
version_args: &[],
process_matchers: &[],
discovery_enabled: false,
allow_bare_cmdline_match: false,
};
#[must_use]
pub fn find_harness_definition(id: &str) -> Option<&'static HarnessDefinition> {
BUILTIN_HARNESSES
.iter()
.find(|definition| definition.id == id || definition.aliases.contains(&id))
}
pub fn builtin_harnesses() -> impl Iterator<Item = &'static HarnessDefinition> {
BUILTIN_HARNESSES.iter()
}
#[must_use]
pub fn builtin_harness_ids_display() -> String {
BUILTIN_HARNESSES
.iter()
.map(|definition| definition.id)
.collect::<Vec<_>>()
.join(", ")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn finds_default_claude_harness() {
let definition = default_harness_definition();
assert_eq!(definition.id, "claude");
assert_eq!(definition.harness, Harness::ClaudeCode);
}
#[test]
fn aliases_resolve_to_canonical_harnesses() {
assert_eq!(
find_harness_definition("claude_code").map(|d| d.id),
Some("claude")
);
assert_eq!(
find_harness_definition("qwen-code").map(|d| d.id),
Some("qwen")
);
assert_eq!(find_harness_definition("nope").map(|d| d.id), None);
}
#[test]
fn process_matchers_cover_expected_paths() {
let claude = find_harness_definition("claude").unwrap_or(default_harness_definition());
assert!(claude.process_matchers.iter().any(|m| m.matches("claude")));
assert!(claude
.process_matchers
.iter()
.any(|m| m.matches("/usr/local/bin/claude")));
let pi = find_harness_definition("pi").unwrap_or(default_harness_definition());
assert!(pi.process_matchers.iter().any(|m| m.matches("/usr/bin/pi")));
assert!(pi.discovery_enabled);
assert!(!pi.allow_bare_cmdline_match);
}
}