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
38 changes: 34 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

> **Hook your coding agents together**

`hcom` is a CLI that agents can use to message, watch, and spawn each other across terminals. It integrates with Claude Code, Gemini, Codex, and OpenCode without changing how you use them.
`hcom` is a CLI that agents can use to message, watch, and spawn each other across terminals. It integrates with Claude Code, Gemini, Codex, OpenCode, and Kilo Code without changing how you use them.

Use it to coordinate pipelines, run different AI CLIs as each other's subagents, or just instead of copy-paste.

Expand Down Expand Up @@ -43,7 +43,7 @@ uv tool install hcom # or: pip install hcom
Terminal 1:

```bash
hcom claude # codex / gemini / opencode
hcom claude # codex / gemini / opencode / kilo
```

Terminal 2:
Expand Down Expand Up @@ -216,6 +216,7 @@ brew uninstall hcom # or: rm $(which hcom)
| Gemini CLI | automatic | `hcom gemini` |
| Codex CLI | automatic | `hcom codex` |
| OpenCode | automatic | `hcom opencode` |
| Kilo Code | automatic | `hcom kilo` |
| Anything else | manual via `hcom listen` | `hcom start` (run inside tool) |

```bash
Expand Down Expand Up @@ -248,7 +249,7 @@ What you might type from a shell. Agents run their own commands that they learn
### Spawn

```bash
hcom [N] claude|gemini|codex|opencode # launch N agents
hcom [N] claude|gemini|codex|opencode|kilo # launch N agents
hcom r <name|session_id> # resume agent
hcom f <name|session_id> # fork session
hcom kill <name|tag:T|all> # kill + close terminal pane
Expand Down Expand Up @@ -311,7 +312,7 @@ hcom config -i <name> <key> <value> # per-agent override at runtime
| `terminal` | Where new agent windows open (`hcom config terminal --info`) |
| `timeout` | Idle timeout for headless/vanilla Claude (seconds) |
| `subagent_timeout` | Keep-alive for Claude subagents (seconds) |
| `claude_args` / `gemini_args` / `codex_args` / `opencode_args` | Default args passed to the tool |
| `claude_args` / `gemini_args` / `codex_args` / `opencode_args` / `kilo_args` | Default args passed to the tool |

### Scope

Expand Down Expand Up @@ -399,6 +400,35 @@ For concurrent worktrees, scope each to its own DB:
HCOM_DIR=$PWD/.hcom HCOM_DEV_ROOT=$PWD hcom claude
```

### Testing a fork alongside the original

To test a custom fork (e.g., with kilo support) without replacing your installed `hcom`:

```bash
# 1. Clone your fork
git clone https://github.com/Solar2004/hcom.git ~/hcom-kilo
cd ~/hcom-kilo
cargo build

# 2. Use dev_root to point at the fork build
hcom config dev_root $(pwd)
hcom status # now runs your fork

# 3. Revert to original anytime
hcom config dev_root --unset
```

Or use a symlink swap:

```bash
# Point to your fork build
ln -sf ~/hcom-kilo/target/debug/hcom ~/.cargo/bin/hcom-kilo

# Run either
hcom # original installed version
hcom-kilo # your fork
```

</details>


Expand Down
6 changes: 3 additions & 3 deletions src/bootstrap.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,10 +63,10 @@ You MUST use `hcom <cmd+flags> --name {instance_name}` for all hcom commands:
Filters (same flag=OR, different=AND): --agent NAME | --type message|status|life | --status listening|active|blocked | --cmd PATTERN (contains, ^prefix, =exact) | --file PATH (*.py for glob, file.py for contains)
Event-based notifications, watch agents, subscribe, react: events sub [filters] | --help
- Handoff context: bundle prepare
- Spawn agents: [num] <claude|gemini|codex|opencode> [--tag labelOrGroup] [--terminal tmux|kitty|wezterm|etc]
- Spawn agents: [num] <claude|gemini|codex|opencode|kilo> [--tag labelOrGroup] [--terminal tmux|kitty|wezterm|etc]
Example: `hcom 1 claude --tag cool` -> automatic <hcom> msg when ready -> send it task via hcom send
Resume: hcom r <name> [args] | Fork: hcom f <name> [args] | Kill: hcom kill <name(s)>
background, set prompt, system, forward args: <claude|gemini|codex|opencode> --help
background, set prompt, system, forward args: <claude|gemini|codex|opencode|kilo> --help
- Run workflows: run <script> [args] [--help]
{scripts}
- View agent screen: term [name] | inject text/enter: term inject <name> ['text'] [--enter]
Expand Down Expand Up @@ -412,7 +412,7 @@ pub fn get_bootstrap(

// Tool-specific delivery
if tool == "claude"
|| ((tool == "codex" || tool == "gemini" || tool == "opencode") && ctx.is_launched)
|| ((tool == "codex" || tool == "gemini" || tool == "opencode" || tool == "kilo") && ctx.is_launched)
{
parts.push(DELIVERY_AUTO);
} else {
Expand Down
68 changes: 56 additions & 12 deletions src/commands/help.rs
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ const LIST_HELP: &[HelpEntry] = &[
("Tool labels:", ""),
(
"",
"[CLAUDE] [GEMINI] [CODEX] [OPENCODE] hcom-launched (PTY + hooks)",
"[CLAUDE] [GEMINI] [CODEX] [OPENCODE] [KILO] [KILOCODE] [CLINE] [CLINECODE] hcom-launched (PTY + hooks)",
),
(
"",
Expand Down Expand Up @@ -432,7 +432,7 @@ const RESET_HELP: &[HelpEntry] = &[
),
(
"",
" HCOM_DIR=$PWD/.hcom -> $PWD/.claude, .gemini, .codex, .opencode",
" HCOM_DIR=$PWD/.hcom -> $PWD/.claude, .gemini, .codex, .opencode, .kilo, .kilocode, .cline, .clinecode",
),
("", ""),
("", "To remove local setup:"),
Expand Down Expand Up @@ -466,7 +466,7 @@ const CONFIG_HELP: &[HelpEntry] = &[
"Subagent keep-alive seconds after task",
),
(
" claude_args / gemini_args / codex_args / opencode_args",
" claude_args / gemini_args / codex_args / opencode_args / kilo_args / cline_args",
"",
),
(" auto_approve", "Auto-approve safe hcom commands"),
Expand Down Expand Up @@ -530,7 +530,7 @@ const TRANSCRIPT_HELP: &[HelpEntry] = &[
(" --limit N", "Max results (default: 20)"),
(
" --agent TYPE",
"Filter: claude | gemini | codex | opencode",
"Filter: claude | gemini | codex | opencode | kilo | kilocode | cline | clinecode",
),
(
" --exclude-self",
Expand Down Expand Up @@ -598,11 +598,11 @@ const HOOKS_HELP: &[HelpEntry] = &[
("hooks status", "Same as above"),
(
"hooks add [tool]",
"Add hooks (claude | gemini | codex | opencode | all)",
"Add hooks (claude | gemini | codex | opencode | kilo | kilocode | cline | clinecode | all)",
),
(
"hooks remove [tool]",
"Remove hooks (claude | gemini | codex | opencode | all)",
"Remove hooks (claude | gemini | codex | opencode | kilocode | cline | clinecode | all)",
),
("", ""),
(
Expand Down Expand Up @@ -723,12 +723,48 @@ const OPENCODE_SPEC: ToolHelpSpec = ToolHelpSpec {
has_fork: true,
};

const KILO_SPEC: ToolHelpSpec = ToolHelpSpec {
name: "kilo",
label: "Kilo",
unique_examples: &[],
extra_env: &[],
has_fork: true,
};

const KILOCODE_SPEC: ToolHelpSpec = ToolHelpSpec {
name: "kilocode",
label: "KiloCode",
unique_examples: &[],
extra_env: &[],
has_fork: true,
};

const CLINE_SPEC: ToolHelpSpec = ToolHelpSpec {
name: "cline",
label: "Cline",
unique_examples: &[],
extra_env: &[],
has_fork: true,
};

const CLINECODE_SPEC: ToolHelpSpec = ToolHelpSpec {
name: "clinecode",
label: "ClineCode",
unique_examples: &[],
extra_env: &[],
has_fork: true,
};

fn get_tool_spec(name: &str) -> Option<&'static ToolHelpSpec> {
match name {
"claude" => Some(&CLAUDE_SPEC),
"gemini" => Some(&GEMINI_SPEC),
"codex" => Some(&CODEX_SPEC),
"opencode" => Some(&OPENCODE_SPEC),
"kilo" => Some(&KILO_SPEC),
"kilocode" => Some(&KILOCODE_SPEC),
"cline" => Some(&CLINE_SPEC),
"clinecode" => Some(&CLINECODE_SPEC),
_ => None,
}
}
Expand Down Expand Up @@ -888,6 +924,10 @@ pub const COMMAND_NAMES: &[&str] = &[
"gemini",
"codex",
"opencode",
"kilo",
"kilocode",
"cline",
"clinecode",
];

/// Get the top-level help text as a String.
Expand All @@ -900,9 +940,9 @@ Usage:\n\
hcom <command> Run command\n\
\n\
Launch:\n\
hcom [N] claude|gemini|codex|opencode [flags] [tool-args]\n\
hcom [N] claude|gemini|codex|opencode|kilo|kilocode [flags] [tool-args]\n\
hcom r <name> Resume stopped agent\n\
hcom f <name> Fork agent session (claude/codex/opencode)\n\
hcom f <name> Fork agent session (claude/codex/opencode/kilo/kilocode/cline/clinecode)\n\
hcom kill <name(s)|tag:T|all> Kill + close terminal pane\n\
\n\
Commands:\n\
Expand Down Expand Up @@ -997,7 +1037,7 @@ pub fn get_command_help(name: &str) -> String {
return resume_fork_help(
"hcom f <target> [tool-args...] Fork an agent session (active or stopped)",
"Creates a new agent that continues from the forked session.\n\
Supported tools: claude, codex, opencode. (gemini does not fork.)\n\
Supported tools: claude, codex, opencode, kilo, kilocode. (gemini does not fork.)\n\
Remote fork (`:<device>`) requires --dir to pin the target cwd.",
"hcom r <target> Resume a stopped agent",
);
Expand Down Expand Up @@ -1118,6 +1158,10 @@ mod tests {
"gemini",
"codex",
"opencode",
"kilo",
"kilocode",
"cline",
"clinecode",
];
for cmd in commands {
let help = get_command_help(cmd);
Expand Down Expand Up @@ -1169,8 +1213,8 @@ mod tests {
#[test]
fn top_level_help_scopes_fork_to_supported_tools() {
let help = get_help_text();
assert!(help.contains(
"hcom f <name> Fork agent session (claude/codex/opencode)"
));
assert!(help.contains(
"hcom f <name> Fork agent session (claude/codex/opencode/kilo/kilocode/cline/clinecode)"
));
}
}
53 changes: 47 additions & 6 deletions src/commands/hooks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ pub struct HooksArgs {
}

/// Valid tool names for hooks management.
const HOOK_TOOLS: &[&str] = &["claude", "gemini", "codex", "opencode"];
const HOOK_TOOLS: &[&str] = &["claude", "gemini", "codex", "opencode", "kilocode", "cline", "clinecode"];

/// Get hook installation status for each tool.
fn get_tool_status() -> Vec<(&'static str, bool, String)> {
Expand All @@ -40,11 +40,21 @@ fn get_tool_status() -> Vec<(&'static str, bool, String)> {
.to_string_lossy()
.to_string();

let kilocode_installed = crate::hooks::kilo::verify_kilocode_plugin_installed();
let kilocode_path = crate::hooks::kilo::get_kilocode_plugin_path()
.to_string_lossy()
.to_string();

let cline_installed = crate::hooks::cline::verify_cline_plugin_installed();

vec![
("claude", claude_installed, claude_path),
("gemini", gemini_installed, gemini_path),
("codex", codex_installed, codex_path),
("opencode", opencode_installed, opencode_path),
("kilocode", kilocode_installed, kilocode_path),
("cline", cline_installed, String::new()),
("clinecode", cline_installed, String::new()),
]
}

Expand Down Expand Up @@ -81,7 +91,7 @@ fn cmd_hooks_add(argv: &[String]) -> i32 {
vec![argv[0].as_str()]
} else {
eprintln!("Error: Unknown tool: {}", argv[0]);
eprintln!("Valid options: claude, gemini, codex, opencode, all");
eprintln!("Valid options: claude, gemini, codex, opencode, kilocode, all");
return 1;
};

Expand All @@ -101,6 +111,8 @@ fn cmd_hooks_add(argv: &[String]) -> i32 {
"gemini" => crate::hooks::gemini::verify_gemini_hooks_installed(include_permissions),
"codex" => crate::hooks::codex::verify_codex_hooks_installed(include_permissions),
"opencode" => crate::hooks::opencode::verify_opencode_plugin_installed(),
"kilocode" => crate::hooks::kilo::verify_kilocode_plugin_installed(),
"cline" | "clinecode" => crate::hooks::cline::verify_cline_plugin_installed(),
_ => false,
};
if already {
Expand All @@ -125,6 +137,16 @@ fn cmd_hooks_add(argv: &[String]) -> i32 {
Ok(false) => AddResult::Failed(None),
Err(e) => AddResult::Failed(Some(e.to_string())),
},
"kilocode" => match crate::hooks::kilo::install_kilocode_plugin() {
Ok(true) => AddResult::Added,
Ok(false) => AddResult::Failed(None),
Err(e) => AddResult::Failed(Some(e.to_string())),
},
"cline" | "clinecode" => match crate::hooks::cline::install_cline_plugin() {
Ok(true) => AddResult::Added,
Ok(false) => AddResult::Failed(None),
Err(e) => AddResult::Failed(Some(e.to_string())),
},
_ => AddResult::Failed(None),
};
results.push((tool, outcome));
Expand Down Expand Up @@ -165,6 +187,9 @@ fn cmd_hooks_add(argv: &[String]) -> i32 {
"gemini" => "Gemini CLI",
"codex" => "Codex",
"opencode" => "OpenCode",
"kilocode" => "KiloCode",
"cline" => "Cline",
"clinecode" => "ClineCode",
other => other,
};
println!("Restart {tool_name} to activate hooks.");
Expand All @@ -185,7 +210,7 @@ pub fn cmd_hooks_remove(argv: &[String]) -> i32 {
vec![argv[0].as_str()]
} else {
eprintln!("Error: Unknown tool: {}", argv[0]);
eprintln!("Valid options: claude, gemini, codex, opencode, all");
eprintln!("Valid options: claude, gemini, codex, opencode, kilocode, cline, clinecode, all");
return 1;
};

Expand All @@ -212,6 +237,22 @@ pub fn cmd_hooks_remove(argv: &[String]) -> i32 {
continue;
}
},
"kilocode" => match crate::hooks::kilo::remove_kilocode_plugin() {
Ok(()) => true,
Err(e) => {
eprintln!("Failed to remove {tool} hooks: {e}");
fail_count += 1;
continue;
}
},
"cline" | "clinecode" => match crate::hooks::cline::remove_cline_plugin() {
Ok(()) => true,
Err(e) => {
eprintln!("Failed to remove {tool} hooks: {e}");
fail_count += 1;
continue;
}
},
_ => false,
};
if ok {
Expand Down Expand Up @@ -251,8 +292,8 @@ pub fn cmd_hooks(_db: &HcomDb, args: &HooksArgs, _ctx: Option<&CommandContext>)
Usage:\n \
hcom hooks Show hook status for all tools\n \
hcom hooks status Same as above\n \
hcom hooks add [tool] Add hooks (claude|gemini|codex|opencode|all)\n \
hcom hooks remove [tool] Remove hooks (claude|gemini|codex|opencode|all)\n\n\
hcom hooks add [tool] Add hooks (claude|gemini|codex|opencode|kilocode|all)\n \
hcom hooks remove [tool] Remove hooks (claude|gemini|codex|opencode|kilocode|all)\n\n\
Examples:\n \
hcom hooks add claude Add Claude Code hooks only\n \
hcom hooks add Auto-detect tool or add all\n \
Expand Down Expand Up @@ -286,7 +327,7 @@ mod tests {
// (unless running inside one, which is fine — it'll detect it)
let tool = detect_current_tool();
assert!(
["claude", "gemini", "codex", "opencode", "adhoc"].contains(&tool),
["claude", "gemini", "codex", "opencode", "kilocode", "adhoc"].contains(&tool),
"unexpected tool: {tool}"
);
}
Expand Down
Loading