#!/usr/bin/env bash # onboard-rtk.sh — wire ANY machine's OpenCode to `rtk` (the Rust Token Killer): a CLI # proxy that filters/summarizes shell-command output BEFORE it reaches the model context # (git status, ls, kubectl, grep, docker, find, test/build runners …) for big token # savings. This is the cost-control layer: LSP/memory make the agent smarter; rtk makes # each tool call cheaper. # # FIRST-TIMER ONE-LINER (from your project root): # curl -fsSL https://static-ai-stack.dc7.io/install/rtk | bash -s -- install # # install [--global] [--project DIR] [--no-skill] [--no-brew] # status [--global] [--project DIR] # uninstall [--global] [--project DIR] # # WHY PER-PROJECT (and not `rtk init -g`): rtk's own CLI installs its OpenCode plugin # GLOBAL-ONLY. To keep everything self-contained in ./.opencode/ (one source of truth per # repo, no surprise global state), we drop the SAME thin delegator plugin into # .opencode/plugin/rtk.ts — loaded by opencode's auto-discovery of the plugin/ dir. The # plugin is a stable, thin delegator: ALL rewrite rules live in the `rtk` binary's Rust # registry, so the plugin file never needs hand-maintenance. # # WHAT IT DOES: # • brew-installs `rtk` if missing (skip with --no-brew; on non-brew hosts it warns). # • writes .opencode/plugin/rtk.ts (auto-discovered → rewrites bash via `rtk rewrite`). # • writes .opencode/skills/rtk/SKILL.md (awareness skill; loads on demand, zero token # cost until rtk is the task). Skip with --no-skill. # • touches NO opencode.json keys (both plugin + skill load via auto-discovery), so # there's nothing to merge/back-up in the config itself. # # DESIGN GUARANTEES (mirror of the other onboarders): # • Idempotent: re-running never duplicates anything. # • Surgical: only OUR files are added; uninstall removes exactly them. # • Per-project by default (writes ./.opencode/); --global writes ~/.config/opencode/. # # Requires: bash. (curl optional; brew optional for auto-install of rtk.) # This script uses bashisms. Re-exec under bash when possible; for the piped one-liner # invoke with `bash -s` (NOT `sh`). if [ -z "${BASH_VERSION:-}" ] && command -v bash >/dev/null 2>&1 \ && [ -f "$0" ] && [ "$0" != "sh" ] && [ "$0" != "-" ]; then exec bash "$0" "$@" fi set -euo pipefail # ── colors / logging ──────────────────────────────────────────────────────────── if [ -t 1 ]; then C_G=$'\033[32m'; C_Y=$'\033[33m'; C_R=$'\033[31m'; C_B=$'\033[1m'; C_0=$'\033[0m'; else C_G= C_Y= C_R= C_B= C_0=; fi log() { printf '%s%s%s\n' "$C_B" "$*" "$C_0"; } ok() { printf '%s✓%s %s\n' "$C_G" "$C_0" "$*"; } warn() { printf '%s!%s %s\n' "$C_Y" "$C_0" "$*" >&2; } die() { printf '%s✗%s %s\n' "$C_R" "$C_0" "$*" >&2; exit 1; } # ── arg parsing ────────────────────────────────────────────────────────────────── CMD="${1:-}"; shift || true SCOPE="project"; PROJECT_DIR="$PWD" WANT_SKILL=1; WANT_BREW=1 CFG_DIR_PREEXISTED=1 while [ $# -gt 0 ]; do case "$1" in --global) SCOPE="global" ;; --project) PROJECT_DIR="${2:?}"; shift ;; --no-skill) WANT_SKILL=0 ;; --no-brew) WANT_BREW=0 ;; -h|--help) CMD="help" ;; *) die "unknown arg: $1" ;; esac shift done cfg_dir() { if [ "$SCOPE" = "global" ]; then echo "$HOME/.config/opencode"; else echo "$PROJECT_DIR/.opencode"; fi; } CFG_DIR="$(cfg_dir)" # Plural `plugins/` — the only discovery dir the OpenCode 2.x docs define (design D4b). PLUGIN_FILE="$CFG_DIR/plugins/rtk.ts" RTK_PLUGIN_URL="${RTK_PLUGIN_URL:-https://static-ai-stack.dc7.io/rtk.ts}" SKILL_DIR="$CFG_DIR/skills/rtk" OURS_MARKER="$CFG_DIR/.rtk-managed" # ── git hygiene: keep local state + secrets out of commits ─────────────────────── # .opencode/ mixes SHAREABLE config (opencode.json) with PER-MACHINE state (managed # markers, pre-install backups) and SECRETS (the notify sidecar). Nothing used to stop # `git add .opencode/` from committing rows 2-3 — which is exactly how a live ntfy # credential + LiteLLM key reached this repo's remote history. Append-only + idempotent: # we never rewrite a user's .gitignore, only add our missing lines under a marked header. # Duplicated verbatim across the onboarders (they're standalone by design, no shared lib). GI_HEADER="# ── ai-stack onboarders (local state — do not commit) ──" ensure_gitignore() { local gi="$CFG_DIR/.gitignore" line mkdir -p "$CFG_DIR" [ -f "$gi" ] || : > "$gi" # An existing .gitignore may lack a trailing newline (opencode writes one like that), # which would glue our header onto its last entry and corrupt both. Terminate it first. if [ -s "$gi" ] && [ "$(tail -c1 "$gi" 2>/dev/null | wc -l)" -eq 0 ]; then printf '\n' >> "$gi" fi grep -qxF "$GI_HEADER" "$gi" 2>/dev/null || printf '%s\n' "$GI_HEADER" >> "$gi" for line in '.*-managed' '.opencode.json.*-backup' 'plugin/notify.json'; do grep -qxF "$line" "$gi" 2>/dev/null || printf '%s\n' "$line" >> "$gi" done } # ── ensure the rtk binary is present ─────────────────────────────────────────────── ensure_rtk() { if command -v rtk >/dev/null 2>&1; then ok "rtk present ($(rtk --version 2>/dev/null || echo 'unknown version'))" return 0 fi if [ "$WANT_BREW" = "1" ] && command -v brew >/dev/null 2>&1; then log "installing rtk via Homebrew…" brew install rtk >/dev/null 2>&1 && command -v rtk >/dev/null 2>&1 \ && { ok "installed rtk ($(rtk --version 2>/dev/null))"; return 0; } \ || warn "brew install rtk failed — install it manually (https://github.com/… / brew install rtk)." else warn "rtk not found and Homebrew unavailable (or --no-brew). Install rtk so the plugin activates." warn " macOS/Linux (brew): brew install rtk" fi # Not fatal: the plugin self-disables at runtime if the binary is absent. return 0 } # ── the plugin: fetched from the ai-stack (design D1/D4a) ────────────────────────── # This installer used to EMBED a V1 plugin inline. Two problems with that under # OpenCode 2.x: the V1 entrypoint does not load at all, and the embedded copy reached # for `ctx.$`, which V2's plugin context does not provide — so even a mechanical port # would have self-disabled silently. ai-stack owns a ported V2 plugin and publishes it; # we fetch that instead of carrying a second copy here that can drift. write_plugin() { mkdir -p "$CFG_DIR/plugins" if curl -fsSL --max-time 60 "${RTK_PLUGIN_URL}" -o "$PLUGIN_FILE" 2>/dev/null \ && grep -q 'Plugin.define' "$PLUGIN_FILE" 2>/dev/null; then ok "wrote $PLUGIN_FILE (auto-discovered; rewrites shell commands via 'rtk rewrite')" else rm -f "$PLUGIN_FILE" 2>/dev/null || true warn "could not fetch the rtk plugin from ${RTK_PLUGIN_URL} — command rewriting is off until re-run." fi # Remove the V1 copy an older installer wrote, or the surface loads rtk twice. rm -f "$CFG_DIR/plugin/rtk.ts" 2>/dev/null || true rmdir "$CFG_DIR/plugin" 2>/dev/null || true } # ── the awareness skill (loads on demand — zero token cost until rtk is the task) ─── write_skill() { [ "$WANT_SKILL" = "1" ] || return 0 mkdir -p "$SKILL_DIR" cat > "$SKILL_DIR/SKILL.md" <<'MD' --- name: rtk description: rtk — the Rust Token Killer CLI proxy that compresses shell command output to save context tokens. NOT Redux Toolkit. when_to_use: "When the user asks about rtk, whether you're 'wired to use rtk', token savings, or command-output compression. NOT for Redux Toolkit / RTK Query." allowed-tools: Read, Bash --- # RTK — Rust Token Killer (CLI token-optimizing proxy) > In this environment "rtk" = the `rtk` CLI proxy that compresses shell output > (~60–98% token savings). NOT Redux Toolkit. The OpenCode plugin at > .opencode/plugin/rtk.ts rewrites bash commands transparently > (git status -> rtk git status). Meta cmds: `rtk gain`, `rtk cc-economics`. > Verify: `rtk rewrite "git status"` -> "rtk git status". MD ok "wrote $SKILL_DIR/SKILL.md (awareness skill; on-demand)" } write_all() { if [ ! -f "$OURS_MARKER" ]; then [ -d "$CFG_DIR" ] && CFG_DIR_PREEXISTED=1 || CFG_DIR_PREEXISTED=0 else CFG_DIR_PREEXISTED="$(sed -n 's/^dir_preexisted=//p' "$OURS_MARKER" 2>/dev/null || echo 1)" fi mkdir -p "$CFG_DIR" write_plugin write_skill cat > "$OURS_MARKER" </dev/null || echo 1)" rm -f "$PLUGIN_FILE"; ok "removed $PLUGIN_FILE" rm -f "$SKILL_DIR/SKILL.md"; rmdir "$SKILL_DIR" 2>/dev/null || true rmdir "$CFG_DIR/skills" 2>/dev/null || true rmdir "$CFG_DIR/plugin" 2>/dev/null || true rm -f "$OURS_MARKER" if [ "$dir_preexisted" = "0" ] && [ "$SCOPE" = "project" ]; then rmdir "$CFG_DIR" 2>/dev/null && ok "removed empty $CFG_DIR" || true fi ok "the rtk BINARY was left installed (remove with: brew uninstall rtk)." } # ── commands ────────────────────────────────────────────────────────────────────── cmd_install() { log "== install ($SCOPE) → $CFG_DIR ==" ensure_rtk command -v opencode >/dev/null 2>&1 || warn "opencode not found — install it to load the plugin." write_all # Sanity: prove the rewrite works (non-fatal). if command -v rtk >/dev/null 2>&1; then local rw; rw="$(rtk rewrite 'git status' 2>/dev/null || true)" [ "$rw" = "rtk git status" ] && ok "rtk rewrite sanity: 'git status' -> '$rw'" \ || warn "rtk rewrite returned unexpected output: '${rw:-}'" fi log "restart any running opencode session — plugins load at startup." cmd_status } cmd_status() { log "== status ($SCOPE) ==" command -v rtk >/dev/null 2>&1 \ && printf ' binary : rtk present ✓ (%s)\n' "$(rtk --version 2>/dev/null || echo '?')" \ || printf ' binary : rtk NOT in PATH ✗ (plugin self-disables until installed)\n' [ -f "$PLUGIN_FILE" ] \ && printf ' plugin : %s ✓ (auto-discovered)\n' "$PLUGIN_FILE" \ || printf ' plugin : not installed in %s\n' "$CFG_DIR" [ -f "$SKILL_DIR/SKILL.md" ] \ && printf ' skill : %s ✓\n' "$SKILL_DIR/SKILL.md" \ || printf ' skill : not installed\n' } cmd_uninstall() { log "== uninstall ($SCOPE) → $CFG_DIR =="; remove_all; ok "uninstall complete."; } cmd_help() { sed -n '2,37p' "$0" | sed 's/^# \{0,1\}//'; } case "$CMD" in install) cmd_install ;; status) cmd_status ;; uninstall) cmd_uninstall ;; help|"") cmd_help ;; *) die "unknown command: $CMD (use install|status|uninstall|help)" ;; esac