@hheei/omp-optimizer 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,277 @@
1
+ import { describe, expect, it, mock } from "bun:test";
2
+ import type { ExtensionAPI } from "@oh-my-pi/pi-coding-agent";
3
+ import {
4
+ applyRtkRewrite,
5
+ type BashCallEvent,
6
+ buildSudoBlockReason,
7
+ detectSudoSegments,
8
+ probeRtkAvailability,
9
+ rewriteChain,
10
+ splitChain,
11
+ } from "./rtk.ts";
12
+
13
+ /** Build a fresh bash tool_call event for hook tests. */
14
+ function bashEvent(command: string): BashCallEvent {
15
+ return { toolName: "bash", input: { command } };
16
+ }
17
+
18
+ describe("probeRtkAvailability", () => {
19
+ it("probes rtk directly and accepts a successful version check", async () => {
20
+ const exec = mock(async () => ({
21
+ stdout: "rtk 0.1.0",
22
+ stderr: "",
23
+ code: 0,
24
+ killed: false,
25
+ }));
26
+
27
+ expect(await probeRtkAvailability({ exec } as Pick<ExtensionAPI, "exec">)).toBe(true);
28
+ expect(exec).toHaveBeenCalledWith("rtk", ["--version"], { timeout: 3000 });
29
+ });
30
+
31
+ it("rejects an unsuccessful version check", async () => {
32
+ const exec = mock(async () => ({ stdout: "", stderr: "missing", code: 1, killed: false }));
33
+
34
+ expect(await probeRtkAvailability({ exec } as Pick<ExtensionAPI, "exec">)).toBe(false);
35
+ });
36
+
37
+ it("treats a spawn failure as unavailable", async () => {
38
+ const exec = mock(async () => {
39
+ throw new Error("ENOENT");
40
+ });
41
+
42
+ expect(await probeRtkAvailability({ exec } as Pick<ExtensionAPI, "exec">)).toBe(false);
43
+ });
44
+ });
45
+
46
+ describe("splitChain", () => {
47
+ it("returns single segment for plain command", () => {
48
+ expect(splitChain("git status")).toEqual(["git status"]);
49
+ });
50
+
51
+ it("splits on && keeping operator", () => {
52
+ expect(splitChain("git add . && git push")).toEqual(["git add . ", "&&", " git push"]);
53
+ });
54
+
55
+ it("splits on ||, ;, |", () => {
56
+ expect(splitChain("a || b")).toEqual(["a ", "||", " b"]);
57
+ expect(splitChain("a ; b")).toEqual(["a ", ";", " b"]);
58
+ expect(splitChain("a | b")).toEqual(["a ", "|", " b"]);
59
+ });
60
+
61
+ it("ignores operators inside double quotes", () => {
62
+ expect(splitChain('git commit -m "a && b"')).toEqual(['git commit -m "a && b"']);
63
+ });
64
+
65
+ it("ignores operators inside single quotes", () => {
66
+ expect(splitChain("echo 'x | y'")).toEqual(["echo 'x | y'"]);
67
+ });
68
+
69
+ it("returns null on unbalanced quotes", () => {
70
+ expect(splitChain('git commit -m "oops')).toBeNull();
71
+ });
72
+ });
73
+
74
+ describe("rewriteChain", () => {
75
+ it("prefixes a single known command", () => {
76
+ expect(rewriteChain("git status")).toBe("rtk git status");
77
+ });
78
+
79
+ it("prefixes every segment in a chain", () => {
80
+ expect(rewriteChain("git add . && git commit -m x && git push")).toBe(
81
+ "rtk git add . && rtk git commit -m x && rtk git push",
82
+ );
83
+ });
84
+
85
+ it("prefixes mixed known commands", () => {
86
+ expect(rewriteChain("cargo build && npm test")).toBe("rtk cargo build && rtk npm test");
87
+ });
88
+
89
+ it("leaves unknown commands alone", () => {
90
+ expect(rewriteChain("echo hi && mkdir x")).toBe("echo hi && mkdir x");
91
+ });
92
+
93
+ it("does not prefix find (rtk find rejects -not/-exec)", () => {
94
+ const cmd = "find . -type f -not -path '*/node_modules/*'";
95
+ expect(rewriteChain(cmd)).toBe(cmd);
96
+ });
97
+
98
+ it("only prefixes known segments in a mixed chain", () => {
99
+ expect(rewriteChain("cd /tmp && git status")).toBe("cd /tmp && rtk git status");
100
+ });
101
+
102
+ it("does not double-prefix already-rtk commands", () => {
103
+ expect(rewriteChain("rtk git status")).toBe("rtk git status");
104
+ expect(rewriteChain("rtk git add . && git push")).toBe("rtk git add . && rtk git push");
105
+ });
106
+
107
+ it("does not touch operators inside quotes", () => {
108
+ expect(rewriteChain('git commit -m "a && b"')).toBe('rtk git commit -m "a && b"');
109
+ });
110
+
111
+ it("returns original on unbalanced quotes", () => {
112
+ const cmd = 'git commit -m "oops';
113
+ expect(rewriteChain(cmd)).toBe(cmd);
114
+ });
115
+
116
+ it("prefixes known commands across a pipe (ls, wc)", () => {
117
+ expect(rewriteChain("ls -la | wc -l")).toBe("rtk ls -la | rtk wc -l");
118
+ });
119
+
120
+ it("truly leaves a chain of only-unknown commands untouched", () => {
121
+ expect(rewriteChain("cd /tmp | sort | uniq")).toBe("cd /tmp | sort | uniq");
122
+ });
123
+
124
+ it("handles pipes between known commands", () => {
125
+ expect(rewriteChain("git log | grep fix")).toBe("rtk git log | rtk grep fix");
126
+ });
127
+ });
128
+
129
+ // ── detectSudoSegments ───────────────────────────────────────────────────────
130
+
131
+ describe("detectSudoSegments", () => {
132
+ it("returns empty for command with no sudo", () => {
133
+ expect(detectSudoSegments(["git status"])).toEqual([]);
134
+ });
135
+
136
+ it("detects a plain sudo segment", () => {
137
+ expect(detectSudoSegments(["sudo apt-get install foo"])).toEqual(["sudo apt-get install foo"]);
138
+ });
139
+
140
+ it("detects sudo in a chain (operators excluded)", () => {
141
+ const parts = ["git status ", "&&", " sudo make install"];
142
+ expect(detectSudoSegments(parts)).toEqual(["sudo make install"]);
143
+ });
144
+
145
+ it("detects multiple sudo segments", () => {
146
+ const parts = ["sudo rm -rf /tmp ", ";", " sudo reboot"];
147
+ expect(detectSudoSegments(parts)).toEqual(["sudo rm -rf /tmp", "sudo reboot"]);
148
+ });
149
+
150
+ it("does not match 'sudoer' or 'pseudo'", () => {
151
+ expect(detectSudoSegments(["sudoers-check", "pseudo sudo"])).toEqual([]);
152
+ });
153
+
154
+ it("returns empty for operators-only parts", () => {
155
+ expect(detectSudoSegments(["&&", "||", ";"])).toEqual([]);
156
+ });
157
+ });
158
+
159
+ // ── buildSudoBlockReason ──────────────────────────────────────────────────────
160
+
161
+ describe("buildSudoBlockReason", () => {
162
+ it("with sudo_run available: mentions sudo_run tool", () => {
163
+ const reason = buildSudoBlockReason(["sudo apt install curl"], true);
164
+ expect(reason).toContain("sudo_run");
165
+ expect(reason).toContain("sudo apt install curl");
166
+ expect(reason).not.toContain("not available");
167
+ });
168
+
169
+ it("with sudo_run available: instructs to strip sudo prefix", () => {
170
+ const reason = buildSudoBlockReason(["sudo make install"], true);
171
+ expect(reason).toContain("Strip the leading");
172
+ });
173
+
174
+ it("without sudo_run: explains restriction and asks user to run manually", () => {
175
+ const reason = buildSudoBlockReason(["sudo reboot"], false);
176
+ expect(reason).toContain("no sudo_run tool is available");
177
+ expect(reason).toContain("manually");
178
+ expect(reason).toContain("sudo reboot");
179
+ });
180
+
181
+ it("lists all blocked commands", () => {
182
+ const reason = buildSudoBlockReason(["sudo rm -rf /tmp", "sudo reboot"], true);
183
+ expect(reason).toContain("sudo rm -rf /tmp");
184
+ expect(reason).toContain("sudo reboot");
185
+ });
186
+ });
187
+
188
+ // Integration tests for the `tool_call` hook step. These guard the bug that
189
+ // silently disabled rewriting: wrong event name + wrong field + wrong patch
190
+ // mechanism. They assert on the IN-PLACE mutation contract the SDK requires.
191
+ describe("applyRtkRewrite (tool_call hook step)", () => {
192
+ it("mutates event.input.command in place for a known bash command", () => {
193
+ const event = bashEvent("git status");
194
+ const changed = applyRtkRewrite(event, {
195
+ enabled: true,
196
+ rtkAvailable: true,
197
+ });
198
+ expect(changed).toBe(true);
199
+ expect(event.input.command).toBe("rtk git status");
200
+ });
201
+
202
+ it("rewrites every segment of a chain in place", () => {
203
+ const event = bashEvent("git add . && git push");
204
+ applyRtkRewrite(event, { enabled: true, rtkAvailable: true });
205
+ expect(event.input.command).toBe("rtk git add . && rtk git push");
206
+ });
207
+
208
+ it("does not mutate when disabled", () => {
209
+ const event = bashEvent("git status");
210
+ const changed = applyRtkRewrite(event, {
211
+ enabled: false,
212
+ rtkAvailable: true,
213
+ });
214
+ expect(changed).toBe(false);
215
+ expect(event.input.command).toBe("git status");
216
+ });
217
+
218
+ it("does not mutate when rtk binary is unavailable", () => {
219
+ const event = bashEvent("git status");
220
+ const changed = applyRtkRewrite(event, {
221
+ enabled: true,
222
+ rtkAvailable: false,
223
+ });
224
+ expect(changed).toBe(false);
225
+ expect(event.input.command).toBe("git status");
226
+ });
227
+
228
+ it("ignores non-bash tools", () => {
229
+ const event: BashCallEvent = {
230
+ toolName: "grep",
231
+ input: { command: "git status" },
232
+ };
233
+ const changed = applyRtkRewrite(event, {
234
+ enabled: true,
235
+ rtkAvailable: true,
236
+ });
237
+ expect(changed).toBe(false);
238
+ expect(event.input.command).toBe("git status");
239
+ });
240
+
241
+ it("leaves unknown commands untouched", () => {
242
+ const event = bashEvent("mkdir build && cd build");
243
+ const changed = applyRtkRewrite(event, {
244
+ enabled: true,
245
+ rtkAvailable: true,
246
+ });
247
+ expect(changed).toBe(false);
248
+ expect(event.input.command).toBe("mkdir build && cd build");
249
+ });
250
+
251
+ it("does not double-prefix an already-rtk command", () => {
252
+ const event = bashEvent("rtk git status");
253
+ const changed = applyRtkRewrite(event, {
254
+ enabled: true,
255
+ rtkAvailable: true,
256
+ });
257
+ expect(changed).toBe(false);
258
+ expect(event.input.command).toBe("rtk git status");
259
+ });
260
+
261
+ it("handles missing / non-string command safely", () => {
262
+ const event: BashCallEvent = { toolName: "bash", input: {} };
263
+ expect(applyRtkRewrite(event, { enabled: true, rtkAvailable: true })).toBe(false);
264
+ const event2: BashCallEvent = { toolName: "bash", input: { command: 123 } };
265
+ expect(applyRtkRewrite(event2, { enabled: true, rtkAvailable: true })).toBe(false);
266
+ });
267
+
268
+ it("leaves command unchanged on unbalanced quotes", () => {
269
+ const event = bashEvent('git commit -m "oops');
270
+ const changed = applyRtkRewrite(event, {
271
+ enabled: true,
272
+ rtkAvailable: true,
273
+ });
274
+ expect(changed).toBe(false);
275
+ expect(event.input.command).toBe('git commit -m "oops');
276
+ });
277
+ });
package/src/rtk.ts ADDED
@@ -0,0 +1,355 @@
1
+ /**
2
+ * Simple RTK Integration
3
+ *
4
+ * 1. Injects RTK system prompt (tells model to prefix commands with rtk)
5
+ * 2. Rewrites bash commands to add rtk prefix when model forgets
6
+ * 3. Falls back gracefully if rtk binary is missing
7
+ */
8
+
9
+ import type {
10
+ ExtensionAPI,
11
+ ExtensionCommandContext,
12
+ ExtensionContext,
13
+ } from "@oh-my-pi/pi-coding-agent";
14
+ import { canExecute } from "./capability.ts";
15
+ import { loadOptValue, saveOptValue } from "./persist.ts";
16
+ import type { OptimizerHandle, OptimizerStatus } from "./status.ts";
17
+
18
+ /**
19
+ * Minimal structural shape of the SDK `tool_call` event we care about.
20
+ * Mirrors `BashToolCallEvent` from the SDK without importing it, so the
21
+ * rewrite logic stays unit-testable with plain objects.
22
+ */
23
+ export interface BashCallEvent {
24
+ toolName: string;
25
+ input: { command?: unknown };
26
+ }
27
+
28
+ /**
29
+ * Pure decision + mutation step for the `tool_call` hook.
30
+ *
31
+ * Given a tool-call event and whether RTK is available, mutate `event.input`
32
+ * in place (the SDK's only supported way to patch tool args) when the command
33
+ * is a rewritable bash command. Returns true if the command was rewritten.
34
+ *
35
+ * Extracted from the hook closure so the integration is directly testable
36
+ * without a live ExtensionAPI.
37
+ */
38
+ /**
39
+ * Return the list of sudo sub-commands found in parsed chain segments.
40
+ * Each entry is the full segment body (trimmed) that starts with `sudo`.
41
+ * Operators are excluded. Returns empty array if none found.
42
+ */
43
+ export function detectSudoSegments(parts: string[]): string[] {
44
+ return parts
45
+ .filter((p) => !CHAIN_OPERATORS.has(p.trim()))
46
+ .map((p) => p.trim())
47
+ .filter((p) => /^sudo\b/.test(p));
48
+ }
49
+
50
+ /**
51
+ * Build the block reason string shown to the model when a sudo command is
52
+ * intercepted. Directs the model to `sudo_run` when available, otherwise
53
+ * explains the restriction clearly.
54
+ */
55
+ export function buildSudoBlockReason(sudoCmds: string[], hasSudoRunTool: boolean): string {
56
+ const list = sudoCmds.map((c) => ` - ${c}`).join("\n");
57
+ if (hasSudoRunTool) {
58
+ return (
59
+ `bash cannot run sudo commands directly. ` +
60
+ `Use the \`sudo_run\` tool instead — it shows the user a confirmation dialog ` +
61
+ `and handles authentication securely.\n` +
62
+ `Blocked command(s):\n${list}\n` +
63
+ `Strip the leading \`sudo\` from the command and pass the rest to \`sudo_run\` ` +
64
+ `with a clear \`reason\` parameter.`
65
+ );
66
+ }
67
+ return (
68
+ `bash cannot run sudo commands in this session — ` +
69
+ `no sudo_run tool is available and direct sudo is not permitted.\n` +
70
+ `Blocked command(s):\n${list}\n` +
71
+ `Ask the user to run the command manually with elevated privileges.`
72
+ );
73
+ }
74
+
75
+ export function applyRtkRewrite(
76
+ event: BashCallEvent,
77
+ opts: { enabled: boolean; rtkAvailable: boolean },
78
+ ): boolean {
79
+ if (!opts.enabled) return false;
80
+ if (!opts.rtkAvailable) return false;
81
+ if (event.toolName !== "bash") return false;
82
+
83
+ const command = event.input?.command;
84
+ if (typeof command !== "string" || !command) return false;
85
+
86
+ const rewritten = rewriteChain(command);
87
+ if (rewritten === command) return false;
88
+
89
+ event.input.command = rewritten;
90
+ return true;
91
+ }
92
+
93
+ const RTK_SYSTEM_PROMPT = `# RTK — token-optimized command wrapper
94
+
95
+ Prefix shell commands with \`rtk\` (e.g. \`rtk git status\`). RTK compacts output for git, gh, cargo, npm/pnpm/yarn/bun, tsc, lint, vitest/jest/playwright, docker, kubectl, ls, grep, prisma — and passes anything else through unchanged, so it's always safe.
96
+
97
+ Prefix EVERY segment in a chain, not just the first:
98
+ \`rtk git add . && rtk git commit -m "msg" && rtk git push\`
99
+
100
+ RTK also has filtering subcommands the auto-rewriter won't add — reach for these yourself when useful: \`rtk err <cmd>\` (errors only), \`rtk summary <cmd>\`, \`rtk log <file>\` (dedup), \`rtk json <file>\` (structure), \`rtk test <cmd>\` (failures only), \`rtk gain\` (savings stats).`;
101
+
102
+ // Commands that should be prefixed with rtk
103
+ const RTK_COMMANDS = new Set([
104
+ "git",
105
+ "gh",
106
+ "ls",
107
+ "tree",
108
+ "grep",
109
+ "cat",
110
+ "head",
111
+ "tail",
112
+ "tsc",
113
+ "lint",
114
+ "eslint",
115
+ "prettier",
116
+ "next",
117
+ "cargo",
118
+ "rustc",
119
+ "vitest",
120
+ "playwright",
121
+ "jest",
122
+ "test",
123
+ "pnpm",
124
+ "npm",
125
+ "npx",
126
+ "yarn",
127
+ "bun",
128
+ "docker",
129
+ "kubectl",
130
+ "aws",
131
+ "psql",
132
+ "wc",
133
+ "prisma",
134
+ "dotnet",
135
+ ]);
136
+
137
+ interface RtkStatus {
138
+ available: boolean;
139
+ checkedAt: number;
140
+ }
141
+
142
+ /** Probe the command we actually use instead of relying on a platform-specific locator. */
143
+ export function probeRtkAvailability(pi: Pick<ExtensionAPI, "exec">): Promise<boolean> {
144
+ return canExecute(pi, "rtk", ["--version"]);
145
+ }
146
+
147
+ /**
148
+ * Split a command line into segments at top-level shell operators
149
+ * (&&, ||, ;, |), keeping the operators as their own tokens. Operators
150
+ * inside single/double quotes are ignored.
151
+ *
152
+ * Returns null if the parser hits something it can't safely reason about
153
+ * (unbalanced quotes), so the caller can skip rewriting.
154
+ */
155
+ export function splitChain(command: string): string[] | null {
156
+ const out: string[] = [];
157
+ let buf = "";
158
+ let quote: "'" | '"' | null = null;
159
+
160
+ for (let i = 0; i < command.length; i++) {
161
+ const c = command[i] ?? "";
162
+ const next = command[i + 1];
163
+
164
+ if (quote) {
165
+ buf += c;
166
+ if (c === quote) quote = null;
167
+ continue;
168
+ }
169
+
170
+ if (c === "'" || c === '"') {
171
+ quote = c;
172
+ buf += c;
173
+ continue;
174
+ }
175
+
176
+ // two-char operators
177
+ if ((c === "&" && next === "&") || (c === "|" && next === "|")) {
178
+ out.push(buf, c + c);
179
+ buf = "";
180
+ i++;
181
+ continue;
182
+ }
183
+
184
+ // single-char operators
185
+ if (c === ";" || c === "|") {
186
+ out.push(buf, c);
187
+ buf = "";
188
+ continue;
189
+ }
190
+
191
+ buf += c;
192
+ }
193
+
194
+ if (quote) return null; // unbalanced quote — bail out
195
+ out.push(buf);
196
+ return out;
197
+ }
198
+
199
+ const CHAIN_OPERATORS = new Set(["&&", "||", ";", "|"]);
200
+
201
+ /**
202
+ * Prefix each command segment with `rtk` when its first word is a known
203
+ * RTK command and it is not already prefixed. Operators are preserved.
204
+ * Returns the rewritten command, or the original if nothing changed.
205
+ */
206
+ export function rewriteChain(command: string): string {
207
+ const parts = splitChain(command);
208
+ if (!parts) return command; // unparseable — leave untouched
209
+
210
+ let changed = false;
211
+ const rewritten = parts.map((part) => {
212
+ if (CHAIN_OPERATORS.has(part.trim())) return part;
213
+
214
+ const leading = part.match(/^\s*/)?.[0] ?? "";
215
+ const body = part.slice(leading.length);
216
+ if (!body) return part;
217
+
218
+ const firstWord = body.split(/\s+/)[0] ?? "";
219
+ if (firstWord === "rtk") return part;
220
+ if (!RTK_COMMANDS.has(firstWord)) return part;
221
+
222
+ changed = true;
223
+ return `${leading}rtk ${body}`;
224
+ });
225
+
226
+ return changed ? rewritten.join("") : command;
227
+ }
228
+
229
+ export function rtk(pi: ExtensionAPI, status: OptimizerStatus): OptimizerHandle {
230
+ let rtkStatus: RtkStatus | null = null;
231
+ let warnedMissing = false;
232
+ let enabled = true;
233
+ // Tracks whether pix-sudo's sudo_run tool is active this session.
234
+ // Set from before_agent_start selectedTools; defaults to false until known.
235
+ let hasSudoRunTool = false;
236
+
237
+ // Report into the shared optimizer indicator. RTK counts as "on" only when
238
+ // enabled AND the binary is actually available.
239
+ function syncStatus(ctx: Pick<ExtensionContext, "ui">) {
240
+ status.set("rtk", enabled && rtkStatus?.available === true, ctx);
241
+ }
242
+
243
+ // Check if rtk binary is available
244
+ const checkRtkAvailability = async (): Promise<RtkStatus> => {
245
+ // Cache for 60 seconds
246
+ if (rtkStatus && Date.now() - rtkStatus.checkedAt < 60000) {
247
+ return rtkStatus;
248
+ }
249
+
250
+ const available = await probeRtkAvailability(pi);
251
+ rtkStatus = {
252
+ available,
253
+ checkedAt: Date.now(),
254
+ };
255
+ if (available) warnedMissing = false;
256
+ return rtkStatus;
257
+ };
258
+
259
+ // Inject RTK system prompt + detect sudo_run tool availability.
260
+ pi.on("before_agent_start", async (event) => {
261
+ // The active tool list is exposed by OMP's public ExtensionAPI.
262
+ hasSudoRunTool = pi.getActiveTools().includes("sudo_run");
263
+
264
+ if (!enabled) return undefined;
265
+ return { systemPrompt: [RTK_SYSTEM_PROMPT, ...event.systemPrompt] };
266
+ });
267
+
268
+
269
+ // Keep the status indicator in sync across the agent lifecycle. Probe
270
+ // availability on session start so the icon reflects reality immediately.
271
+ pi.on("session_start", async (_event, ctx) => {
272
+ // Restore the user's on/off choice from disk (survives quit/restart).
273
+ const saved = loadOptValue("rtk");
274
+ if (saved === "on" || saved === "off") enabled = saved === "on";
275
+ const probe = await checkRtkAvailability();
276
+ if (!probe.available && !warnedMissing) {
277
+ ctx.ui.notify(
278
+ "rtk not found — RTK rewriting disabled. Install: cargo install rtk-ai",
279
+ "warning",
280
+ );
281
+ warnedMissing = true;
282
+ }
283
+ syncStatus(ctx);
284
+ });
285
+ pi.on("agent_start", async (_event, ctx) => {
286
+ syncStatus(ctx);
287
+ });
288
+ pi.on("agent_end", async (_event, ctx) => {
289
+ syncStatus(ctx);
290
+ });
291
+
292
+ // -- Overlay value handler (called by the /optimizer overlay) --
293
+
294
+ async function run(value: string, ctx: ExtensionCommandContext): Promise<void> {
295
+ enabled = value === "on";
296
+ saveOptValue("rtk", enabled ? "on" : "off");
297
+
298
+ await checkRtkAvailability();
299
+ syncStatus(ctx);
300
+ ctx.ui.notify(`RTK rewriting ${enabled ? "on" : "off"}.`, "info");
301
+ }
302
+
303
+ // Rewrite bash commands to add rtk prefix.
304
+ //
305
+ // The SDK fires a single `tool_call` event for every tool. The bash variant
306
+ // carries `event.toolName === "bash"` and a mutable `event.input` of shape
307
+ // `{ command: string; timeout?: number }`. Arguments are patched by mutating
308
+ // `event.input` IN PLACE — returning `{ toolInput: ... }` does nothing.
309
+ pi.on("tool_call", async (event, ctx) => {
310
+ if (!enabled) {
311
+ return undefined;
312
+ }
313
+
314
+ if (event.toolName !== "bash") {
315
+ return undefined;
316
+ }
317
+
318
+ const probe = await checkRtkAvailability();
319
+
320
+ if (!probe.available) {
321
+ return undefined; // Don't rewrite if rtk not available
322
+ }
323
+
324
+ // First confirmed-available probe may have flipped state — refresh icon.
325
+ syncStatus(ctx);
326
+
327
+ // Block sudo segments before rtk rewriting.
328
+ // splitChain is safe to call here — same parser used by rewriteChain.
329
+ const command = event.input?.command;
330
+ if (typeof command === "string" && command) {
331
+ const parts = splitChain(command);
332
+ if (parts) {
333
+ const sudoCmds = detectSudoSegments(parts);
334
+ if (sudoCmds.length > 0) {
335
+ const reason = buildSudoBlockReason(sudoCmds, hasSudoRunTool);
336
+ return { block: true, reason };
337
+ }
338
+ }
339
+ }
340
+
341
+ // Rewrite every segment in the command chain that uses a known RTK
342
+ // command (e.g. `git add . && git push` -> `rtk git add . && rtk git push`).
343
+ // Mutates `event.input.command` in place — the SDK's supported patch path.
344
+ applyRtkRewrite(event, { enabled, rtkAvailable: probe.available });
345
+ return undefined;
346
+ });
347
+
348
+ return {
349
+ name: "rtk",
350
+ help: "rtk — prefix shell commands with rtk (token-optimized)",
351
+ values: ["off", "on"],
352
+ current: () => (enabled ? "on" : "off"),
353
+ run,
354
+ };
355
+ }