diff --git a/hooks/pi-rtk-rewrite.sh b/hooks/pi-rtk-rewrite.sh new file mode 100755 index 000000000..8c6993dd9 --- /dev/null +++ b/hooks/pi-rtk-rewrite.sh @@ -0,0 +1,58 @@ +#!/usr/bin/env bash +# rtk-hook-version: 1 +# RTK Pi Agent hook — rewrites shell commands to use rtk for token savings. +# Works with Pi (oh-my-pi / omp) CLI. +# Pi preToolUse hook format: receives JSON on stdin, returns JSON on stdout. +# Requires: rtk >= 0.23.0, jq +# +# This is a thin delegating hook: all rewrite logic lives in `rtk rewrite`, +# which is the single source of truth (src/discover/registry.rs). +# To add or change rewrite rules, edit the Rust registry — not this file. + +if ! command -v jq &>/dev/null; then + echo "[rtk] WARNING: jq is not installed. Hook cannot rewrite commands. Install jq: https://jqlang.github.io/jq/download/" >&2 + exit 0 +fi + +if ! command -v rtk &>/dev/null; then + echo "[rtk] WARNING: rtk is not installed or not in PATH. Hook cannot rewrite commands. Install: https://github.com/rtk-ai/rtk#installation" >&2 + exit 0 +fi + +# Version guard: rtk rewrite was added in 0.23.0. +RTK_VERSION=$(rtk --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1) +if [ -n "$RTK_VERSION" ]; then + MAJOR=$(echo "$RTK_VERSION" | cut -d. -f1) + MINOR=$(echo "$RTK_VERSION" | cut -d. -f2) + if [ "$MAJOR" -eq 0 ] && [ "$MINOR" -lt 23 ]; then + echo "[rtk] WARNING: rtk $RTK_VERSION is too old (need >= 0.23.0). Upgrade: cargo install rtk" >&2 + exit 0 + fi +fi + +INPUT=$(cat) + +# Pi sends tool_call event with command in different format +# Try multiple possible JSON paths for compatibility +CMD=$(echo "$INPUT" | jq -r '.tool_input.command // .input.command // .command // empty') + +if [ -z "$CMD" ]; then + echo '{}' + exit 0 +fi + +# Delegate all rewrite logic to the Rust binary. +# rtk rewrite exits 1 when there's no rewrite — hook passes through silently. +REWRITTEN=$(rtk rewrite "$CMD" 2>/dev/null) || { echo '{}'; exit 0; } + +# No change — nothing to do. +if [ "$CMD" = "$REWRITTEN" ]; then + echo '{}' + exit 0 +fi + +# Pi hook response format - try to match expected format +jq -n --arg cmd "$REWRITTEN" '{ + "permission": "allow", + "updated_input": { "command": $cmd } +}' diff --git a/src/init.rs b/src/init.rs index 241a7ef55..368a0ca07 100644 --- a/src/init.rs +++ b/src/init.rs @@ -12,6 +12,9 @@ const REWRITE_HOOK: &str = include_str!("../hooks/rtk-rewrite.sh"); // Embedded Cursor hook script (preToolUse format) const CURSOR_REWRITE_HOOK: &str = include_str!("../hooks/cursor-rtk-rewrite.sh"); +// Embedded Pi hook script (preToolUse format) +const PI_REWRITE_HOOK: &str = include_str!("../hooks/pi-rtk-rewrite.sh"); + // Embedded OpenCode plugin (auto-rewrite) const OPENCODE_PLUGIN: &str = include_str!("../hooks/opencode-rtk.ts"); @@ -212,6 +215,7 @@ pub fn run( install_cursor: bool, install_windsurf: bool, install_cline: bool, + install_pi: bool, claude_md: bool, hook_only: bool, codex: bool, @@ -279,6 +283,11 @@ pub fn run( install_cursor_hooks(verbose)?; } + // Pi hooks (additive, installed alongside Claude Code) + if install_pi { + install_pi_hooks(verbose)?; + } + Ok(()) } @@ -515,8 +524,15 @@ fn remove_hook_from_settings(verbose: u8) -> Result { Ok(removed) } -/// Full uninstall for Claude, Gemini, Codex, or Cursor artifacts. -pub fn uninstall(global: bool, gemini: bool, codex: bool, cursor: bool, verbose: u8) -> Result<()> { +/// Full uninstall for Claude, Gemini, Codex, Cursor, or Pi artifacts. +pub fn uninstall( + global: bool, + gemini: bool, + codex: bool, + cursor: bool, + pi: bool, + verbose: u8, +) -> Result<()> { if codex { return uninstall_codex(global, verbose); } @@ -539,6 +555,23 @@ pub fn uninstall(global: bool, gemini: bool, codex: bool, cursor: bool, verbose: return Ok(()); } + if pi { + if !global { + anyhow::bail!("Pi uninstall only works with --global flag"); + } + let pi_removed = remove_pi_hooks(verbose).context("Failed to remove Pi hooks")?; + if !pi_removed.is_empty() { + println!("RTK uninstalled (Pi):"); + for item in &pi_removed { + println!(" - {}", item); + } + println!("\nRestart Pi to apply changes."); + } else { + println!("RTK Pi support was not installed (nothing to remove)"); + } + return Ok(()); + } + if !global { anyhow::bail!("Uninstall only works with --global flag. For local projects, manually remove RTK from CLAUDE.md"); } @@ -1789,6 +1822,83 @@ fn remove_cursor_hook_from_json(root: &mut serde_json::Value) -> bool { pre_tool_use.len() < original_len } +// ─── Pi Agent support ───────────────────────────────────────────── + +/// Resolve ~/.omp directory +fn resolve_pi_dir() -> Result { + dirs::home_dir() + .map(|h| h.join(".omp")) + .context("Cannot determine home directory. Is $HOME set?") +} + +/// Install Pi hooks: hook script +fn install_pi_hooks(verbose: u8) -> Result<()> { + let pi_dir = resolve_pi_dir()?; + let hooks_dir = pi_dir.join("hooks"); + fs::create_dir_all(&hooks_dir).with_context(|| { + format!( + "Failed to create Pi hooks directory: {}", + hooks_dir.display() + ) + })?; + + // 1. Write hook script + let hook_path = hooks_dir.join("rtk-rewrite.sh"); + let hook_changed = write_if_changed(&hook_path, PI_REWRITE_HOOK, "Pi hook", verbose)?; + + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + fs::set_permissions(&hook_path, fs::Permissions::from_mode(0o755)).with_context(|| { + format!("Failed to set Pi hook permissions: {}", hook_path.display()) + })?; + } + + // Report + let hook_status = if hook_changed { + "installed/updated" + } else { + "already up to date" + }; + println!("\nPi hook {} (global).\n", hook_status); + println!(" Hook: {}", hook_path.display()); + println!(" Pi will automatically detect the hook. Test with: git status\n"); + + Ok(()) +} + +/// Remove Pi hooks +fn remove_pi_hooks(verbose: u8) -> Result> { + let pi_dir = resolve_pi_dir()?; + let hooks_dir = pi_dir.join("hooks"); + let hook_path = hooks_dir.join("rtk-rewrite.sh"); + + let mut removed = Vec::new(); + + if hook_path.exists() { + fs::remove_file(&hook_path) + .with_context(|| format!("Failed to remove Pi hook: {}", hook_path.display()))?; + removed.push(hook_path.display().to_string()); + if verbose > 0 { + eprintln!("Removed Pi hook: {}", hook_path.display()); + } + } + + // Remove hooks directory if empty + if hooks_dir.exists() { + if let Ok(mut entries) = fs::read_dir(&hooks_dir) { + if entries.next().is_none() { + fs::remove_dir(&hooks_dir).ok(); + if verbose > 0 { + eprintln!("Removed empty Pi hooks directory: {}", hooks_dir.display()); + } + } + } + } + + Ok(removed) +} + /// Show current rtk configuration pub fn show_config(codex: bool) -> Result<()> { if codex { @@ -2531,6 +2641,7 @@ More notes false, false, false, + false, true, PatchMode::Auto, 0, @@ -2553,6 +2664,7 @@ More notes false, false, false, + false, true, PatchMode::Skip, 0, diff --git a/src/main.rs b/src/main.rs index 2bbc4bb2d..ded38e856 100644 --- a/src/main.rs +++ b/src/main.rs @@ -82,6 +82,8 @@ pub enum AgentTarget { Windsurf, /// Cline / Roo Code (VS Code) Cline, + /// Pi Agent (oh-my-pi / omp) + Pi, } #[derive(Parser)] @@ -1662,7 +1664,8 @@ fn main() -> Result<()> { init::show_config(codex)?; } else if uninstall { let cursor = agent == Some(AgentTarget::Cursor); - init::uninstall(global, gemini, codex, cursor, cli.verbose)?; + let pi = agent == Some(AgentTarget::Pi); + init::uninstall(global, gemini, codex, cursor, pi, cli.verbose)?; } else if gemini { let patch_mode = if auto_patch { init::PatchMode::Auto @@ -1678,6 +1681,7 @@ fn main() -> Result<()> { let install_cursor = agent == Some(AgentTarget::Cursor); let install_windsurf = agent == Some(AgentTarget::Windsurf); let install_cline = agent == Some(AgentTarget::Cline); + let install_pi = agent == Some(AgentTarget::Pi); let patch_mode = if auto_patch { init::PatchMode::Auto @@ -1693,6 +1697,7 @@ fn main() -> Result<()> { install_cursor, install_windsurf, install_cline, + install_pi, claude_md, hook_only, codex,