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
58 changes: 58 additions & 0 deletions hooks/pi-rtk-rewrite.sh
Original file line number Diff line number Diff line change
@@ -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 }
}'
116 changes: 114 additions & 2 deletions src/init.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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");

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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(())
}

Expand Down Expand Up @@ -515,8 +524,15 @@ fn remove_hook_from_settings(verbose: u8) -> Result<bool> {
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);
}
Expand All @@ -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");
}
Expand Down Expand Up @@ -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<PathBuf> {
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<Vec<String>> {
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 {
Expand Down Expand Up @@ -2531,6 +2641,7 @@ More notes
false,
false,
false,
false,
true,
PatchMode::Auto,
0,
Expand All @@ -2553,6 +2664,7 @@ More notes
false,
false,
false,
false,
true,
PatchMode::Skip,
0,
Expand Down
7 changes: 6 additions & 1 deletion src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,8 @@ pub enum AgentTarget {
Windsurf,
/// Cline / Roo Code (VS Code)
Cline,
/// Pi Agent (oh-my-pi / omp)
Pi,
}

#[derive(Parser)]
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -1693,6 +1697,7 @@ fn main() -> Result<()> {
install_cursor,
install_windsurf,
install_cline,
install_pi,
claude_md,
hook_only,
codex,
Expand Down