diff --git a/README.md b/README.md index f8d65efe52..c96c610e90 100644 --- a/README.md +++ b/README.md @@ -111,6 +111,7 @@ rtk init --agent cline # Cline / Roo Code rtk init --agent kilocode # Kilo Code rtk init --agent antigravity # Google Antigravity rtk init --agent hermes # Hermes +rtk init --agent omp # Oh My Pi (OMP) # 2. Restart your AI tool, then test git status # Automatically rewritten to rtk git status @@ -366,6 +367,7 @@ RTK supports 13 AI coding tools. Each integration rewrites shell commands to `rt | **OpenCode** | `rtk init -g --opencode` | Plugin TS (tool.execute.before) | | **OpenClaw** | `openclaw plugins install ./openclaw` | Plugin TS (before_tool_call) | | **Hermes** | `rtk init --agent hermes` | Python plugin adapter (terminal command mutation via `rtk rewrite`) | +| **Oh My Pi (OMP)** | `rtk init --agent omp` / `rtk init -g --agent omp` | OMP extension (`tool_call`) | | **Mistral Vibe** | Planned ([#800](https://github.com/rtk-ai/rtk/issues/800)) | Blocked on upstream | | **Kilo Code** | `rtk init --agent kilocode` | .kilocode/rules/rtk-rules.md (project-scoped) | | **Google Antigravity** | `rtk init --agent antigravity` | .agents/rules/antigravity-rtk-rules.md (project-scoped) | diff --git a/docs/guide/getting-started/supported-agents.md b/docs/guide/getting-started/supported-agents.md index b7fb920deb..027660aab8 100644 --- a/docs/guide/getting-started/supported-agents.md +++ b/docs/guide/getting-started/supported-agents.md @@ -37,6 +37,7 @@ Agent runs "cargo test" | OpenClaw | TypeScript plugin (`before_tool_call`) | Yes | | Pi | TypeScript extension (`tool_call` event) | Yes | | Hermes | Python plugin (`terminal` command mutation) | Yes | +| Oh My Pi (OMP) | TypeScript extension (`tool_call`) | Yes | | Cline / Roo Code | Rules file (prompt-level) | N/A | | Windsurf | Rules file (prompt-level) | N/A | | Codex CLI | AGENTS.md instructions | N/A | @@ -125,6 +126,13 @@ Creates `~/.hermes/plugins/rtk-rewrite/` and enables it through `plugins.enabled The plugin fails open. If `rtk` is missing at load time, the hook is not registered. If `rtk rewrite` errors, the tool is not `terminal`, the payload has no string `command`, or the plugin raises an exception, Hermes runs the original command unchanged. The same `rtk rewrite` limitations apply: already-prefixed `rtk` commands, compound shell commands, heredocs, and commands without filters are not rewritten. +### Oh My Pi + +```bash +rtk init --agent omp # creates ./.omp/extensions/rtk.ts +rtk init -g --agent omp # creates ~/.omp/agent/extensions/rtk.ts +``` + ### Cline / Roo Code ```bash diff --git a/hooks/README.md b/hooks/README.md index 55b2149ddf..f1101b587d 100644 --- a/hooks/README.md +++ b/hooks/README.md @@ -4,7 +4,7 @@ **Deployed hook artifacts** — the actual files installed on user machines by `rtk init`. These are shell scripts, TypeScript plugins, and rules files that run outside the Rust binary. They are **thin delegates**: parse agent-specific JSON, call `rtk rewrite` as a subprocess, format agent-specific response. Zero filtering logic lives here. -Owns: per-agent hook scripts and configuration files for 9 supported agents (Claude Code, Copilot, Cursor, Cline, Windsurf, Codex, OpenCode, Hermes, Pi). +Owns: per-agent hook scripts and configuration files for supported agents (Claude Code, Copilot, Cursor, Cline, Windsurf, Codex, OpenCode, Hermes, Pi, Oh My Pi). Does **not** own: hook installation/uninstallation (that's `src/hooks/init.rs`), the rewrite pattern registry (that's `discover/registry`), or integrity verification (that's `src/hooks/integrity.rs`). @@ -41,6 +41,7 @@ Each agent subdirectory has its own README with hook-specific details: - **[`codex/`](codex/README.md)** — Awareness document, `AGENTS.md` integration, `$CODEX_HOME` or `~/.codex/` location - **[`opencode/`](opencode/README.md)** — TypeScript plugin, `zx` library, `tool.execute.before` event, in-place mutation - **[`pi/`](pi/README.md)** — TypeScript extension, `tool_call` event, `isToolCallEventType` guard, in-place mutation, `~/.pi/agent/extensions/` +- **[`omp/`](omp/README.md)** — TypeScript extension, OMP `tool_call` rewrite via `./.omp/extensions/rtk.ts` or `~/.omp/agent/extensions/rtk.ts` - **[`hermes/`](hermes/README.md)** — Python plugin, `pre_tool_call` hook, in-place terminal command mutation ## Supported Agents @@ -58,6 +59,7 @@ Each agent subdirectory has its own README with hook-specific details: | OpenCode | TypeScript plugin (`tool.execute.before`) | In-place mutation | Yes | | Pi | TypeScript extension (`tool_call` event) | In-place mutation | Yes | | Hermes | Python plugin (`pre_tool_call`) | In-place mutation | Yes | +| Oh My Pi (OMP) | TypeScript extension (`tool_call`) | In-place mutation | Yes (`event.input.command`) | ## JSON Formats by Agent diff --git a/hooks/omp/README.md b/hooks/omp/README.md new file mode 100644 index 0000000000..95f2e03a3c --- /dev/null +++ b/hooks/omp/README.md @@ -0,0 +1,12 @@ +# Oh My Pi Hooks + +> Part of [`hooks/`](../README.md) — see also [`src/hooks/`](../../src/hooks/README.md) for installation code + +## Specifics + +- TypeScript extension module, not a shell hook or rules file +- Sets its OMP extension display label to `RTK` +- Installs to `./.omp/extensions/rtk.ts` with `rtk init --agent omp`, or to `~/.omp/agent/extensions/rtk.ts` with `rtk init -g --agent omp` +- Intercepts OMP `tool_call` events for the `bash` tool and delegates rewrite decisions to `rtk rewrite` +- Requires Bun runtime (uses `Bun.which` and `Bun.spawn`); OMP currently ships with Bun +- Multi-extension chaining: OMP dispatches `tool_call` handlers sequentially. Downstream handlers observe the RTK-rewritten `event.input.command` only when RTK actually rewrites it diff --git a/hooks/omp/rtk.ts b/hooks/omp/rtk.ts new file mode 100644 index 0000000000..32a8662dcc --- /dev/null +++ b/hooks/omp/rtk.ts @@ -0,0 +1,101 @@ +// RTK - Rust Token Killer +// OMP extension: rewrite bash tool calls through `rtk rewrite`. +// +// This is a thin delegating extension. All rewrite logic lives in RTK's Rust +// registry via `rtk rewrite`, which remains the single source of truth. + +type BashToolCallEvent = { + toolName: string; + input: { command: string }; +}; + +type ExtensionContext = { + ui: { + setStatus(key: string, text: string | undefined): void; + }; +}; + +type ExtensionAPI = { + setLabel(label: string): void; + on( + event: "session_start", + handler: ( + event: unknown, + ctx: ExtensionContext, + ) => Promise | void, + ): void; + on( + event: "tool_call", + handler: ( + event: BashToolCallEvent, + ) => Promise | void, + ): void; +}; + +type RewriteDecision = { kind: "rewrite"; rewritten: string } | { kind: "skip" }; + +function readText(stream: ReadableStream | null | undefined, name: string): Promise { + if (!stream) { + throw new Error(`rtk rewrite ${name} stream was unavailable`); + } + return new Response(stream).text().then((text) => text.trim()); +} + +async function rewriteWithRtk(command: string): Promise { + const proc = Bun.spawn(["rtk", "rewrite", command], { + stdout: "pipe", + stderr: "pipe", + }); + + const [exitCode, stdout] = await Promise.all([ + proc.exited, + readText(proc.stdout, "stdout"), + proc.stderr?.cancel(), + ]); + + switch (exitCode) { + case 0: + case 3: + if (!stdout || stdout === command) { + return { kind: "skip" }; + } + return { kind: "rewrite", rewritten: stdout }; + default: + return { kind: "skip" }; + } +} + +export default function rtkOmpExtension(pi: ExtensionAPI) { + pi.setLabel("RTK"); + + const hasRtk = Boolean(Bun.which("rtk")); + + if (!hasRtk) { + pi.on("session_start", (_event, ctx) => { + ctx.ui.setStatus("rtk", "RTK extension disabled: rtk binary not found in PATH."); + }); + return; + } + + pi.on("tool_call", async (event) => { + if (event.toolName !== "bash") { + return; + } + + const original = event.input.command; + if (original.trim() === "") { + return; + } + + try { + const decision = await rewriteWithRtk(original); + if (decision.kind === "skip") { + return; + } + + event.input.command = decision.rewritten; + } catch { + return; + } + }); +} diff --git a/src/hooks/README.md b/src/hooks/README.md index a0c76b76de..40d275e991 100644 --- a/src/hooks/README.md +++ b/src/hooks/README.md @@ -6,7 +6,7 @@ The **lifecycle management** layer for LLM agent hooks: install, uninstall, verify integrity, audit usage, and manage trust. This component creates and maintains the hook artifacts that live in `hooks/` (root), but does **not** execute rewrite logic itself — that lives in `discover/registry`. -Owns: `rtk init` installation flows (5 agents via `AgentTarget` enum + 3 special modes: Gemini, Codex, OpenCode), SHA-256 integrity verification, hook version checking, audit log analysis, `rtk rewrite` CLI entry point, and TOML filter trust management. +Owns: `rtk init` installation flows for AgentTarget-based agents and special modes (Gemini, Codex, OpenCode, Oh My Pi), SHA-256 integrity verification, hook version checking, audit log analysis, `rtk rewrite` CLI entry point, and TOML filter trust management. Does **not** own: the deployed hook scripts themselves (that's `hooks/`), the rewrite pattern registry (that's `discover/`), or command filtering (that's `cmds/`). @@ -32,6 +32,7 @@ LLM agent integration layer that installs, validates, and executes command-rewri | Cursor | `rtk init -g --agent cursor` | Cursor hook | hooks.json | | Pi | `rtk init --agent pi` | `.pi/extensions/rtk.ts` | -- | | Hermes | `rtk init --agent hermes` | Python plugin in `~/.hermes/plugins/rtk-rewrite/` | `config.yaml` `plugins.enabled` | +| Oh My Pi | `rtk init --agent omp` / `rtk init -g --agent omp` | OMP extension | -- | ## Integrity Verification diff --git a/src/hooks/constants.rs b/src/hooks/constants.rs index 506e88cdf8..2ab73e992d 100644 --- a/src/hooks/constants.rs +++ b/src/hooks/constants.rs @@ -33,3 +33,5 @@ pub const HERMES_PLUGINS_SUBDIR: &str = "plugins"; pub const HERMES_PLUGIN_NAME: &str = "rtk-rewrite"; pub const HERMES_PLUGIN_INIT_FILE: &str = "__init__.py"; pub const HERMES_PLUGIN_MANIFEST_FILE: &str = "plugin.yaml"; +pub const OMP_GLOBAL_EXTENSION_PATH: &str = ".omp/agent/extensions/rtk.ts"; +pub const OMP_PROJECT_EXTENSION_PATH: &str = ".omp/extensions/rtk.ts"; diff --git a/src/hooks/init.rs b/src/hooks/init.rs index 189f5de557..8b2f1796ef 100644 --- a/src/hooks/init.rs +++ b/src/hooks/init.rs @@ -15,13 +15,15 @@ use super::constants::{ BEFORE_TOOL_KEY, CLAUDE_DIR, CLAUDE_HOOK_COMMAND, CODEX_DIR, CURSOR_HOOK_COMMAND, GEMINI_HOOK_FILE, HERMES_DIR, HERMES_PLUGINS_SUBDIR, HERMES_PLUGIN_INIT_FILE, HERMES_PLUGIN_MANIFEST_FILE, HERMES_PLUGIN_NAME, HOOKS_JSON, HOOKS_SUBDIR, - PI_CODING_AGENT_DIR_ENV, PI_DIR, PI_EXTENSIONS_SUBDIR, PI_LOCAL_DIR, PI_PLUGIN_FILE, - PRE_TOOL_USE_KEY, REWRITE_HOOK_FILE, SETTINGS_JSON, + OMP_GLOBAL_EXTENSION_PATH, OMP_PROJECT_EXTENSION_PATH, PI_CODING_AGENT_DIR_ENV, PI_DIR, + PI_EXTENSIONS_SUBDIR, PI_LOCAL_DIR, PI_PLUGIN_FILE, PRE_TOOL_USE_KEY, REWRITE_HOOK_FILE, + SETTINGS_JSON, }; use super::integrity; // Embedded OpenCode plugin (auto-rewrite) const OPENCODE_PLUGIN: &str = include_str!("../../hooks/opencode/rtk.ts"); +const OMP_EXTENSION: &str = include_str!("../../hooks/omp/rtk.ts"); // Embedded Pi extension (auto-rewrite) const PI_PLUGIN: &str = include_str!("../../hooks/pi/rtk.ts"); @@ -604,56 +606,93 @@ fn remove_hook_from_settings(ctx: InitContext) -> Result { Ok(removed) } -/// Full uninstall for Claude, Gemini, Codex, Cursor, or Pi artifacts. +/// Full uninstall for Claude, Gemini, Codex, Cursor, Pi, or OMP artifacts. pub fn uninstall( global: bool, gemini: bool, codex: bool, cursor: bool, pi: bool, + omp: bool, ctx: InitContext, ) -> Result<()> { let InitContext { verbose, dry_run } = ctx; - if codex { - uninstall_codex(global, ctx)?; - if dry_run { - print_dry_run_footer(); - } - return Ok(()); - } - if cursor { - if !global { + let selected_target = gemini || codex || cursor || pi || omp; + if selected_target { + if codex && !global { + anyhow::bail!( + "Uninstall only works with --global flag. For local projects, manually remove RTK from AGENTS.md" + ); + } + if cursor && !global { anyhow::bail!("Cursor uninstall only works with --global flag"); } - let cursor_removed = remove_cursor_hooks(ctx).context("Failed to remove Cursor hooks")?; - if !cursor_removed.is_empty() { - let header = if dry_run { - "[dry-run] would uninstall RTK (Cursor):" + if gemini && !global { + anyhow::bail!( + "Uninstall only works with --global flag. For local projects, manually remove RTK from GEMINI.md" + ); + } + + if gemini { + let gemini_removed = uninstall_gemini(ctx)?; + if !gemini_removed.is_empty() { + let header = if dry_run { + "[dry-run] would uninstall RTK (Gemini):" + } else { + "RTK uninstalled (Gemini):" + }; + println!("{}", header); + for item in &gemini_removed { + println!(" - {}", item); + } + if !dry_run { + println!("\nRestart Gemini CLI to apply changes."); + } } else { - "RTK uninstalled (Cursor):" - }; - println!("{}", header); - for item in &cursor_removed { - println!(" - {}", item); + println!("RTK Gemini support was not installed (nothing to remove)"); } - if !dry_run { - println!("\nRestart Cursor to apply changes."); + } + + if codex { + uninstall_codex(global, ctx)?; + } + + if cursor { + let cursor_removed = + remove_cursor_hooks(ctx).context("Failed to remove Cursor hooks")?; + if !cursor_removed.is_empty() { + let header = if dry_run { + "[dry-run] would uninstall RTK (Cursor):" + } else { + "RTK uninstalled (Cursor):" + }; + println!("{}", header); + for item in &cursor_removed { + println!(" - {}", item); + } + if !dry_run { + println!("\nRestart Cursor to apply changes."); + } + } else { + println!("RTK Cursor support was not installed (nothing to remove)"); } - } else { - println!("RTK Cursor support was not installed (nothing to remove)"); } + + if pi { + uninstall_pi(global, ctx)?; + } + + if omp { + uninstall_omp(global, ctx)?; + } + if dry_run { print_dry_run_footer(); } return Ok(()); } - if pi { - uninstall_pi(global, ctx)?; - return Ok(()); - } - if !global { anyhow::bail!("Uninstall only works with --global flag. For local projects, manually remove RTK from CLAUDE.md"); } @@ -661,32 +700,6 @@ pub fn uninstall( let claude_dir = resolve_claude_dir()?; let mut removed = Vec::new(); - // Also uninstall Gemini artifacts if --gemini or always (clean everything) - if gemini { - let gemini_removed = uninstall_gemini(ctx)?; - removed.extend(gemini_removed); - if !removed.is_empty() { - let header = if dry_run { - "[dry-run] would uninstall RTK (Gemini):" - } else { - "RTK uninstalled (Gemini):" - }; - println!("{}", header); - for item in &removed { - println!(" - {}", item); - } - if !dry_run { - println!("\nRestart Gemini CLI to apply changes."); - } - } else { - println!("RTK Gemini support was not installed (nothing to remove)"); - } - if dry_run { - print_dry_run_footer(); - } - return Ok(()); - } - // 1. Remove legacy hook file (if exists from old installation) let hook_path = claude_dir.join(HOOKS_SUBDIR).join(REWRITE_HOOK_FILE); if hook_path.exists() { @@ -1745,6 +1758,158 @@ fn run_antigravity_mode_at(base_dir: &Path, ctx: InitContext) -> Result<()> { Ok(()) } +// ─── Oh My Pi (OMP) support ──────────────────────────────────── + +const OMP_EXTENSION_MARKER: &str = "// RTK - Rust Token Killer"; + +fn omp_extension_contains_rtk(existing: &str) -> bool { + existing.contains(OMP_EXTENSION_MARKER) +} + +fn omp_extension_matches_stock(existing: &str) -> bool { + existing.trim() == OMP_EXTENSION.trim() +} + +fn resolve_omp_extension_path(global: bool) -> Result { + let base_dir = if global { + dirs::home_dir().context("Cannot determine home directory. Is $HOME set?")? + } else { + std::env::current_dir()? + }; + + Ok(if global { + base_dir.join(OMP_GLOBAL_EXTENSION_PATH) + } else { + base_dir.join(OMP_PROJECT_EXTENSION_PATH) + }) +} + +fn install_omp_extension_file(extension_path: &Path, ctx: InitContext) -> Result { + let InitContext { dry_run, .. } = ctx; + if !dry_run { + if let Some(parent) = extension_path + .parent() + .filter(|parent| !parent.as_os_str().is_empty()) + { + fs::create_dir_all(parent) + .with_context(|| format!("Failed to create directory: {}", parent.display()))?; + } + } + + write_if_changed(extension_path, OMP_EXTENSION, "OMP extension", ctx) +} + +pub fn run_omp_mode(global: bool, ctx: InitContext) -> Result<()> { + let InitContext { dry_run, .. } = ctx; + let extension_path = resolve_omp_extension_path(global)?; + let installed = install_omp_extension_file(&extension_path, ctx)?; + + if dry_run { + print_dry_run_footer(); + } else if installed { + if global { + println!("\nRTK configured for Oh My Pi (global).\n"); + println!(" Extension: {} (installed)", extension_path.display()); + } else { + println!("\nRTK configured for Oh My Pi in this project.\n"); + println!(" Extension: {} (installed)", extension_path.display()); + } + println!(" Restart OMP. Test with: git status\n"); + } else { + println!("\nRTK already configured for Oh My Pi.\n"); + println!( + " Extension: {} (already present)", + extension_path.display() + ); + } + + Ok(()) +} + +pub fn uninstall_omp(global: bool, ctx: InitContext) -> Result<()> { + let InitContext { verbose, dry_run } = ctx; + let extension_path = resolve_omp_extension_path(global)?; + + if !extension_path.exists() { + println!("RTK was not installed for Oh My Pi (nothing to remove)"); + return Ok(()); + } + + let content = fs::read_to_string(&extension_path) + .with_context(|| format!("Failed to read OMP extension: {}", extension_path.display()))?; + + if omp_extension_matches_stock(&content) { + if dry_run { + println!( + "[dry-run] would remove OMP extension: {}", + extension_path.display() + ); + } else { + fs::remove_file(&extension_path).with_context(|| { + format!( + "Failed to remove OMP extension: {}", + extension_path.display() + ) + })?; + if verbose > 0 { + eprintln!("Removed OMP extension: {}", extension_path.display()); + } + println!("RTK uninstalled for Oh My Pi:"); + println!(" - Extension: {}", extension_path.display()); + } + } else if omp_extension_contains_rtk(&content) { + anyhow::bail!( + "OMP extension at {} contains RTK content that does not match the stock extension. Remove the file manually.", + extension_path.display() + ); + } else { + println!("RTK was not installed for Oh My Pi (nothing to remove)"); + } + + Ok(()) +} + +fn print_omp_extension_status(label: &str, extension_path: &Path) -> Result<()> { + if extension_path.exists() { + let content = fs::read_to_string(extension_path)?; + if omp_extension_matches_stock(&content) { + println!("[ok] {}: {}", label, extension_path.display()); + } else if omp_extension_contains_rtk(&content) { + println!( + "[warn] {}: {} contains RTK content but differs from the stock OMP extension", + label, + extension_path.display() + ); + } else { + println!( + "[--] {}: {} exists but rtk is not configured", + label, + extension_path.display() + ); + } + } else { + println!("[--] {}: {} (not found)", label, extension_path.display()); + } + Ok(()) +} + +fn show_omp_config() -> Result<()> { + let global_extension = resolve_omp_extension_path(true)?; + let project_extension = resolve_omp_extension_path(false)?; + + println!("rtk Configuration (Oh My Pi):\n"); + print_omp_extension_status("Global extension", &global_extension)?; + print_omp_extension_status("Project extension", &project_extension)?; + + println!("\nUsage:"); + println!(" rtk init --agent omp # Configure ./.omp/extensions/rtk.ts"); + println!(" rtk init -g --agent omp # Configure ~/.omp/agent/extensions/rtk.ts"); + println!(" rtk init --agent omp --uninstall # Remove project OMP RTK extension"); + println!(" rtk init -g --agent omp --uninstall # Remove global OMP RTK extension"); + + Ok(()) +} + // ─── Hermes support ──────────────────────────────────────────── const HERMES_PLUGIN_INIT: &str = include_str!("../../hooks/hermes/rtk-rewrite/__init__.py"); @@ -2818,8 +2983,9 @@ fn uninstall_pi(global: bool, ctx: InitContext) -> Result<()> { let InitContext { verbose, dry_run } = ctx; let plugin_path = pi_plugin_path_for_scope(global)?; let mut removed: Vec = Vec::new(); + let plugin_exists = plugin_path.exists(); - if plugin_path.exists() { + if plugin_exists { if dry_run { println!( "[dry-run] would remove Pi extension: {}", @@ -2838,7 +3004,9 @@ fn uninstall_pi(global: bool, ctx: InitContext) -> Result<()> { } if dry_run { - print_dry_run_footer(); + if !plugin_exists { + println!("RTK Pi extension was not installed (nothing to remove)"); + } } else if !removed.is_empty() { println!("RTK uninstalled (Pi):"); for item in &removed { @@ -3265,10 +3433,13 @@ fn remove_cursor_hook_from_json(root: &mut serde_json::Value) -> bool { } /// Show current rtk configuration -pub fn show_config(codex: bool) -> Result<()> { +pub fn show_config(codex: bool, omp: bool) -> Result<()> { if codex { return show_codex_config(); } + if omp { + return show_omp_config(); + } show_claude_config() } @@ -5603,7 +5774,16 @@ mod tests { let tmp = TempDir::new().unwrap(); with_claude_dir_override(&tmp, |claude_dir| { run_default_mode(true, PatchMode::Auto, false, InitContext::default()).unwrap(); - uninstall(true, false, false, false, false, InitContext::default()).unwrap(); + uninstall( + true, + false, + false, + false, + false, + false, + InitContext::default(), + ) + .unwrap(); assert!(!claude_dir.join(RTK_MD).exists(), "RTK.md must be removed"); let settings_content = @@ -5731,7 +5911,7 @@ mod tests { dry_run: true, ..Default::default() }; - uninstall(true, false, false, false, false, dry).unwrap(); + uninstall(true, false, false, false, false, false, dry).unwrap(); // Files must still exist with identical content assert!( @@ -5957,7 +6137,16 @@ mod tests { let plugin = pi_dir.join(PI_EXTENSIONS_SUBDIR).join(PI_PLUGIN_FILE); assert!(plugin.exists()); - uninstall(true, false, false, false, true, InitContext::default()).unwrap(); + uninstall( + true, + false, + false, + false, + true, + false, + InitContext::default(), + ) + .unwrap(); assert!(!plugin.exists(), "plugin must be removed"); }); @@ -5971,7 +6160,15 @@ mod tests { std::env::set_current_dir(tmp.path()).unwrap(); run_pi_mode(false, InitContext::default()).unwrap(); - let result = uninstall(false, false, false, false, true, InitContext::default()); + let result = uninstall( + false, + false, + false, + false, + true, + false, + InitContext::default(), + ); std::env::set_current_dir(&cwd).unwrap(); result.unwrap(); @@ -6070,6 +6267,7 @@ mod tests { false, false, true, + false, InitContext { verbose: 0, dry_run: true, @@ -6108,6 +6306,7 @@ mod tests { false, false, true, + false, InitContext { verbose: 0, dry_run: true, diff --git a/src/main.rs b/src/main.rs index 22e6cbca87..57ead74726 100644 --- a/src/main.rs +++ b/src/main.rs @@ -47,6 +47,8 @@ pub enum AgentTarget { Antigravity, /// Pi coding agent Pi, + /// Oh My Pi (OMP) + Omp, /// Hermes CLI Hermes, } @@ -1377,14 +1379,16 @@ fn uninstall_init_dispatch( ) -> Result<()> where UninstallHermes: FnOnce(hooks::init::InitContext) -> Result<()>, - UninstallStandard: FnOnce(bool, bool, bool, bool, bool, hooks::init::InitContext) -> Result<()>, + UninstallStandard: + FnOnce(bool, bool, bool, bool, bool, bool, hooks::init::InitContext) -> Result<()>, { if agent == Some(AgentTarget::Hermes) { uninstall_hermes(ctx) } else { let cursor = agent == Some(AgentTarget::Cursor); let pi = agent == Some(AgentTarget::Pi); - uninstall_standard(global, gemini, codex, cursor, pi, ctx) + let omp = agent == Some(AgentTarget::Omp); + uninstall_standard(global, gemini, codex, cursor, pi, omp, ctx) } } @@ -1820,7 +1824,7 @@ fn run_cli() -> Result { dry_run, }; if show { - hooks::init::show_config(codex)?; + hooks::init::show_config(codex, agent == Some(AgentTarget::Omp))?; } else if uninstall { uninstall_init_dispatch( agent, @@ -1858,6 +1862,8 @@ fn run_cli() -> Result { hooks::init::run_antigravity_mode(ctx)?; } else if agent == Some(AgentTarget::Hermes) { hooks::init::run_hermes_mode(ctx)?; + } else if agent == Some(AgentTarget::Omp) { + hooks::init::run_omp_mode(global, ctx)?; } else { let install_opencode = opencode; let install_claude = !opencode; @@ -2688,6 +2694,34 @@ mod tests { } } + #[test] + fn test_try_parse_init_agent_omp() { + let cli = Cli::try_parse_from(["rtk", "init", "--agent", "omp"]).unwrap(); + match cli.command { + Commands::Init { agent, .. } => assert_eq!(agent, Some(AgentTarget::Omp)), + _ => panic!("Expected Init command"), + } + } + + #[test] + fn test_try_parse_init_agent_omp_global_uninstall() { + let cli = + Cli::try_parse_from(["rtk", "init", "-g", "--agent", "omp", "--uninstall"]).unwrap(); + match cli.command { + Commands::Init { + global, + agent, + uninstall, + .. + } => { + assert!(global); + assert_eq!(agent, Some(AgentTarget::Omp)); + assert!(uninstall); + } + _ => panic!("Expected Init command"), + } + } + #[test] fn test_init_uninstall_dispatch_routes_hermes_to_hermes_cleanup() { let hermes_called = Cell::new(false); @@ -2709,7 +2743,7 @@ mod tests { assert!(ctx.dry_run); Ok(()) }, - |_, _, _, _, _, _| { + |_, _, _, _, _, _, _| { standard_called.set(true); Ok(()) }, @@ -2720,6 +2754,48 @@ mod tests { assert!(!standard_called.get()); } + #[test] + fn test_init_uninstall_dispatch_passes_omp_to_standard_cleanup() { + let standard_called = Cell::new(false); + let ctx = hooks::init::InitContext { + verbose: 1, + dry_run: true, + }; + + let result = uninstall_init_dispatch( + Some(AgentTarget::Omp), + true, + false, + true, + ctx, + |_| panic!("Hermes cleanup should not run"), + |global, gemini, codex, cursor, pi, omp, ctx| { + standard_called.set(true); + assert!(global); + assert!(!gemini); + assert!(codex); + assert!(!cursor); + assert!(!pi); + assert!(omp); + assert_eq!(ctx.verbose, 1); + assert!(ctx.dry_run); + Ok(()) + }, + ); + + assert!(result.is_ok()); + assert!(standard_called.get()); + } + + #[test] + fn test_init_omp_flag_rejected() { + let result = Cli::try_parse_from(["rtk", "init", "--omp"]); + assert!( + result.is_err(), + "--omp must be rejected as unknown argument" + ); + } + #[test] fn test_try_parse_help_is_display_help() { match Cli::try_parse_from(["rtk", "--help"]) {