Skip to content
Closed
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
28 changes: 23 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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) ---+----------+
Expand Down Expand Up @@ -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)
Expand All @@ -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 |
Expand Down
89 changes: 89 additions & 0 deletions hooks/opencode-rtk.js
Original file line number Diff line number Diff line change
@@ -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}`);
}
}
},
};
};
54 changes: 54 additions & 0 deletions src/init.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -1091,6 +1138,13 @@ fn resolve_claude_dir() -> Result<PathBuf> {
.context("Cannot determine home directory. Is $HOME set?")
}

/// Resolve OpenCode configuration directory
fn resolve_opencode_dir() -> Result<PathBuf> {
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()?;
Expand Down
7 changes: 7 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -1486,6 +1490,7 @@ fn main() -> Result<()> {
show,
claude_md,
hook_only,
opencode,
auto_patch,
no_patch,
uninstall,
Expand All @@ -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
Expand Down