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
616 changes: 616 additions & 0 deletions src/commands/completion.rs

Large diffs are not rendered by default.

15 changes: 15 additions & 0 deletions src/commands/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,7 @@ fn toml_path_for_key(field_name: &str) -> Option<&'static str> {
"cursor_args" => Some("launch.cursor.args"),
"kimi_args" => Some("launch.kimi.args"),
"copilot_args" => Some("launch.copilot.args"),
"hermes_args" => Some("launch.hermes.args"),
"relay" => Some("relay.url"),
"relay_id" => Some("relay.id"),
"relay_token" => Some("relay.token"),
Expand Down Expand Up @@ -1647,6 +1648,20 @@ HCOM_KIMI_ARGS - Default args passed to kimi on launch
Example: hcom config kimi_args \"--model kimi-k2.6\"
Clear: hcom config kimi_args \"\"

Prepended to launch-time cli args.",
),

"HCOM_HERMES_ARGS" => Some(
"\
HCOM_HERMES_ARGS - Default args passed to hermes on launch

Hermes runs hcom's ACP (JSON-RPC stdio) server, so the argument list must
start with `acp`; hcom validates this on launch. Extra flags (e.g. model
overrides) are appended after `acp`.

Example: hcom config hermes_args \"acp\"
Clear: hcom config hermes_args \"\"

Prepended to launch-time cli args.",
),

Expand Down
32 changes: 30 additions & 2 deletions src/commands/help.rs
Original file line number Diff line number Diff line change
Expand Up @@ -481,7 +481,7 @@ const CONFIG_HELP: &[HelpEntry] = &[
"Subagent keep-alive seconds after task",
),
(
" claude_args / gemini_args / codex_args / opencode_args / kilo_args / pi_args / omp_args / cursor_args / kimi_args / copilot_args",
" claude_args / gemini_args / codex_args / opencode_args / kilo_args / pi_args / omp_args / cursor_args / kimi_args / copilot_args / hermes_args",
"",
),
(" auto_approve", "Auto-approve safe hcom commands"),
Expand Down Expand Up @@ -613,6 +613,26 @@ const UPDATE_HELP: &[HelpEntry] = &[
("", " curl installer → re-run hcom-installer.sh"),
];

const COMPLETION_HELP: &[HelpEntry] = &[
("completion zsh", "Generate Zsh completion script to stdout"),
("", ""),
("", "Source the script to activate completions immediately:"),
("", " eval \"$(hcom completion zsh)\""),
("", ""),
("", "Install permanently:"),
("", " hcom completion zsh --install"),
(
"",
" # or: hcom completion zsh > /usr/local/share/zsh/site-functions/_hcom",
),
("", " compinit"),
("", ""),
(
"",
"Once installed, tab-complete hcom commands, tools, and flags.",
),
];

const HOOKS_HELP: &[HelpEntry] = &[
("hooks", "Show hook status"),
("hooks status", "Same as above"),
Expand Down Expand Up @@ -852,6 +872,7 @@ pub const COMMAND_NAMES: &[&str] = &[
"relay",
"run",
"update",
"completion",
"claude",
"gemini",
"codex",
Expand All @@ -868,6 +889,7 @@ pub const COMMAND_NAMES: &[&str] = &[
"cursor-agent",
"kimi",
"copilot",
"hermes",
];

fn resumable_tool_names() -> String {
Expand Down Expand Up @@ -928,7 +950,8 @@ Commands:\n\
hooks Add or remove hooks\n\
status Installation and diagnostics\n\
term View/inject into agent PTY screens\n\
update Check and apply updates",
update Check and apply updates\n\
completion Generate shell completion scripts",
env!("CARGO_PKG_VERSION"),
)
}
Expand Down Expand Up @@ -1044,6 +1067,7 @@ pub fn get_command_help(name: &str) -> String {
"run" => Some(RUN_HELP),
"status" => Some(STATUS_HELP),
"update" => Some(UPDATE_HELP),
"completion" => Some(COMPLETION_HELP),
"hooks" => None,
"term" => Some(TERM_HELP),
_ => None,
Expand Down Expand Up @@ -1159,13 +1183,17 @@ mod tests {
"term",
"relay",
"run",
"update",
"completion",
"claude",
"gemini",
"codex",
"opencode",
"kilo",
"agy",
"antigravity",
"kimi",
"hermes",
];
for cmd in commands {
let help = get_command_help(cmd);
Expand Down
3 changes: 3 additions & 0 deletions src/commands/launch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -380,6 +380,7 @@ pub(crate) fn print_launch_preview(preview: LaunchPreview<'_>) {
"cursor" | "cursor-agent" => preview.config.cursor_args.as_str(),
"copilot" => preview.config.copilot_args.as_str(),
"kimi" => preview.config.kimi_args.as_str(),
"hermes" => preview.config.hermes_args.as_str(),
_ => "",
}
} else {
Expand Down Expand Up @@ -528,6 +529,7 @@ pub(crate) fn merge_tool_args(
append_config_args(&config.cursor_args, cli_args)
}
LaunchTool::Copilot => append_config_args(&config.copilot_args, cli_args),
LaunchTool::Hermes => append_config_args(&config.hermes_args, cli_args),
LaunchTool::Pi => append_config_args(&config.pi_args, cli_args),
LaunchTool::Omp => append_config_args(&config.omp_args, cli_args),
LaunchTool::OpenCode => append_config_args(&config.opencode_args, cli_args),
Expand Down Expand Up @@ -560,6 +562,7 @@ pub(crate) fn is_background_from_args(tool: &LaunchTool, args: &[String]) -> boo
| LaunchTool::Cursor
| LaunchTool::Kimi
| LaunchTool::Copilot
| LaunchTool::Hermes
| LaunchTool::Omp => false,
}
}
Expand Down
1 change: 1 addition & 0 deletions src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ pub mod transcript;

// Management
pub mod archive;
pub mod completion;
pub mod config;
pub mod help;
pub mod hooks;
Expand Down
22 changes: 22 additions & 0 deletions src/commands/resume.rs
Original file line number Diff line number Diff line change
Expand Up @@ -438,6 +438,15 @@ fn prepare_resume_plan_from_source(
cli_tool_args
};

// Hermes ACP resume is carried out-of-band: `hermes acp` takes no resume
// CLI flag, so the session id reaches the ACP delivery loop (which issues
// `session/load`) via a host-side env var. It is set on the PTY host
// process before launch and stripped from the hermes child's env by the
// launcher's instance-state cleanup.
if tool == "hermes" && !fork {
unsafe { std::env::set_var("HCOM_HERMES_ACP_RESUME", &session_id) };
}

let mut merged_args = merged_cli_args.clone();

if launch_flags.headless && tool != "claude" && tool != "kimi" {
Expand Down Expand Up @@ -999,6 +1008,11 @@ fn build_resume_args(tool: &str, session_id: &str, fork: bool) -> Vec<String> {
let mut args = match resume_spec.resume {
ResumeArgs::Flag(flag) => vec![flag.to_string(), session_id.to_string()],
ResumeArgs::Subcommand(sub) => vec![sub.to_string(), session_id.to_string()],
// Hermes ACP: resume is session/load over JSON-RPC (the delivery loop
// issues it); `hermes acp` takes no resume CLI flag, so emit none.
// The session id is threaded to the delivery loop via the
// HCOM_HERMES_ACP_RESUME env var set at resume time.
ResumeArgs::JsonRpc => Vec::new(),
};

if fork {
Expand Down Expand Up @@ -1039,6 +1053,14 @@ fn merge_resume_args(tool: &str, original: &[String], resume: &[String]) -> Vec<
crate::tool::Tool::Cursor => merge_cursor_args(original, resume),
crate::tool::Tool::Kimi => merge_kimi_args(original, resume),
crate::tool::Tool::Copilot => merge_copilot_args(original, resume),
crate::tool::Tool::Hermes => {
// Grammar-free like Claude/Gemini/Codex: hermes adds no resume
// args at the command line (ResumeArgs::JsonRpc), so resume
// injects nothing into the stored `hermes acp …` vector.
let mut merged = original.to_vec();
merged.extend_from_slice(resume);
merged
}
crate::tool::Tool::Pi => merge_pi_args(original, resume),
crate::tool::Tool::Omp => merge_omp_args(original, resume),
crate::tool::Tool::Adhoc => {
Expand Down
1 change: 1 addition & 0 deletions src/commands/transcript.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1572,6 +1572,7 @@ mod tests {
),
("/home/user/.pi/agent/sessions/x/20260603_abc.jsonl", "pi"),
("/home/user/.omp/agent/sessions/x/20260603_abc.jsonl", "omp"),
("/home/user/.hermes/state.db", "hermes"),
];
let expected: std::collections::HashSet<&str> =
crate::integration_spec::released_tool_names()
Expand Down
9 changes: 9 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ const TOML_KEY_MAP: &[(&str, &str)] = &[
("cursor_args", "launch.cursor.args"),
("kimi_args", "launch.kimi.args"),
("copilot_args", "launch.copilot.args"),
("hermes_args", "launch.hermes.args"),
("relay", "relay.url"),
("relay_id", "relay.id"),
("relay_token", "relay.token"),
Expand Down Expand Up @@ -173,6 +174,7 @@ const FIELD_TO_ENV: &[(&str, &str)] = &[
("cursor_args", "HCOM_CURSOR_ARGS"),
("kimi_args", "HCOM_KIMI_ARGS"),
("copilot_args", "HCOM_COPILOT_ARGS"),
("hermes_args", "HCOM_HERMES_ARGS"),
("relay", "HCOM_RELAY"),
("relay_id", "HCOM_RELAY_ID"),
("relay_token", "HCOM_RELAY_TOKEN"),
Expand Down Expand Up @@ -287,6 +289,8 @@ pub struct HcomConfig {
pub cursor_args: String,
pub kimi_args: String,
pub copilot_args: String,
/// Hermes ACP specific launch arguments.
pub hermes_args: String,
pub codex_sandbox_mode: String,
pub gemini_system_prompt: String,
pub codex_system_prompt: String,
Expand Down Expand Up @@ -325,6 +329,7 @@ impl Default for HcomConfig {
cursor_args: String::new(),
kimi_args: String::new(),
copilot_args: String::new(),
hermes_args: String::new(),
codex_sandbox_mode: "workspace".to_string(),
gemini_system_prompt: String::new(),
codex_system_prompt: String::new(),
Expand Down Expand Up @@ -445,6 +450,7 @@ impl HcomConfig {
("cursor_args", &self.cursor_args),
("kimi_args", &self.kimi_args),
("copilot_args", &self.copilot_args),
("hermes_args", &self.hermes_args),
] {
if !value.is_empty()
&& let Err(e) = shell_words::split(value)
Expand Down Expand Up @@ -509,6 +515,7 @@ impl HcomConfig {
"cursor_args" => Some(self.cursor_args.clone()),
"kimi_args" => Some(self.kimi_args.clone()),
"copilot_args" => Some(self.copilot_args.clone()),
"hermes_args" => Some(self.hermes_args.clone()),
"codex_sandbox_mode" => Some(self.codex_sandbox_mode.clone()),
"gemini_system_prompt" => Some(self.gemini_system_prompt.clone()),
"codex_system_prompt" => Some(self.codex_system_prompt.clone()),
Expand Down Expand Up @@ -555,6 +562,7 @@ impl HcomConfig {
"cursor_args" => self.cursor_args = value.to_string(),
"kimi_args" => self.kimi_args = value.to_string(),
"copilot_args" => self.copilot_args = value.to_string(),
"hermes_args" => self.hermes_args = value.to_string(),
"codex_sandbox_mode" => {
// Normalize legacy value
self.codex_sandbox_mode = if value == "full-auto" {
Expand Down Expand Up @@ -686,6 +694,7 @@ impl HcomConfig {
"pi_args",
"cursor_args",
"copilot_args",
"hermes_args",
"codex_sandbox_mode",
"gemini_system_prompt",
"codex_system_prompt",
Expand Down
5 changes: 3 additions & 2 deletions src/core/filters.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ const MESSAGE_FLAGS: &[&str] = &["from", "mention", "intent", "thread", "reply_t
const LIFE_FLAGS: &[&str] = &["action"];

/// File-write tool contexts for SQL filters.
pub const FILE_WRITE_CONTEXTS: &str = "('tool:Write', 'tool:Edit', 'tool:NotebookEdit', 'tool:write_file', 'tool:replace', 'tool:apply_patch', 'tool:write', 'tool:edit', 'tool:write_to_file', 'tool:replace_file_content', 'tool:multi_replace_file_content', 'tool:StrReplace', 'tool:create')";
pub const FILE_WRITE_CONTEXTS: &str = "('tool:Write', 'tool:Edit', 'tool:NotebookEdit', 'tool:write_file', 'tool:replace', 'tool:apply_patch', 'tool:patch', 'tool:write', 'tool:edit', 'tool:write_to_file', 'tool:replace_file_content', 'tool:multi_replace_file_content', 'tool:StrReplace', 'tool:create')";

/// All file operation contexts.
pub const FILE_OP_CONTEXTS: &[&str] = &[
Expand All @@ -50,6 +50,7 @@ pub const FILE_OP_CONTEXTS: &[&str] = &[
"tool:replace",
"tool:read_file",
"tool:apply_patch",
"tool:patch",
"tool:write",
"tool:edit",
"tool:write_to_file",
Expand All @@ -60,7 +61,7 @@ pub const FILE_OP_CONTEXTS: &[&str] = &[
];

/// Shell tool contexts.
pub const SHELL_TOOL_CONTEXTS: &str = "('tool:Bash', 'tool:run_shell_command', 'tool:shell', 'tool:run_command', 'tool:Shell', 'tool:run_terminal_cmd', 'tool:execute_command', 'tool:shell_command', 'tool:bash', 'tool:powershell')";
pub const SHELL_TOOL_CONTEXTS: &str = "('tool:Bash', 'tool:run_shell_command', 'tool:shell', 'tool:run_command', 'tool:Shell', 'tool:run_terminal_cmd', 'tool:execute_command', 'tool:shell_command', 'tool:bash', 'tool:powershell', 'tool:terminal')";

/// Parsed filter values — multiple values per key (OR semantics).
pub type FilterMap = HashMap<String, Vec<String>>;
Expand Down
50 changes: 49 additions & 1 deletion src/delivery.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

#[path = "delivery/antigravity.rs"]
mod antigravity;
#[path = "delivery/hermes.rs"]
mod hermes;

use std::io::Write;
use std::net::TcpStream;
Expand Down Expand Up @@ -682,8 +684,17 @@ pub struct ScreenState {
/// set. Only ever true for Claude (see `ScreenTracker::is_claude_subagent_nav_visible`
/// / `is_claude_session_switcher_visible`).
pub nav_overlay: bool,
/// Raw bytes read from the PTY master, capped at [`RAW_OUTPUT_CAP`].
/// Appended by the PTY proxy, drained by the Hermes ACP delivery loop
/// (which parses newline-delimited JSON-RPC). Unused for other tools.
pub raw_output: Vec<u8>,
}

/// Cap for [`ScreenState::raw_output`]. The delivery loop drains frequently,
/// so this only guards against a burst between drains (a single large
/// `session/prompt` response can be hundreds of KB).
pub(crate) const RAW_OUTPUT_CAP: usize = 16 * 1024 * 1024;

impl Default for ScreenState {
fn default() -> Self {
Self {
Expand All @@ -698,6 +709,7 @@ impl Default for ScreenState {
last_prompt_submit: None,
approval_scrape_latched: false,
nav_overlay: false,
raw_output: Vec::new(),
}
}
}
Expand Down Expand Up @@ -1102,6 +1114,23 @@ pub(crate) fn inject_enter(port: u16) -> bool {
}
}

/// Inject a raw JSON-RPC line to the PTY master.
///
/// The raw-inject prefix ([`crate::pty::RAW_PREFIX`]) makes the inject server
/// pass the payload through verbatim — including the trailing newline that
/// newline-delimited JSON-RPC framing needs — and skips the interactive C0
/// filter. Used by the Hermes ACP delivery loop.
pub(crate) fn inject_raw_line(port: u16, line: &str) -> bool {
let mut payload = Vec::with_capacity(line.len() + 2);
payload.push(crate::pty::RAW_PREFIX);
payload.extend_from_slice(line.as_bytes());
payload.push(b'\n');
match TcpStream::connect(format!("127.0.0.1:{}", port)) {
Ok(mut stream) => stream.write_all(&payload).is_ok(),
Err(_) => false,
}
}

/// Fixed retry delay between gate-blocked delivery attempts.
/// TCP notify handles the fast path (instant wake on status change);
/// this is the fallback polling interval for missed notifications.
Expand Down Expand Up @@ -1321,7 +1350,25 @@ pub fn run_delivery_loop(
// After that, the plugin takes over (messages.transform for active, promptAsync for idle).
use crate::tool::Tool;
use std::str::FromStr;
if matches!(
if matches!(Tool::from_str(&config.tool), Ok(Tool::Hermes)) {
// Hermes ACP: JSON-RPC delivery loop over the raw PTY. Runs its own
// state machine (initialize -> session -> prompt) instead of the gate
// machine below; the shared launch-outcome plumbing is passed in so
// launch readiness tracks the ACP handshake.
hermes::run_hermes_acp_loop(
running,
db,
notify,
state,
&mut current_name,
&process_id,
&shared_name,
&shared_status,
&title_wake,
config,
&mut launch_outcome,
);
} else if matches!(
Tool::from_str(&config.tool),
Ok(Tool::OpenCode | Tool::Kilo | Tool::Pi | Tool::Omp)
) {
Expand Down Expand Up @@ -2334,6 +2381,7 @@ mod tests {
last_prompt_submit: None,
approval_scrape_latched: false,
nav_overlay: false,
raw_output: Vec::new(),
}
}

Expand Down
Loading
Loading