library: RTK (rtk-ai.app) can be switched on per agent — a Library tab lists the agents RTK has a hook for (Claude Code, Codex, Gemini CLI, OpenCode, Cursor, Copilot, Pi, OMP, Hermes), each with a switch that runs RTK's own installer (rtk init -g …) for it, keeping the files it edits aside first; which have it is read from their files, and the tab says RTK's version and the tokens it saved. RTK's installers for OpenCode and Cursor give it to Claude Code too, and its uninstaller for Claude Code or OpenCode takes all three: magpie keeps each switch to its own agent. magpie library rtk [on|off <agent>] does it from the CLI. Asked on X

This commit is contained in:
yetone committed 2026-09-26 19:25:23 +08:00
1 parent 7a3c434610
commit 631e4c3096
7 files changed
+615

No files matched your search

+11
View File
@@ -816,6 +816,17 @@ const I18N = {
"MCP servers": "MCP 服务器",
"Skills": "技能",
"Library folder": "资源库文件夹",
"RTK rewrites the shell commands an agent runs — git status, cargo test, ls… — to print only what the model needs, so they cost fewer tokens. Switch it on for an agent and magpie runs RTK's own installer for it.": "RTK 会改写 Agent 执行的命令行命令(git status、cargo test、ls……),只输出模型需要的部分,从而省 token。为某个 Agent 打开后,magpie 会替它运行 RTK 自带的安装程序。",
"{saved} tokens saved over {n} commands — {pct}% on average": "{n} 条命令共节省 {saved} token,平均省 {pct}%",
"Nothing saved yet: the agents' commands go through RTK once it's switched on and the agent is restarted.": "还没有节省记录:打开后重启 Agent,它的命令就会经过 RTK。",
"RTK isn't installed. Install it (brew install rtk, or see its site), then come back here.": "尚未安装 RTK。先安装(brew install rtk,或见其官网),再回到这里。",
"Get RTK": "获取 RTK",
"Check again": "重新检测",
"restart an agent after switching it": "切换后需重启对应 Agent",
"{agent} runs its commands through RTK": "{agent} 的命令经过 RTK",
"None of your agents is one RTK has a hook for.": "你的 Agent 中没有 RTK 支持的。",
"RTK is on for {agent} — restart it to use it": "已为 {agent} 打开 RTK,重启后生效",
"RTK is off for {agent} — restart it to see it": "已为 {agent} 关闭 RTK,重启后生效",
"No agents to write to": "没有可写入的 Agent",
"Install Claude Code, Codex, Gemini CLI, OpenCode… and the library can give them the same instructions, MCP servers and skills.": "安装 Claude Code、Codex、Gemini CLI、OpenCode 等之后,资源库就能给它们同一份指令、MCP 服务器和技能。",
"magpie keeps a copy of every file before it writes it.": "magpie 写入任何文件前都会先备份一份。",
+2
View File
@@ -273,3 +273,5 @@ body.window .lib-head { top: -2px; margin: -2px -16px 0; padding: 8px 16px 4px;
.mk-sheet .mk-hint .mk-ext { padding: 0; font-family: var(--code); font-size: 11.5px; }
.mk-sheet .mk-hint .mk-ext:hover { background: none; text-decoration: underline; text-underline-offset: 2px; }
.mk-sheet .bar .mk-id { margin-left: 2px; }
.lib-rtk-gain { margin: 6px 12px 10px; font-size: 12px; line-height: 1.5; color: var(--muted); }
.lib-card > .lib-acts { padding: 0 12px 12px; }
+85
View File
@@ -162,6 +162,7 @@
async function load() {
if (!lib) renderLoading();
rtk = null; // asked again when its tab is drawn: an agent may have been installed since
try {
lib = await api("library");
render();
@@ -224,6 +225,7 @@
["instructions", t("Instructions")],
["mcp", t("MCP servers") + (counts.mcp ? " · " + counts.mcp : "")],
["skills", t("Skills") + (counts.skills ? " · " + counts.skills : "")],
["rtk", "RTK"],
], tab, (id) => { tab = id; try { localStorage.setItem("magpie.libTab", id); } catch {} render(); page.scrollTop = 0; });
tabs.classList.add("lib-tabs");
head.append(tabs, el("span", "grow"));
@@ -243,6 +245,7 @@
shown = tab;
if (tab === "instructions") renderInstructions(body);
else if (tab === "mcp") renderServers(body);
else if (tab === "rtk") renderRTK(body);
else renderSkills(body);
page.append(body);
for (const n of body.querySelectorAll(".row-head .note")) n.title = n.textContent;
@@ -269,6 +272,88 @@
function intro(text) { return el("p", "lib-intro", text); }
// ---------- RTK ----------
// RTK (rtk-ai.app) cuts down what the shell commands an agent runs print,
// so their output costs fewer tokens. magpie runs rtk's own installer for
// each agent switched on, and reads the agents' files for which have it.
let rtk = null; // /api/library/rtk
const rtkBusy = new Set(); // agents being switched
let rtkLoading = false;
async function loadRTK() {
if (rtkLoading) return;
rtkLoading = true;
try {
rtk = await api("library/rtk");
} catch (e) {
status(e.message, "err");
}
rtkLoading = false;
if (tab === "rtk" && rtk) render();
}
function renderRTK(body) {
body.append(intro(t("RTK rewrites the shell commands an agent runs — git status, cargo test, ls… — to print only what the model needs, so they cost fewer tokens. Switch it on for an agent and magpie runs RTK's own installer for it.")));
if (!rtk) {
body.append(el("div", "row skeleton"));
loadRTK();
return;
}
const card = el("div", "list lib-card");
const ttl = el("div", "lib-cardhead");
ttl.append(glyph(GLYPH.cmd), el("b", "", "RTK"), el("span", "grow"));
if (rtk.path) {
ttl.append(el("span", "note mono", rtk.version ? "v" + rtk.version : tilde(rtk.path)));
card.append(ttl);
const g = rtk.gain;
card.append(el("p", "lib-rtk-gain", g
? t("{saved} tokens saved over {n} commands — {pct}% on average", { saved: tokens(g.saved), n: g.commands.toLocaleString(), pct: Math.round(g.pct) })
: t("Nothing saved yet: the agents' commands go through RTK once it's switched on and the agent is restarted.")));
} else {
card.append(ttl);
const p = el("p", "lib-rtk-gain", t("RTK isn't installed. Install it (brew install rtk, or see its site), then come back here."));
card.append(p);
const acts = el("div", "lib-acts");
acts.append(button(t("Get RTK"), "action", () => browse(rtk.url)), button(t("Check again"), "", () => { rtk = null; render(); }));
card.append(acts);
}
body.append(card);
const rh = el("div", "row-head");
rh.append(el("span", "label", t("Agents")), el("span", "grow"), el("span", "note", t("restart an agent after switching it")));
body.append(rh);
const list = el("div", "list lib-list");
const rows = rtk.agents.filter((a) => !isHidden(a));
for (const a of rows) {
const row = el("div", "row lib-row");
const who = el("div", "who");
who.append(el("div", "name", a.name));
row.append(icon(a.icon), who, el("span", "grow"));
const sw = toggle(a.on, t("{agent} runs its commands through RTK", { agent: a.name }), (on) => setRTK(a, on));
if (!rtk.path || rtkBusy.has(a.id)) sw.disabled = true;
row.append(sw);
list.append(row);
}
if (!rows.length) list.append(el("div", "lib-none", t("None of your agents is one RTK has a hook for.")));
body.append(list);
}
async function setRTK(a, on) {
rtkBusy.add(a.id);
render();
try {
rtk = await api("library/rtk", { agent: a.id, on });
const names = (rtk.restart || [a.id]).map((id) => rtk.agents.find((x) => x.id === id)?.name || id).join(", ");
status(on ? t("RTK is on for {agent} — restart it to use it", { agent: names }) : t("RTK is off for {agent} — restart it to see it", { agent: names }), "ok", 6000);
} catch (e) {
status(e.message, "err", 8000);
}
rtkBusy.delete(a.id);
render();
}
function tokens(n) {
if (n >= 1e6) return (n / 1e6).toFixed(1) + "M";
if (n >= 1e3) return (n / 1e3).toFixed(1) + "k";
return String(n);
}
// ---------- instructions ----------
function autosize(ta, min) {
+20
View File
@@ -131,6 +131,26 @@ func libraryRoutes(mux *http.ServeMux, w Windows) {
}
writeJSON(rw, p)
})
// RTK: which agents have its hook, and switching one on or off
mux.HandleFunc("GET /api/library/rtk", func(rw http.ResponseWriter, r *http.Request) {
writeJSON(rw, library.ReadRTK())
})
mux.HandleFunc("POST /api/library/rtk", func(rw http.ResponseWriter, r *http.Request) {
var in struct {
Agent string
On bool
}
if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
fail(rw, err)
return
}
v, err := library.SetRTK(in.Agent, in.On)
if err != nil {
fail(rw, err)
return
}
writeJSON(rw, v)
})
mux.HandleFunc("POST /api/library/reveal", func(rw http.ResponseWriter, r *http.Request) {
var in struct{ Path string }
if err := json.NewDecoder(r.Body).Decode(&in); err != nil || in.Path == "" {
+354
View File
@@ -0,0 +1,354 @@
package library
import (
"bytes"
"context"
"encoding/json"
"fmt"
"os"
"os/exec"
"path/filepath"
"slices"
"strings"
"sync"
"time"
"github.com/yetone/magpie/internal/agent"
"github.com/yetone/magpie/internal/proc"
)
// RTK (rtk-ai.app) is a CLI that the shell commands an agent runs go
// through — git status, cargo test, ls — cut down to what the model needs,
// so their output costs fewer tokens. Each agent gets it through a hook its
// own installer writes (rtk init -g …): magpie runs that installer for the
// agents switched on, and reads each one's files to say which have it, since
// rtk init --show knows only of Claude Code, OpenCode and Cursor.
// RTKURL is where rtk is installed from.
const RTKURL = "https://www.rtk-ai.app"
// rtkSpec is how rtk's installer names one agent, and what it writes there.
type rtkSpec struct {
flags []string // after rtk init -g
// patch: the installer takes --auto-patch (Codex's refuses it)
patch bool
// has says whether the agent has rtk's hook, by its files
has func(a *agent.Agent) bool
// touches are the files the installer edits (not those it only adds),
// kept aside by magpie first
touches func(a *agent.Agent) []string
// dir is the agent's folder, made first: rtk writes nothing for an
// agent whose folder isn't there yet
dir func(a *agent.Agent) string
// withClaude: the installer gives it to Claude Code too, which is
// pointed at a folder thrown away after
withClaude bool
}
func contains(path, s string) bool {
b, err := os.ReadFile(path)
return err == nil && bytes.Contains(b, []byte(s))
}
func exists(path string) bool {
_, err := os.Stat(path)
return err == nil
}
var rtkSpecs = map[string]rtkSpec{
"claude": {
patch: true,
has: func(*agent.Agent) bool {
return contains(filepath.Join(claudeDir(), "settings.json"), "rtk hook claude")
},
touches: func(*agent.Agent) []string {
return []string{filepath.Join(claudeDir(), "settings.json"), filepath.Join(claudeDir(), "CLAUDE.md")}
},
dir: func(*agent.Agent) string { return claudeDir() },
},
"codex": {
flags: []string{"--codex"},
has: func(*agent.Agent) bool { return contains(filepath.Join(codexDir(), "hooks.json"), "rtk hook codex") },
touches: func(*agent.Agent) []string {
return []string{filepath.Join(codexDir(), "hooks.json"), filepath.Join(codexDir(), "AGENTS.md")}
},
dir: func(*agent.Agent) string { return codexDir() },
},
"gemini": {
flags: []string{"--gemini"}, patch: true,
has: func(*agent.Agent) bool {
return contains(filepath.Join(home(), ".gemini", "settings.json"), "rtk-hook-gemini")
},
touches: func(*agent.Agent) []string {
return []string{filepath.Join(home(), ".gemini", "settings.json"), filepath.Join(home(), ".gemini", "GEMINI.md")}
},
dir: func(*agent.Agent) string { return filepath.Join(home(), ".gemini") },
},
"opencode": {
flags: []string{"--opencode"}, patch: true, withClaude: true,
has: func(a *agent.Agent) bool { return exists(filepath.Join(filepath.Dir(a.Path), "plugins", "rtk.ts")) },
touches: func(*agent.Agent) []string { return nil },
dir: func(a *agent.Agent) string { return filepath.Dir(a.Path) },
},
"cursor": {
flags: []string{"--agent", "cursor"}, patch: true, withClaude: true,
has: func(*agent.Agent) bool {
return contains(filepath.Join(home(), ".cursor", "hooks.json"), "rtk hook cursor")
},
touches: func(*agent.Agent) []string { return []string{filepath.Join(home(), ".cursor", "hooks.json")} },
dir: func(*agent.Agent) string { return filepath.Join(home(), ".cursor") },
},
"copilot": {
flags: []string{"--copilot"}, patch: true,
has: func(*agent.Agent) bool { return exists(filepath.Join(home(), ".copilot", "hooks", "rtk-rewrite.json")) },
touches: func(*agent.Agent) []string {
return []string{filepath.Join(home(), ".copilot", "copilot-instructions.md")}
},
dir: func(*agent.Agent) string { return filepath.Join(home(), ".copilot") },
},
"pi": {
flags: []string{"--agent", "pi"}, patch: true,
has: func(*agent.Agent) bool { return exists(filepath.Join(home(), ".pi", "agent", "extensions", "rtk.ts")) },
touches: func(*agent.Agent) []string { return nil },
dir: func(*agent.Agent) string { return filepath.Join(home(), ".pi", "agent") },
},
"omp": {
flags: []string{"--agent", "omp"}, patch: true,
has: func(*agent.Agent) bool { return exists(filepath.Join(home(), ".omp", "agent", "extensions", "rtk.ts")) },
touches: func(*agent.Agent) []string { return nil },
dir: func(*agent.Agent) string { return filepath.Join(home(), ".omp", "agent") },
},
"hermes": {
flags: []string{"--agent", "hermes"}, patch: true,
has: func(*agent.Agent) bool {
return exists(filepath.Join(home(), ".hermes", "plugins", "rtk-rewrite", "plugin.yaml"))
},
touches: func(*agent.Agent) []string { return []string{filepath.Join(home(), ".hermes", "config.yaml")} },
dir: func(*agent.Agent) string { return filepath.Join(home(), ".hermes") },
},
}
// rtkBundle: rtk's uninstaller for Claude Code or OpenCode takes it out of
// all three of these at once, so the others are put back after.
var rtkBundle = []string{"claude", "opencode", "cursor"}
// RTKAgent is one agent rtk can be given to, and whether it has it.
type RTKAgent struct {
ID string `json:"id"`
Name string `json:"name"`
Icon string `json:"icon"`
On bool `json:"on"`
}
// RTKGain is what rtk says it saved, over every command it has recorded.
type RTKGain struct {
Commands int `json:"commands"`
Input int64 `json:"input"`
Saved int64 `json:"saved"`
Pct float64 `json:"pct"`
}
// RTKView is the RTK part of the Library page.
type RTKView struct {
Path string `json:"path,omitempty"` // "" when rtk isn't installed
Version string `json:"version,omitempty"`
Gain *RTKGain `json:"gain,omitempty"`
Agents []RTKAgent `json:"agents"`
URL string `json:"url"`
// Restart are the agents a change reached, to be restarted to see it
Restart []string `json:"restart,omitempty"`
Backup string `json:"backup,omitempty"`
}
var rtkMu sync.Mutex
func rtkPath() string {
p, err := exec.LookPath("rtk")
if err != nil {
return ""
}
return p
}
func rtkRun(bin string, args ...string) (string, error) { return rtkRunEnv(bin, nil, args...) }
func rtkRunEnv(bin string, env []string, args ...string) (string, error) {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
cmd := proc.CommandContext(ctx, bin, args...)
// rtk asks before a change it isn't told to make: nothing answers it
cmd.Stdin = nil
if env != nil {
cmd.Env = append(os.Environ(), env...)
}
out, err := cmd.CombinedOutput()
text := strings.TrimSpace(string(out))
if err != nil {
if text == "" {
text = err.Error()
}
return text, fmt.Errorf("rtk %s: %s", strings.Join(args, " "), lastLines(text, 4))
}
return text, nil
}
func lastLines(s string, n int) string {
ls := strings.Split(strings.TrimSpace(s), "\n")
if len(ls) > n {
ls = ls[len(ls)-n:]
}
return strings.Join(ls, " ")
}
// rtkAgents are the agents on this machine rtk can be given to.
func rtkAgents() []*agent.Agent {
var out []*agent.Agent
for _, a := range agent.Detected() {
if _, ok := rtkSpecs[a.ID]; ok {
out = append(out, a)
}
}
return out
}
// ReadRTK says whether rtk is installed, what it saved, and which agents
// have it.
func ReadRTK() *RTKView {
v := &RTKView{Agents: []RTKAgent{}, URL: RTKURL, Path: rtkPath()}
for _, a := range rtkAgents() {
v.Agents = append(v.Agents, RTKAgent{ID: a.ID, Name: a.Name, Icon: a.Icon, On: rtkSpecs[a.ID].has(a)})
}
if v.Path == "" {
return v
}
if out, err := rtkRun(v.Path, "--version"); err == nil {
v.Version = strings.TrimSpace(strings.TrimPrefix(out, "rtk"))
}
if out, err := rtkRun(v.Path, "gain", "--format", "json"); err == nil {
var g struct {
Summary struct {
Commands int `json:"total_commands"`
Input int64 `json:"total_input"`
Saved int64 `json:"total_saved"`
Pct float64 `json:"avg_savings_pct"`
} `json:"summary"`
}
if json.Unmarshal([]byte(out), &g) == nil && g.Summary.Commands > 0 {
s := g.Summary
v.Gain = &RTKGain{Commands: s.Commands, Input: s.Input, Saved: s.Saved, Pct: s.Pct}
}
}
return v
}
// SetRTK gives rtk to an agent, or takes it away, with rtk's own installer.
func SetRTK(id string, on bool) (*RTKView, error) {
rtkMu.Lock()
defer rtkMu.Unlock()
bin := rtkPath()
if bin == "" {
return nil, fmt.Errorf("rtk isn't installed — get it from %s", RTKURL)
}
agents := map[string]*agent.Agent{}
for _, a := range rtkAgents() {
agents[a.ID] = a
}
a := agents[id]
if a == nil {
if _, ok := rtkSpecs[id]; !ok {
return nil, fmt.Errorf("rtk has no hook for %s", id)
}
return nil, fmt.Errorf("%s isn't installed", id)
}
sp := rtkSpecs[id]
if sp.has(a) == on {
v := ReadRTK()
return v, nil
}
// what else the uninstaller takes, to put back
var back []*agent.Agent
if !on && (id == "claude" || id == "opencode") {
for _, o := range rtkBundle {
if b := agents[o]; b != nil && o != id && rtkSpecs[o].has(b) {
back = append(back, b)
}
}
}
b := newBackups()
for _, x := range append([]*agent.Agent{a}, back...) {
for _, p := range rtkSpecs[x.ID].touches(x) {
if err := b.keep(x.ID, p); err != nil {
return nil, err
}
}
}
if on {
if err := rtkInstall(bin, a); err != nil {
return nil, err
}
} else {
if _, err := rtkRun(bin, append(append([]string{"init", "-g"}, sp.flags...), "--uninstall")...); err != nil {
return nil, err
}
for _, x := range back {
if err := rtkInstall(bin, x); err != nil {
return nil, fmt.Errorf("rtk was taken out of %s, but putting it back into %s failed: %w", a.Name, x.Name, err)
}
}
if sp.has(a) {
return nil, fmt.Errorf("rtk's uninstaller left its hook in %s", a.Name)
}
}
if b.dir != "" {
pruneBackups()
}
v := ReadRTK()
// those put back are as they were: only this one has anything new
v.Restart, v.Backup = []string{a.ID}, b.dir
return v, nil
}
func rtkInstall(bin string, a *agent.Agent) error {
sp := rtkSpecs[a.ID]
if err := os.MkdirAll(sp.dir(a), 0o755); err != nil {
return err
}
args := append([]string{"init", "-g"}, sp.flags...)
if sp.patch {
args = append(args, "--auto-patch")
}
var env []string
if sp.withClaude {
tmp, err := os.MkdirTemp("", "magpie-rtk-")
if err != nil {
return err
}
defer os.RemoveAll(tmp)
env = []string{"CLAUDE_CONFIG_DIR=" + tmp}
}
out, err := rtkRunEnv(bin, env, args...)
if err != nil {
return err
}
if !sp.has(a) {
return fmt.Errorf("rtk's installer didn't add its hook to %s: %s", a.Name, lastLines(out, 3))
}
return nil
}
// RTKTakes is the id of the agent q names when rtk can be given to it.
func RTKTakes(q string) (string, error) {
a, err := agent.Find(q)
if err != nil {
return "", err
}
if _, ok := rtkSpecs[a.ID]; !ok {
var ids []string
for id := range rtkSpecs {
ids = append(ids, id)
}
slices.Sort(ids)
return "", fmt.Errorf("rtk has no hook for %s (it has for %s)", a.Name, strings.Join(ids, ", "))
}
return a.ID, nil
}
+95
View File
@@ -0,0 +1,95 @@
package library
import (
"os"
"path/filepath"
"runtime"
"testing"
)
// fakeRTK is an rtk on PATH that acts as rtk 0.50's installer does for
// Claude Code, OpenCode and Cursor: installing OpenCode or Cursor gives it to
// Claude Code as well, and uninstalling Claude Code or OpenCode takes all
// three away.
const fakeRTK = `#!/bin/sh
c="${CLAUDE_CONFIG_DIR:-$HOME/.claude}"
claude() { [ -d "$c" ] && echo '{"hooks":{"PreToolUse":[{"hooks":[{"command":"rtk hook claude"}]}]}}' > "$c/settings.json"; }
case "$1" in
--version) echo "rtk 9.9.9"; exit 0 ;;
gain) echo '{"summary":{"total_commands":3,"total_input":100,"total_saved":60,"avg_savings_pct":60}}'; exit 0 ;;
esac
shift 2
case "$*" in
"--auto-patch") claude ;;
"--opencode --auto-patch") claude; /bin/mkdir -p "$XDG_CONFIG_HOME/opencode/plugins"; echo x > "$XDG_CONFIG_HOME/opencode/plugins/rtk.ts" ;;
"--agent cursor --auto-patch") claude; echo '{"hooks":{"preToolUse":[{"command":"rtk hook cursor"}]}}' > "$HOME/.cursor/hooks.json" ;;
"--agent cursor --uninstall") echo '{}' > "$HOME/.cursor/hooks.json" ;;
"--uninstall"|"--opencode --uninstall")
echo '{}' > "$c/settings.json"; /bin/rm -f "$XDG_CONFIG_HOME/opencode/plugins/rtk.ts"; echo '{}' > "$HOME/.cursor/hooks.json" ;;
*) echo "unknown: $*" >&2; exit 2 ;;
esac
`
func TestRTK(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("the fake rtk is a shell script")
}
h := sandbox(t)
bin := filepath.Join(h, "bin")
write(t, filepath.Join(bin, "rtk"), fakeRTK)
if err := os.Chmod(filepath.Join(bin, "rtk"), 0o755); err != nil {
t.Fatal(err)
}
t.Setenv("PATH", bin)
state := func() map[string]bool {
m := map[string]bool{}
for _, a := range ReadRTK().Agents {
m[a.ID] = a.On
}
return m
}
want := func(claude, opencode, cursor bool) {
t.Helper()
s := state()
if s["claude"] != claude || s["opencode"] != opencode || s["cursor"] != cursor {
t.Fatalf("claude, opencode, cursor = %v, %v, %v; want %v, %v, %v", s["claude"], s["opencode"], s["cursor"], claude, opencode, cursor)
}
}
set := func(id string, on bool) {
t.Helper()
if _, err := SetRTK(id, on); err != nil {
t.Fatal(err)
}
}
v := ReadRTK()
if v.Version != "9.9.9" || v.Gain == nil || v.Gain.Saved != 60 {
t.Fatalf("version %q, gain %+v", v.Version, v.Gain)
}
want(false, false, false)
// OpenCode's and Cursor's installers leave Claude Code as it was
set("opencode", true)
set("cursor", true)
want(false, true, true)
set("claude", true)
want(true, true, true)
// Claude Code's uninstaller takes the other two with it: they're put back
set("claude", false)
want(false, true, true)
set("claude", true)
set("opencode", false)
want(true, false, true)
set("cursor", false)
want(true, false, false)
if _, err := SetRTK("goose", true); err == nil {
t.Fatal("goose has no rtk hook, yet it was switched on")
}
t.Setenv("PATH", "")
if v := ReadRTK(); v.Path != "" {
t.Fatalf("rtk found at %s with nothing on PATH", v.Path)
}
if _, err := SetRTK("claude", false); err == nil {
t.Fatal("switched without rtk installed")
}
}
+48
View File
@@ -19,6 +19,8 @@ const libraryUsage = `magpie library what the library gives
magpie library mcp rm <name>
magpie library skill agents <name> <a,b…|none>
magpie library skill rm <name> (skills are installed from the app's Library page)
magpie library rtk which agents run their shell commands through RTK (rtk-ai.app), to save tokens
magpie library rtk on|off <agent> switch it, with RTK's own installer
`
// libraryCmd is magpie library …: the instructions, MCP servers and skills
@@ -115,6 +117,8 @@ func libraryCmd(args []string) error {
default:
return fmt.Errorf("usage:\n %s", libraryUsage)
}
case "rtk":
return rtkCmd(rest)
case "help", "-h", "--help":
fmt.Print(" " + libraryUsage)
return nil
@@ -239,3 +243,47 @@ func libraryStatus() error {
fmt.Println(muted.Render(" kept in " + v.Dir + " · magpie library help"))
return nil
}
// rtkCmd is magpie library rtk …: RTK's hook in each agent.
func rtkCmd(args []string) error {
var v *library.RTKView
switch {
case len(args) == 0:
v = library.ReadRTK()
case len(args) == 2 && (args[0] == "on" || args[0] == "off"):
id, err := library.RTKTakes(args[1])
if err != nil {
return err
}
if v, err = library.SetRTK(id, args[0] == "on"); err != nil {
return err
}
default:
return fmt.Errorf("usage:\n %s", libraryUsage)
}
if v.Path == "" {
fmt.Println(amber.Render("!"), "rtk isn't installed —", v.URL)
} else {
fmt.Println(bold.Render("RTK"), muted.Render(v.Version+" · "+v.Path))
if g := v.Gain; g != nil {
fmt.Printf(" %d tokens saved over %d commands (%.0f%% on average)\n", g.Saved, g.Commands, g.Pct)
}
}
for _, a := range v.Agents {
mark := muted.Render("off")
if a.On {
mark = green.Render("on ")
}
fmt.Println(" ", mark, a.Name)
}
if len(v.Agents) == 0 {
fmt.Println(" ", muted.Render("none of the agents here is one RTK has a hook for"))
}
if len(v.Restart) > 0 {
fmt.Println(muted.Render(" restart " + strings.Join(v.Restart, ", ") + " for it to take effect"))
}
if v.Backup != "" {
fmt.Println(muted.Render(" what was there before is kept in " + v.Backup))
}
return nil
}