diff --git a/README.md b/README.md index 1bb15b77..7147618d 100644 --- a/README.md +++ b/README.md @@ -98,23 +98,33 @@ rtk gain # Should show token savings stats ## Quick Start +### Claude Code + ```bash -# 1. Install hook for Claude Code (recommended) +# Install hook for Claude Code rtk init --global # Follow instructions to register in ~/.claude/settings.json +# Restart Claude Code, then test +git status # Automatically rewritten to rtk git status +``` -# 2. Restart Claude Code, then test +### OpenCode + +```bash +# Install plugin for OpenCode (opencode.ai) +rtk init --opencode +# Restart OpenCode, then test git status # Automatically rewritten to rtk git status ``` -The hook transparently rewrites commands (e.g., `git status` -> `rtk git status`) before execution. Claude never sees the rewrite, it just gets compressed output. +The hook/plugin transparently rewrites commands (e.g., `git status` -> `rtk git status`) before execution. The LLM never sees the rewrite, it just gets compressed output. ## How It Works ``` Without rtk: With rtk: - Claude --git status--> shell --> git Claude --git status--> RTK --> git + Agent --git status--> shell --> git Agent --git status--> RTK --> git ^ | ^ | | | ~2,000 tokens (raw) | | ~200 tokens | filter | +-----------------------------------+ +------- (filtered) ---+----------+ @@ -268,7 +278,7 @@ The most effective way to use rtk. The hook transparently intercepts Bash comman **Result**: 100% rtk adoption across all conversations and subagents, zero token overhead. -### Setup +### Claude Code Setup ```bash rtk init -g # Install hook + RTK.md (recommended) @@ -279,6 +289,14 @@ rtk init --show # Verify installation After install, **restart Claude Code**. +### OpenCode Setup + +```bash +rtk init --opencode # Install plugin to ~/.config/opencode/plugins/ +``` + +After install, **restart OpenCode**. The plugin uses OpenCode's `tool.execute.before` hook to rewrite commands transparently. + ### Commands Rewritten | Raw Command | Rewritten To | diff --git a/hooks/opencode-rtk.js b/hooks/opencode-rtk.js new file mode 100644 index 00000000..98c15833 --- /dev/null +++ b/hooks/opencode-rtk.js @@ -0,0 +1,89 @@ +/** + * opencode-rtk — OpenCode plugin for rtk (Rust Token Killer) + * + * Transparently rewrites bash commands to use rtk equivalents, + * reducing LLM token consumption by 60-90% on common dev commands. + * + * Requires: rtk >= 0.23.0 (https://github.com/rtk-ai/rtk) + * + * Equivalent to rtk's Claude Code PreToolUse hook, adapted for + * OpenCode's plugin API (tool.execute.before hook). + */ + +import { execFileSync, execSync } from "child_process"; + +/** + * Check if rtk binary is available and meets minimum version. + * Returns true if available, null if unavailable. + */ +const checkRtk = () => { + try { + const version = execSync("rtk --version 2>/dev/null", { + encoding: "utf8", + timeout: 5000, + }).trim(); + + const match = version.match(/(\d+)\.(\d+)\.(\d+)/); + if (!match) return null; + + const [, major, minor] = match.map(Number); + if (major === 0 && minor < 23) { + console.warn( + `[opencode-rtk] rtk ${match[0]} too old (need >= 0.23.0). Upgrade: brew upgrade rtk`, + ); + return null; + } + + return true; + } catch { + return null; + } +}; + +/** + * Call `rtk rewrite` on a command string. + * Returns the rewritten command, or null if no rewrite available. + */ +const rewriteCommand = (cmd) => { + try { + const rewritten = execFileSync("rtk", ["rewrite", cmd], { + encoding: "utf8", + timeout: 5000, + stdio: ["pipe", "pipe", "pipe"], + }).trim(); + + if (rewritten && rewritten !== cmd) { + return rewritten; + } + return null; + } catch { + return null; + } +}; + +export const RtkPlugin = async (_ctx) => { + const available = checkRtk(); + if (!available) { + console.warn("[opencode-rtk] rtk binary not found or too old — plugin disabled"); + return {}; + } + + const debug = process.env.RTK_DEBUG === "1"; + + return { + "tool.execute.before": async (input, output) => { + if (input.tool !== "bash") return; + + const cmd = output.args?.command; + if (!cmd || typeof cmd !== "string") return; + + const rewritten = rewriteCommand(cmd); + if (rewritten) { + output.args.command = rewritten; + if (debug) { + console.error(`[opencode-rtk] ${cmd} → ${rewritten}`); + } + } + }, + }; +}; diff --git a/src/init.rs b/src/init.rs index 63e0f0c1..f3a5ecb9 100644 --- a/src/init.rs +++ b/src/init.rs @@ -43,6 +43,9 @@ schema_version = 1 # max_lines = 40 "#; +// Embedded OpenCode plugin for tool.execute.before hook +const OPENCODE_PLUGIN: &str = include_str!("../hooks/opencode-rtk.js"); + /// Control flow for settings.json patching #[derive(Debug, Clone, Copy, PartialEq)] pub enum PatchMode { @@ -858,6 +861,50 @@ fn run_hook_only_mode(global: bool, patch_mode: PatchMode, verbose: u8) -> Resul Ok(()) } +/// OpenCode mode: install plugin to ~/.config/opencode/plugins/ +pub fn run_opencode_mode(verbose: u8) -> Result<()> { + let opencode_dir = resolve_opencode_dir()?; + let plugins_dir = opencode_dir.join("plugins"); + let plugin_path = plugins_dir.join("opencode-rtk.js"); + + // Check OpenCode is installed + if !opencode_dir.exists() { + anyhow::bail!( + "OpenCode config directory not found at {}\n\ + Install OpenCode first: https://opencode.ai", + opencode_dir.display() + ); + } + + // Create plugins directory if needed + fs::create_dir_all(&plugins_dir).with_context(|| { + format!( + "Failed to create plugins directory: {}", + plugins_dir.display() + ) + })?; + + // Write plugin (idempotent) + let changed = write_if_changed(&plugin_path, OPENCODE_PLUGIN, "opencode-rtk.js", verbose)?; + + let status = if changed { + "installed/updated" + } else { + "already up to date" + }; + + println!("\nRTK OpenCode plugin {} ✓\n", status); + println!(" Plugin: {}", plugin_path.display()); + println!( + " OpenCode auto-discovers plugins from {}/", + plugins_dir.display() + ); + println!("\n Restart OpenCode to activate. Test with: git status"); + println!(); + + Ok(()) +} + /// Legacy mode: full 137-line injection into CLAUDE.md fn run_claude_md_mode(global: bool, verbose: u8) -> Result<()> { let path = if global { @@ -1091,6 +1138,13 @@ fn resolve_claude_dir() -> Result { .context("Cannot determine home directory. Is $HOME set?") } +/// Resolve OpenCode configuration directory +fn resolve_opencode_dir() -> Result { + dirs::home_dir() + .map(|h| h.join(".config").join("opencode")) + .context("Cannot determine home directory. Is $HOME set?") +} + /// Show current rtk configuration pub fn show_config() -> Result<()> { let claude_dir = resolve_claude_dir()?; diff --git a/src/main.rs b/src/main.rs index 067f343e..2529c137 100644 --- a/src/main.rs +++ b/src/main.rs @@ -325,6 +325,10 @@ enum Commands { #[arg(long = "hook-only", group = "mode")] hook_only: bool, + /// Install OpenCode plugin (auto-rewrite hook for opencode.ai) + #[arg(long, group = "mode")] + opencode: bool, + /// Auto-patch settings.json without prompting #[arg(long = "auto-patch", group = "patch")] auto_patch: bool, @@ -1486,6 +1490,7 @@ fn main() -> Result<()> { show, claude_md, hook_only, + opencode, auto_patch, no_patch, uninstall, @@ -1494,6 +1499,8 @@ fn main() -> Result<()> { init::show_config()?; } else if uninstall { init::uninstall(global, cli.verbose)?; + } else if opencode { + init::run_opencode_mode(cli.verbose)?; } else { let patch_mode = if auto_patch { init::PatchMode::Auto