@xynogen/pix-optimizer 1.1.15 → 1.1.17

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.
package/README.md CHANGED
@@ -44,7 +44,7 @@ Switch the style live from the **`/optimizer` overlay** — a fifth `icons` row
44
44
  at the bottom cycles `nerd → unicode → ascii` with `←→`; the choice persists to
45
45
  `~/.pi/agent/optimizer.json`.
46
46
 
47
- Icons follow the **global** `pix-pretty` mode — set via `/pretty` or `PRETTY_ICONS`
47
+ Icons follow the **global** `pix-pretty` mode — set via `/pix` or `PRETTY_ICONS`
48
48
  env var. The optimizer no longer has its own toggle.
49
49
 
50
50
  ## Features
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xynogen/pix-optimizer",
3
- "version": "1.1.15",
3
+ "version": "1.1.17",
4
4
  "description": "Performance optimization suite for Pi Coding Agent - caveman mode + RTK tool rewriting + jq/TOON JSON compression + ponytail lazy-dev mode",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -0,0 +1,42 @@
1
+ import { describe, expect, it, mock } from "bun:test";
2
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
3
+ import { canExecute } from "./capability.ts";
4
+
5
+ function executor(impl: ExtensionAPI["exec"]): Pick<ExtensionAPI, "exec"> {
6
+ return { exec: impl };
7
+ }
8
+
9
+ describe("canExecute", () => {
10
+ it("runs the requested capability probe without a shell", async () => {
11
+ const exec = mock(async () => ({
12
+ stdout: "jq-1.7",
13
+ stderr: "",
14
+ code: 0,
15
+ killed: false,
16
+ }));
17
+
18
+ expect(await canExecute(executor(exec), "jq", ["--version"])).toBe(true);
19
+ expect(exec).toHaveBeenCalledWith("jq", ["--version"], { timeout: 3000 });
20
+ });
21
+
22
+ it("supports a custom timeout", async () => {
23
+ const exec = mock(async () => ({ stdout: "2.3.0", stderr: "", code: 0, killed: false }));
24
+
25
+ expect(await canExecute(executor(exec), "toon", ["--version"], 1000)).toBe(true);
26
+ expect(exec).toHaveBeenCalledWith("toon", ["--version"], { timeout: 1000 });
27
+ });
28
+
29
+ it("returns false for a non-zero exit", async () => {
30
+ const exec = mock(async () => ({ stdout: "", stderr: "failed", code: 1, killed: false }));
31
+
32
+ expect(await canExecute(executor(exec), "jq", ["--version"])).toBe(false);
33
+ });
34
+
35
+ it("returns false when the executable cannot be spawned", async () => {
36
+ const exec = mock(async () => {
37
+ throw new Error("ENOENT");
38
+ });
39
+
40
+ expect(await canExecute(executor(exec), "toon", ["--version"])).toBe(false);
41
+ });
42
+ });
@@ -0,0 +1,16 @@
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+
3
+ /** Test whether a command can perform a harmless operation through Pi's executor. */
4
+ export async function canExecute(
5
+ pi: Pick<ExtensionAPI, "exec">,
6
+ command: string,
7
+ args: string[],
8
+ timeout = 3000,
9
+ ): Promise<boolean> {
10
+ try {
11
+ const result = await pi.exec(command, args, { timeout });
12
+ return result.code === 0;
13
+ } catch {
14
+ return false;
15
+ }
16
+ }
package/src/caveman.ts CHANGED
@@ -108,8 +108,7 @@ export function resolveLevel(arg: string): Level | null {
108
108
  * Help text shown when /caveman is run with no argument.
109
109
  */
110
110
  export function buildHelp(current: Level): string {
111
- const statusLine =
112
- current === "off" ? "off" : `${STATUS_LABELS[current]} (${current})`;
111
+ const statusLine = current === "off" ? "off" : `${STATUS_LABELS[current]} (${current})`;
113
112
  return [
114
113
  `Caveman mode: ${statusLine}`,
115
114
  "",
@@ -133,10 +132,7 @@ export function toggleLevel(current: Level): Level {
133
132
 
134
133
  // ── Pi extension ────────────────────────────────────────────────────────────
135
134
 
136
- export function caveman(
137
- pi: ExtensionAPI,
138
- status: OptimizerStatus,
139
- ): OptimizerHandle {
135
+ export function caveman(pi: ExtensionAPI, status: OptimizerStatus): OptimizerHandle {
140
136
  let level: Level = "off";
141
137
 
142
138
  // -- Status: report into the shared optimizer indicator. --
@@ -179,10 +175,7 @@ export function caveman(
179
175
 
180
176
  // -- Overlay value handler (called by the /optimizer overlay) --
181
177
 
182
- async function run(
183
- value: string,
184
- ctx: ExtensionCommandContext,
185
- ): Promise<void> {
178
+ async function run(value: string, ctx: ExtensionCommandContext): Promise<void> {
186
179
  const resolved = resolveLevel(value);
187
180
  if (resolved === null) return;
188
181
  level = resolved;
@@ -192,9 +185,7 @@ export function caveman(
192
185
  syncStatus(ctx);
193
186
 
194
187
  ctx.ui.notify(
195
- level === "off"
196
- ? "Caveman mode off."
197
- : `Caveman: ${STATUS_LABELS[level]}`,
188
+ level === "off" ? "Caveman mode off." : `Caveman: ${STATUS_LABELS[level]}`,
198
189
  "info",
199
190
  );
200
191
  }
package/src/index.ts CHANGED
@@ -19,15 +19,11 @@ import { json } from "./json.ts";
19
19
  import { registerOptCommand } from "./opt.ts";
20
20
  import { ponytail } from "./ponytail.ts";
21
21
  import { rtk } from "./rtk.ts";
22
- import {
23
- type OptimizerHandle,
24
- OptimizerStatus,
25
- type OptimizerTool,
26
- } from "./status.ts";
22
+ import { type OptimizerHandle, OptimizerStatus, type OptimizerTool } from "./status.ts";
27
23
  import { filterModelWarnings } from "./tool-result-filter.ts";
28
24
 
29
25
  export default function optimizer(pi: ExtensionAPI) {
30
- // Icons follow the global pix-pretty mode (set via /pretty); no local state.
26
+ // Icons follow the global pix-pretty mode (set via /pix); no local state.
31
27
  const status = new OptimizerStatus();
32
28
 
33
29
  // Each module registers its own lifecycle hooks and returns a handle the
package/src/json.test.ts CHANGED
@@ -71,9 +71,7 @@ describe("isUniformObjectArray", () => {
71
71
  });
72
72
 
73
73
  it("false for differing keys", () => {
74
- expect(isUniformObjectArray([{ id: 1 }, { id: 2, extra: true }])).toBe(
75
- false,
76
- );
74
+ expect(isUniformObjectArray([{ id: 1 }, { id: 2, extra: true }])).toBe(false);
77
75
  });
78
76
 
79
77
  it("false when a value is itself an object", () => {
package/src/json.ts CHANGED
@@ -22,6 +22,7 @@ import type {
22
22
  ExtensionCommandContext,
23
23
  ExtensionContext,
24
24
  } from "@earendil-works/pi-coding-agent";
25
+ import { canExecute } from "./capability.ts";
25
26
  import { loadOptValue, saveOptValue } from "./persist.ts";
26
27
  import type { OptimizerHandle, OptimizerStatus } from "./status.ts";
27
28
 
@@ -182,8 +183,7 @@ export function isUniformObjectArray(arr: unknown[]): boolean {
182
183
  for (let i = 0; i < keys.length; i++) {
183
184
  if (elKeys[i] !== keys[i]) return false;
184
185
  // values must be primitive for a clean tabular row
185
- if (isObjectLike((el as Record<string, unknown>)[keys[i] as string]))
186
- return false;
186
+ if (isObjectLike((el as Record<string, unknown>)[keys[i] as string])) return false;
187
187
  }
188
188
  return true;
189
189
  });
@@ -209,21 +209,14 @@ export function objectDepth(value: unknown): number {
209
209
 
210
210
  // ── Pi extension ──────────────────────────────────────────────────────────────
211
211
 
212
- export function json(
213
- pi: ExtensionAPI,
214
- status: OptimizerStatus,
215
- ): OptimizerHandle {
212
+ export function json(pi: ExtensionAPI, status: OptimizerStatus): OptimizerHandle {
216
213
  let enabled = true;
217
214
  let jqAvailable: boolean | null = null;
218
215
  let toonAvailable: boolean | null = null;
219
216
 
220
217
  // Report into the shared optimizer indicator.
221
218
  function syncStatus(ctx: Pick<ExtensionContext, "ui">) {
222
- status.set(
223
- "toon",
224
- enabled && jqAvailable !== false && toonAvailable !== false,
225
- ctx,
226
- );
219
+ status.set("toon", enabled && jqAvailable !== false && toonAvailable !== false, ctx);
227
220
  }
228
221
 
229
222
  pi.on("session_start", async (_event, ctx) => {
@@ -249,14 +242,10 @@ export function json(
249
242
 
250
243
  // Probe once on first JSON-relevant prompt.
251
244
  if (jqAvailable === null || toonAvailable === null) {
252
- const [jqRes, toonRes] = await Promise.all([
253
- pi.exec("which", ["jq"], { timeout: 1000 }).catch(() => ({ code: 1 })),
254
- pi
255
- .exec("which", ["toon"], { timeout: 1000 })
256
- .catch(() => ({ code: 1 })),
245
+ [jqAvailable, toonAvailable] = await Promise.all([
246
+ canExecute(pi, "jq", ["--version"]),
247
+ canExecute(pi, "toon", ["--version"]),
257
248
  ]);
258
- jqAvailable = jqRes.code === 0;
259
- toonAvailable = toonRes.code === 0;
260
249
  if (!jqAvailable)
261
250
  ctx.ui.notify(
262
251
  "jq not found — JSON/TOON guidance disabled. Install: sudo apt install jq or brew install jq",
@@ -277,10 +266,7 @@ export function json(
277
266
 
278
267
  // -- Overlay value handler (called by the /optimizer overlay) --
279
268
 
280
- async function run(
281
- value: string,
282
- ctx: ExtensionCommandContext,
283
- ): Promise<void> {
269
+ async function run(value: string, ctx: ExtensionCommandContext): Promise<void> {
284
270
  enabled = value === "on";
285
271
  saveOptValue("toon", enabled ? "on" : "off");
286
272
 
package/src/opt.test.ts CHANGED
@@ -4,11 +4,7 @@ import type { OptimizerHandle, OptimizerTool } from "./status.ts";
4
4
 
5
5
  /** Build a handle set with fixed current values + value lists. */
6
6
  function fakeHandles(): Record<OptimizerTool, OptimizerHandle> {
7
- const mk = (
8
- name: OptimizerTool,
9
- current: string,
10
- values: string[],
11
- ): OptimizerHandle => ({
7
+ const mk = (name: OptimizerTool, current: string, values: string[]): OptimizerHandle => ({
12
8
  name,
13
9
  help: `${name} — ${name} help`,
14
10
  values,
package/src/opt.ts CHANGED
@@ -16,11 +16,7 @@
16
16
 
17
17
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
18
18
  import { frameLines } from "@xynogen/pix-pretty/modal-frame";
19
- import type {
20
- OptimizerHandle,
21
- OptimizerStatus,
22
- OptimizerTool,
23
- } from "./status.ts";
19
+ import type { OptimizerHandle, OptimizerStatus, OptimizerTool } from "./status.ts";
24
20
  import { toolIcon } from "./status.ts";
25
21
 
26
22
  /** Min/max content width for the overlay box (excludes 4 cols of chrome). */
@@ -28,12 +24,7 @@ const MIN_CONTENT = 28;
28
24
  const MAX_CONTENT = 60;
29
25
 
30
26
  /** Fixed render order — matches the status-bar cell. */
31
- const TOOL_ORDER: readonly OptimizerTool[] = [
32
- "caveman",
33
- "rtk",
34
- "toon",
35
- "ponytail",
36
- ];
27
+ const TOOL_ORDER: readonly OptimizerTool[] = ["caveman", "rtk", "toon", "ponytail"];
37
28
 
38
29
  /** Strip the leading "name [args] — " prefix from a handle's help string. */
39
30
  function helpSummary(help: string): string {
@@ -64,9 +55,7 @@ export function levelBar(current: string, values: readonly string[]): string {
64
55
  }
65
56
 
66
57
  /** Plain-text status fallback when there's no custom-UI host (headless/tests). */
67
- export function buildOptHelp(
68
- handles: Record<OptimizerTool, OptimizerHandle>,
69
- ): string {
58
+ export function buildOptHelp(handles: Record<OptimizerTool, OptimizerHandle>): string {
70
59
  const lines = TOOL_ORDER.map(
71
60
  (n) => ` ${n}: ${handles[n].current()} — ${helpSummary(handles[n].help)}`,
72
61
  );
@@ -140,8 +129,7 @@ export function registerOptCommand(
140
129
  };
141
130
 
142
131
  const move = (direction: -1 | 1) => {
143
- selected =
144
- (selected + direction + TOOL_ORDER.length) % TOOL_ORDER.length;
132
+ selected = (selected + direction + TOOL_ORDER.length) % TOOL_ORDER.length;
145
133
  };
146
134
 
147
135
  return {
@@ -156,14 +144,8 @@ export function registerOptCommand(
156
144
  tool.padEnd(nameWidth),
157
145
  );
158
146
  const cur = handles[tool].current();
159
- const value = theme.fg(
160
- on ? "success" : "dim",
161
- cur.padEnd(valueWidth),
162
- );
163
- const bar = theme.fg(
164
- on ? "accent" : "dim",
165
- levelBar(cur, handles[tool].values),
166
- );
147
+ const value = theme.fg(on ? "success" : "dim", cur.padEnd(valueWidth));
148
+ const bar = theme.fg(on ? "accent" : "dim", levelBar(cur, handles[tool].values));
167
149
  return `${cursor} ${glyph} ${name} ${value} ${bar}`;
168
150
  });
169
151
  const lines = [
@@ -171,12 +153,7 @@ export function registerOptCommand(
171
153
  "",
172
154
  ...rows,
173
155
  "",
174
- theme.fg(
175
- "dim",
176
- helpSummary(
177
- handles[TOOL_ORDER[selected] as OptimizerTool].help,
178
- ),
179
- ),
156
+ theme.fg("dim", helpSummary(handles[TOOL_ORDER[selected] as OptimizerTool].help)),
180
157
  "",
181
158
  theme.fg("dim", "←→ change · ↑↓ move · esc close"),
182
159
  ];
@@ -192,12 +169,7 @@ export function registerOptCommand(
192
169
  if (data === "k" || data === "\u001b[A") move(-1);
193
170
  else if (data === "j" || data === "\u001b[B") move(1);
194
171
  else if (data === "h" || data === "\u001b[D") cycle(-1);
195
- else if (
196
- data === "l" ||
197
- data === "\u001b[C" ||
198
- data === " " ||
199
- data === "\r"
200
- )
172
+ else if (data === "l" || data === "\u001b[C" || data === " " || data === "\r")
201
173
  cycle(1);
202
174
  else if (data === "\u001b" || data === "q") {
203
175
  done(null);
package/src/ponytail.ts CHANGED
@@ -113,8 +113,7 @@ export function resolveLevel(arg: string): Level | null {
113
113
  * Help text shown when /opt ponytail is run with no argument.
114
114
  */
115
115
  export function buildHelp(current: Level): string {
116
- const statusLine =
117
- current === "off" ? "off" : `${STATUS_LABELS[current]} (${current})`;
116
+ const statusLine = current === "off" ? "off" : `${STATUS_LABELS[current]} (${current})`;
118
117
  return [
119
118
  `Ponytail mode: ${statusLine}`,
120
119
  "",
@@ -137,10 +136,7 @@ export function toggleLevel(current: Level): Level {
137
136
 
138
137
  // ── Pi extension ────────────────────────────────────────────────────────────
139
138
 
140
- export function ponytail(
141
- pi: ExtensionAPI,
142
- status: OptimizerStatus,
143
- ): OptimizerHandle {
139
+ export function ponytail(pi: ExtensionAPI, status: OptimizerStatus): OptimizerHandle {
144
140
  let level: Level = "off";
145
141
 
146
142
  // -- Status: report into the shared optimizer indicator. --
@@ -182,10 +178,7 @@ export function ponytail(
182
178
 
183
179
  // -- Overlay value handler (called by the /optimizer overlay) --
184
180
 
185
- async function run(
186
- value: string,
187
- ctx: ExtensionCommandContext,
188
- ): Promise<void> {
181
+ async function run(value: string, ctx: ExtensionCommandContext): Promise<void> {
189
182
  const resolved = resolveLevel(value);
190
183
  if (resolved === null) return;
191
184
  level = resolved;
@@ -195,9 +188,7 @@ export function ponytail(
195
188
  syncStatus(ctx);
196
189
 
197
190
  ctx.ui.notify(
198
- level === "off"
199
- ? "Ponytail mode off."
200
- : `Ponytail: ${STATUS_LABELS[level]}`,
191
+ level === "off" ? "Ponytail mode off." : `Ponytail: ${STATUS_LABELS[level]}`,
201
192
  "info",
202
193
  );
203
194
  }
package/src/rtk.test.ts CHANGED
@@ -1,9 +1,11 @@
1
- import { describe, expect, it } from "bun:test";
1
+ import { describe, expect, it, mock } from "bun:test";
2
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
3
  import {
3
4
  applyRtkRewrite,
4
5
  type BashCallEvent,
5
6
  buildSudoBlockReason,
6
7
  detectSudoSegments,
8
+ probeRtkAvailability,
7
9
  rewriteChain,
8
10
  splitChain,
9
11
  } from "./rtk.ts";
@@ -13,17 +15,41 @@ function bashEvent(command: string): BashCallEvent {
13
15
  return { toolName: "bash", input: { command } };
14
16
  }
15
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
+
16
46
  describe("splitChain", () => {
17
47
  it("returns single segment for plain command", () => {
18
48
  expect(splitChain("git status")).toEqual(["git status"]);
19
49
  });
20
50
 
21
51
  it("splits on && keeping operator", () => {
22
- expect(splitChain("git add . && git push")).toEqual([
23
- "git add . ",
24
- "&&",
25
- " git push",
26
- ]);
52
+ expect(splitChain("git add . && git push")).toEqual(["git add . ", "&&", " git push"]);
27
53
  });
28
54
 
29
55
  it("splits on ||, ;, |", () => {
@@ -33,9 +59,7 @@ describe("splitChain", () => {
33
59
  });
34
60
 
35
61
  it("ignores operators inside double quotes", () => {
36
- expect(splitChain('git commit -m "a && b"')).toEqual([
37
- 'git commit -m "a && b"',
38
- ]);
62
+ expect(splitChain('git commit -m "a && b"')).toEqual(['git commit -m "a && b"']);
39
63
  });
40
64
 
41
65
  it("ignores operators inside single quotes", () => {
@@ -59,9 +83,7 @@ describe("rewriteChain", () => {
59
83
  });
60
84
 
61
85
  it("prefixes mixed known commands", () => {
62
- expect(rewriteChain("cargo build && npm test")).toBe(
63
- "rtk cargo build && rtk npm test",
64
- );
86
+ expect(rewriteChain("cargo build && npm test")).toBe("rtk cargo build && rtk npm test");
65
87
  });
66
88
 
67
89
  it("leaves unknown commands alone", () => {
@@ -74,22 +96,16 @@ describe("rewriteChain", () => {
74
96
  });
75
97
 
76
98
  it("only prefixes known segments in a mixed chain", () => {
77
- expect(rewriteChain("cd /tmp && git status")).toBe(
78
- "cd /tmp && rtk git status",
79
- );
99
+ expect(rewriteChain("cd /tmp && git status")).toBe("cd /tmp && rtk git status");
80
100
  });
81
101
 
82
102
  it("does not double-prefix already-rtk commands", () => {
83
103
  expect(rewriteChain("rtk git status")).toBe("rtk git status");
84
- expect(rewriteChain("rtk git add . && git push")).toBe(
85
- "rtk git add . && rtk git push",
86
- );
104
+ expect(rewriteChain("rtk git add . && git push")).toBe("rtk git add . && rtk git push");
87
105
  });
88
106
 
89
107
  it("does not touch operators inside quotes", () => {
90
- expect(rewriteChain('git commit -m "a && b"')).toBe(
91
- 'rtk git commit -m "a && b"',
92
- );
108
+ expect(rewriteChain('git commit -m "a && b"')).toBe('rtk git commit -m "a && b"');
93
109
  });
94
110
 
95
111
  it("returns original on unbalanced quotes", () => {
@@ -106,9 +122,7 @@ describe("rewriteChain", () => {
106
122
  });
107
123
 
108
124
  it("handles pipes between known commands", () => {
109
- expect(rewriteChain("git log | grep fix")).toBe(
110
- "rtk git log | rtk grep fix",
111
- );
125
+ expect(rewriteChain("git log | grep fix")).toBe("rtk git log | rtk grep fix");
112
126
  });
113
127
  });
114
128
 
@@ -120,9 +134,7 @@ describe("detectSudoSegments", () => {
120
134
  });
121
135
 
122
136
  it("detects a plain sudo segment", () => {
123
- expect(detectSudoSegments(["sudo apt-get install foo"])).toEqual([
124
- "sudo apt-get install foo",
125
- ]);
137
+ expect(detectSudoSegments(["sudo apt-get install foo"])).toEqual(["sudo apt-get install foo"]);
126
138
  });
127
139
 
128
140
  it("detects sudo in a chain (operators excluded)", () => {
@@ -132,10 +144,7 @@ describe("detectSudoSegments", () => {
132
144
 
133
145
  it("detects multiple sudo segments", () => {
134
146
  const parts = ["sudo rm -rf /tmp ", ";", " sudo reboot"];
135
- expect(detectSudoSegments(parts)).toEqual([
136
- "sudo rm -rf /tmp",
137
- "sudo reboot",
138
- ]);
147
+ expect(detectSudoSegments(parts)).toEqual(["sudo rm -rf /tmp", "sudo reboot"]);
139
148
  });
140
149
 
141
150
  it("does not match 'sudoer' or 'pseudo'", () => {
@@ -170,10 +179,7 @@ describe("buildSudoBlockReason", () => {
170
179
  });
171
180
 
172
181
  it("lists all blocked commands", () => {
173
- const reason = buildSudoBlockReason(
174
- ["sudo rm -rf /tmp", "sudo reboot"],
175
- true,
176
- );
182
+ const reason = buildSudoBlockReason(["sudo rm -rf /tmp", "sudo reboot"], true);
177
183
  expect(reason).toContain("sudo rm -rf /tmp");
178
184
  expect(reason).toContain("sudo reboot");
179
185
  });
@@ -254,13 +260,9 @@ describe("applyRtkRewrite (tool_call hook step)", () => {
254
260
 
255
261
  it("handles missing / non-string command safely", () => {
256
262
  const event: BashCallEvent = { toolName: "bash", input: {} };
257
- expect(applyRtkRewrite(event, { enabled: true, rtkAvailable: true })).toBe(
258
- false,
259
- );
263
+ expect(applyRtkRewrite(event, { enabled: true, rtkAvailable: true })).toBe(false);
260
264
  const event2: BashCallEvent = { toolName: "bash", input: { command: 123 } };
261
- expect(applyRtkRewrite(event2, { enabled: true, rtkAvailable: true })).toBe(
262
- false,
263
- );
265
+ expect(applyRtkRewrite(event2, { enabled: true, rtkAvailable: true })).toBe(false);
264
266
  });
265
267
 
266
268
  it("leaves command unchanged on unbalanced quotes", () => {
package/src/rtk.ts CHANGED
@@ -11,6 +11,7 @@ import type {
11
11
  ExtensionCommandContext,
12
12
  ExtensionContext,
13
13
  } from "@earendil-works/pi-coding-agent";
14
+ import { canExecute } from "./capability.ts";
14
15
  import { loadOptValue, saveOptValue } from "./persist.ts";
15
16
  import type { OptimizerHandle, OptimizerStatus } from "./status.ts";
16
17
 
@@ -51,10 +52,7 @@ export function detectSudoSegments(parts: string[]): string[] {
51
52
  * intercepted. Directs the model to `sudo_run` when available, otherwise
52
53
  * explains the restriction clearly.
53
54
  */
54
- export function buildSudoBlockReason(
55
- sudoCmds: string[],
56
- hasSudoRunTool: boolean,
57
- ): string {
55
+ export function buildSudoBlockReason(sudoCmds: string[], hasSudoRunTool: boolean): string {
58
56
  const list = sudoCmds.map((c) => ` - ${c}`).join("\n");
59
57
  if (hasSudoRunTool) {
60
58
  return (
@@ -139,7 +137,11 @@ const RTK_COMMANDS = new Set([
139
137
  interface RtkStatus {
140
138
  available: boolean;
141
139
  checkedAt: number;
142
- path?: string;
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"]);
143
145
  }
144
146
 
145
147
  /**
@@ -224,10 +226,7 @@ export function rewriteChain(command: string): string {
224
226
  return changed ? rewritten.join("") : command;
225
227
  }
226
228
 
227
- export function rtk(
228
- pi: ExtensionAPI,
229
- status: OptimizerStatus,
230
- ): OptimizerHandle {
229
+ export function rtk(pi: ExtensionAPI, status: OptimizerStatus): OptimizerHandle {
231
230
  let rtkStatus: RtkStatus | null = null;
232
231
  let warnedMissing = false;
233
232
  let enabled = true;
@@ -248,25 +247,12 @@ export function rtk(
248
247
  return rtkStatus;
249
248
  }
250
249
 
251
- try {
252
- const result = await pi.exec("which", ["rtk"], { timeout: 1000 });
253
- if (result.code === 0 && result.stdout?.trim()) {
254
- rtkStatus = {
255
- available: true,
256
- checkedAt: Date.now(),
257
- path: result.stdout.trim(),
258
- };
259
- warnedMissing = false;
260
- return rtkStatus;
261
- }
262
- } catch (_error) {
263
- // which command failed
264
- }
265
-
250
+ const available = await probeRtkAvailability(pi);
266
251
  rtkStatus = {
267
- available: false,
252
+ available,
268
253
  checkedAt: Date.now(),
269
254
  };
255
+ if (available) warnedMissing = false;
270
256
  return rtkStatus;
271
257
  };
272
258
 
@@ -306,10 +292,7 @@ export function rtk(
306
292
 
307
293
  // -- Overlay value handler (called by the /optimizer overlay) --
308
294
 
309
- async function run(
310
- value: string,
311
- ctx: ExtensionCommandContext,
312
- ): Promise<void> {
295
+ async function run(value: string, ctx: ExtensionCommandContext): Promise<void> {
313
296
  enabled = value === "on";
314
297
  saveOptValue("rtk", enabled ? "on" : "off");
315
298
 
@@ -1,10 +1,5 @@
1
1
  import { describe, expect, it } from "bun:test";
2
- import {
3
- OptimizerStatus,
4
- renderStatus,
5
- STATUS_KEY,
6
- toolIcon,
7
- } from "./status.ts";
2
+ import { OptimizerStatus, renderStatus, STATUS_KEY, toolIcon } from "./status.ts";
8
3
 
9
4
  /** Tagging colorizer: <accent>X</accent> / <dim>X</dim> for assertions. */
10
5
  const tag = (c: string, t: string) => `<${c}>${t}</${c}>`;
@@ -17,23 +12,13 @@ const PT = toolIcon("ponytail");
17
12
 
18
13
  describe("renderStatus", () => {
19
14
  it("shows ALL icons in order, accent when enabled", () => {
20
- expect(
21
- renderStatus(
22
- { caveman: true, rtk: true, toon: true, ponytail: true },
23
- tag,
24
- ),
25
- ).toBe(
15
+ expect(renderStatus({ caveman: true, rtk: true, toon: true, ponytail: true }, tag)).toBe(
26
16
  `<accent>${CV}</accent> <accent>${RK}</accent> <accent>${TN}</accent> <accent>${PT}</accent> `,
27
17
  );
28
18
  });
29
19
 
30
20
  it("dims disabled tools but still shows them", () => {
31
- expect(
32
- renderStatus(
33
- { caveman: false, rtk: true, toon: true, ponytail: true },
34
- tag,
35
- ),
36
- ).toBe(
21
+ expect(renderStatus({ caveman: false, rtk: true, toon: true, ponytail: true }, tag)).toBe(
37
22
  `<dim>${CV}</dim> <accent>${RK}</accent> <accent>${TN}</accent> <accent>${PT}</accent> `,
38
23
  );
39
24
  });
@@ -63,8 +48,7 @@ describe("OptimizerStatus", () => {
63
48
  return {
64
49
  calls,
65
50
  ui: {
66
- setStatus: (key: string, text: string | undefined) =>
67
- calls.push({ key, text: text ?? "" }),
51
+ setStatus: (key: string, text: string | undefined) => calls.push({ key, text: text ?? "" }),
68
52
  theme: { fg: (c: string, t: string) => `<${c}>${t}</${c}>` },
69
53
  },
70
54
  } as const;
package/src/status.ts CHANGED
@@ -17,11 +17,7 @@ import type {
17
17
  ExtensionContext,
18
18
  ThemeColor,
19
19
  } from "@earendil-works/pi-coding-agent";
20
- import {
21
- type IconKey,
22
- icon,
23
- onIconModeChange,
24
- } from "@xynogen/pix-pretty/icon-catalog";
20
+ import { type IconKey, icon, onIconModeChange } from "@xynogen/pix-pretty/icon-catalog";
25
21
 
26
22
  /**
27
23
  * Each optimizer tool (caveman/rtk/toon/ponytail) exposes a handle so the
@@ -49,7 +45,7 @@ export type OptimizerTool = "caveman" | "rtk" | "toon" | "ponytail";
49
45
 
50
46
  /**
51
47
  * Icons come from the shared pix-pretty catalog and follow the ONE global icon
52
- * mode (set via /pretty). The optimizer no longer owns a mode: each tool maps
48
+ * mode (set via /pix). The optimizer no longer owns a mode: each tool maps
53
49
  * to a semantic catalog key, and the cell renders whatever the active mode
54
50
  * resolves — nerd / unicode / ascii — with no per-package knob.
55
51
  */
@@ -66,12 +62,7 @@ export function toolIcon(tool: OptimizerTool): string {
66
62
  }
67
63
 
68
64
  /** Fixed left-to-right order of icons in the cell. */
69
- const TOOL_ORDER: readonly OptimizerTool[] = [
70
- "caveman",
71
- "rtk",
72
- "toon",
73
- "ponytail",
74
- ];
65
+ const TOOL_ORDER: readonly OptimizerTool[] = ["caveman", "rtk", "toon", "ponytail"];
75
66
 
76
67
  /** Theme color for enabled icons. */
77
68
  const ENABLED_COLOR: ThemeColor = "accent";
@@ -85,7 +76,7 @@ export type Colorize = (color: ThemeColor, text: string) => string;
85
76
  * Build the colored status string for a set of tool states. ALL tool icons are
86
77
  * always shown, in TOOL_ORDER; each is accent-colored when its tool is enabled
87
78
  * and dim when disabled. Glyphs resolve against the active global icon mode
88
- * (see /pretty). `color` applies the theme color (e.g. theme.fg).
79
+ * (see /pix). `color` applies the theme color (e.g. theme.fg).
89
80
  *
90
81
  * Pure + exported for tests (pass a tagging colorizer to assert per-icon color).
91
82
  * A trailing space separates the cell from the next status segment.
@@ -111,7 +102,7 @@ export class OptimizerStatus {
111
102
 
112
103
  constructor() {
113
104
  // The cell is a pushed status (setStatus), so it does NOT auto-refresh
114
- // when /pretty flips the global icon mode. Repaint it on mode change
105
+ // when /pix flips the global icon mode. Repaint it on mode change
115
106
  // using the last-seen ctx (no-op until the cell has painted once).
116
107
  onIconModeChange(() => {
117
108
  if (this.lastCtx) this.paint(this.lastCtx);
@@ -124,11 +115,7 @@ export class OptimizerStatus {
124
115
  }
125
116
 
126
117
  /** Update one tool's enabled state and repaint the shared cell. */
127
- set(
128
- tool: OptimizerTool,
129
- enabled: boolean,
130
- ctx: Pick<ExtensionContext, "ui">,
131
- ): void {
118
+ set(tool: OptimizerTool, enabled: boolean, ctx: Pick<ExtensionContext, "ui">): void {
132
119
  this.states[tool] = enabled;
133
120
  this.paint(ctx);
134
121
  }
@@ -1,8 +1,5 @@
1
1
  import { describe, expect, it } from "bun:test";
2
- import {
3
- filterModelWarnings,
4
- stripModelWarningParagraphs,
5
- } from "./tool-result-filter.ts";
2
+ import { filterModelWarnings, stripModelWarningParagraphs } from "./tool-result-filter.ts";
6
3
 
7
4
  // ── stripModelWarningParagraphs ───────────────────────────────────────────────
8
5
 
@@ -29,8 +26,7 @@ describe("stripModelWarningParagraphs", () => {
29
26
  });
30
27
 
31
28
  it("strips multiple warning paragraphs", () => {
32
- const text =
33
- "output\n\n⚠ BLIND WRITE — foo\n\n🔴 THRASHING — bar\n\nmore output";
29
+ const text = "output\n\n⚠ BLIND WRITE — foo\n\n🔴 THRASHING — bar\n\nmore output";
34
30
  const result = stripModelWarningParagraphs(text);
35
31
  expect(result).not.toContain("BLIND WRITE");
36
32
  expect(result).not.toContain("THRASHING");
@@ -75,9 +71,7 @@ describe("filterModelWarnings", () => {
75
71
  expect(result).not.toBe(content);
76
72
  expect(result[0]?.type).toBe("text");
77
73
  if (result[0]?.type === "text") {
78
- expect((result[0] as { type: string; text: string }).text).not.toContain(
79
- "BLIND WRITE",
80
- );
74
+ expect((result[0] as { type: string; text: string }).text).not.toContain("BLIND WRITE");
81
75
  }
82
76
  });
83
77
 
@@ -28,10 +28,7 @@ export function stripModelWarningParagraphs(text: string): string {
28
28
  // then rejoin. Trim trailing whitespace from the result.
29
29
  const paragraphs = text.split(/\n\n/);
30
30
  const kept = paragraphs.filter(
31
- (p) =>
32
- !MODEL_WARNING_PREFIXES.some((prefix) =>
33
- p.trimStart().startsWith(prefix),
34
- ),
31
+ (p) => !MODEL_WARNING_PREFIXES.some((prefix) => p.trimStart().startsWith(prefix)),
35
32
  );
36
33
  if (kept.length === paragraphs.length) return text;
37
34
  return kept.join("\n\n").trimEnd();