@xynogen/pix-optimizer 1.2.0 → 1.3.1

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
@@ -23,7 +23,7 @@ There is no text-arg form: the overlay is the only UI. Selecting a value calls
23
23
  the tool's `run()` handler, which persists the new value and repaints the
24
24
  shared status cell. Headless/test fallbacks print a plain status summary.
25
25
 
26
- State persists to `~/.pi/agent/optimizer.json` (caveman/rtk/ponytail). **Initial values** for a new session can be set in the `optimizer` section of `~/.pi/agent/pix.json` — the runtime toggle via `/optimizer` still persists changes to `optimizer.json` as before.
26
+ State (caveman/rtk/ponytail) persists in the `optimizer` section of `~/.pi/agent/pix.json`. That section is the only place it is stored; the old `optimizer.json` file is imported once by pix-runtime and never written again.
27
27
 
28
28
  ## Status bar
29
29
 
@@ -63,7 +63,7 @@ article-dropping caveman prompt.
63
63
  | micro | Experimental prompt-minimized |
64
64
 
65
65
  The `/optimizer` overlay opens a settings dialog when needed. Default level
66
- for new sessions is restored from `~/.pi/agent/optimizer.json`.
66
+ for new sessions is restored from `pix.json` → `optimizer`.
67
67
 
68
68
  ### RTK Tool Rewriting (`Rk`)
69
69
 
@@ -77,14 +77,26 @@ Two layers, both active automatically:
77
77
  `||`, `;` and `|`, and every known segment is prefixed** — e.g.
78
78
  `git add . && git push` becomes `rtk git add . && rtk git push`.
79
79
  Operators inside quotes are ignored, and unparseable commands are left
80
- untouched. Falls back gracefully when the `rtk` binary is missing
81
- (warns once).
80
+ untouched. Commands are never rewritten while `rtk` is missing.
82
81
 
83
- **Requirement:** the `rtk` binary must be on `PATH`.
82
+ **Binary:** pix finds `rtk` in this order: `binary.json` → `~/.pi/agent/bin` →
83
+ PATH (see pix-runtime, Binaries).
84
84
 
85
- ```bash
86
- cargo install rtk-ai
87
- ```
85
+ When RTK is on and `rtk` is missing, pix downloads the latest official
86
+ release from `rtk-ai/rtk` into `~/.pi/agent/bin` the first time Pi loads. It
87
+ verifies the download against the release's `checksums.txt`.
88
+
89
+ The download is visible: a footer status while it runs, then one line naming
90
+ the version and source. It never blocks startup or a tool call.
91
+
92
+ The download is skipped when:
93
+
94
+ - `PI_OFFLINE` is set;
95
+ - RTK is off in `/optimizer`;
96
+ - `binary.json` names a path that doesn't exist.
97
+
98
+ If you pin a path in `binary.json`, rewritten commands use that quoted path
99
+ instead of a bare `rtk`.
88
100
 
89
101
  ### Ponytail Mode (`Pt`)
90
102
 
@@ -105,7 +117,7 @@ PATH dependency.
105
117
 
106
118
  ## Configuration via `pix.json`
107
119
 
108
- Set the initial optimizer state for new sessions in `~/.pi/agent/pix.json`. These values are applied once at session start; subsequent changes via `/optimizer` persist to `optimizer.json` and take precedence.
120
+ Optimizer state lives in `~/.pi/agent/pix.json`. You can edit it by hand, or use `/optimizer`, which writes to the same section.
109
121
 
110
122
  ```jsonc
111
123
  {
@@ -117,18 +129,6 @@ Set the initial optimizer state for new sessions in `~/.pi/agent/pix.json`. Thes
117
129
  }
118
130
  ```
119
131
 
120
- ## Installation
121
-
122
- ```bash
123
- pi install npm:@xynogen/pix-optimizer
124
- ```
125
-
126
- > Also included in [`@xynogen/pix-core`](https://www.npmjs.com/package/@xynogen/pix-core):
127
- >
128
- > ```bash
129
- > pi install npm:@xynogen/pix-core
130
- > ```
131
-
132
132
  ## Architecture
133
133
 
134
134
  | File | Role |
@@ -139,7 +139,7 @@ pi install npm:@xynogen/pix-optimizer
139
139
  | `src/caveman.ts` | Caveman logic, levels, prompt |
140
140
  | `src/rtk.ts` | RTK prompt + bash command rewriting |
141
141
  | `src/ponytail.ts` | Ponytail logic, levels, prompt |
142
- | `src/persist.ts` | Disk-backed `~/.pi/agent/optimizer.json` persistence; seeds initial state from `pix.json` |
142
+ | `src/persist.ts` | Reads/writes the `optimizer` section of `pix.json` via pix-runtime |
143
143
  | `src/tool-result-filter.ts` | Strips model-guidance warnings from tool_result |
144
144
 
145
145
  Each tool registers its own lifecycle hooks and exposes an `OptimizerHandle`
@@ -172,16 +172,28 @@ All upstreams are MIT licensed. No codebase was copied directly — the logic wa
172
172
  rewritten and combined into a single extension with a unified `/optimizer`
173
173
  command and shared status bar. This package does not sync back to any upstream.
174
174
 
175
- ## Full distro
175
+ ## Install
176
+
177
+ ```bash
178
+ pi install npm:@xynogen/pix-optimizer
179
+ ```
180
+
181
+ > Bundled in [`@xynogen/pix-core`](https://www.npmjs.com/package/@xynogen/pix-core). Install it alone only if you do not use pix-core.
176
182
 
177
- Source: [github.com/xynogen/pix-mono](https://github.com/xynogen/pix-mono)
183
+ ## Full distro
178
184
 
179
- To install the complete pix suite (all packages + Pi itself):
185
+ This package is part of [Pix](https://github.com/xynogen/pix-mono). The installer sets up Pi and the full distro. See [Install](https://github.com/xynogen/pix-mono#install) for the notes for each OS.
180
186
 
181
187
  ```bash
188
+ # Linux / macOS
182
189
  curl -fsSL https://raw.githubusercontent.com/xynogen/pix-mono/main/scripts/install.sh | sh
183
190
  ```
184
191
 
192
+ ```powershell
193
+ # Windows
194
+ irm https://raw.githubusercontent.com/xynogen/pix-mono/main/scripts/install.ps1 | iex
195
+ ```
196
+
185
197
  ## License
186
198
 
187
- MIT
199
+ MIT. See [LICENSE](LICENSE).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xynogen/pix-optimizer",
3
- "version": "1.2.0",
3
+ "version": "1.3.1",
4
4
  "description": "Performance optimization suite for Pi Coding Agent - caveman mode + RTK tool rewriting + ponytail lazy-dev mode",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -43,7 +43,8 @@
43
43
  "access": "public"
44
44
  },
45
45
  "dependencies": {
46
- "@xynogen/pix-pretty": "^1.24.0"
46
+ "@xynogen/pix-pretty": "^1.30.0",
47
+ "@xynogen/pix-runtime": "^0.13.0"
47
48
  },
48
49
  "peerDependencies": {
49
50
  "@earendil-works/pi-coding-agent": "*",
package/src/caveman.ts CHANGED
@@ -80,7 +80,7 @@ Write in Simplified Technical English. Use short common words, the active voice,
80
80
  const INTENSITY: Record<Exclude<Level, "off" | "micro">, string> = {
81
81
  lite: `\
82
82
  STE-flavored words. Keep the sentence, tense, active-voice, and no-phrasal-verb discipline. \
83
- Relax the strict dictionary. Apply Layer 2 lightly: lead with the answer, no preamble or closer.
83
+ Relax the strict dictionary. Apply the reply shape lightly: lead with the answer, no preamble or closer.
84
84
  Example: "The component re-renders because you create a new object reference each render. Wrap it in \`useMemo\`."`,
85
85
 
86
86
  full: `\
@@ -109,6 +109,9 @@ Boundaries: this governs prose, not code. "stop caveman" or "normal mode" revert
109
109
  export function buildPrompt(level: Level): string {
110
110
  if (level === "off") return "";
111
111
  if (level === "micro") return MICRO_PROMPT;
112
+ // ponytail: lite relaxes most of BASE, so it ships the short MICRO rules + its intensity (~800 tokens less).
113
+ if (level === "lite")
114
+ return [MICRO_PROMPT, "", `Intensity: ${INTENSITY.lite}`, "", SAFETY].join("\n");
112
115
  return [BASE, "", `Intensity: ${INTENSITY[level]}`, "", SAFETY].join("\n");
113
116
  }
114
117
 
package/src/mode.ts CHANGED
@@ -14,6 +14,7 @@ import type {
14
14
  ExtensionContext,
15
15
  } from "@earendil-works/pi-coding-agent";
16
16
  import { showTransientError } from "@xynogen/pix-pretty/transient-error";
17
+ import { getErrorMessage } from "@xynogen/pix-pretty/utils";
17
18
  import { loadOptValue, saveOptValue } from "./persist.ts";
18
19
  import type { OptimizerHandle, OptimizerStatus, OptimizerTool } from "./status.ts";
19
20
 
@@ -87,7 +88,7 @@ export function createMode<L extends string>(
87
88
  }
88
89
  }
89
90
  const saved = loadOptValue(name);
90
- if (saved && levels.includes(saved as L)) level = saved as L;
91
+ if (levels.includes(saved as L)) level = saved as L;
91
92
  syncStatus(ctx);
92
93
  });
93
94
 
@@ -102,9 +103,9 @@ export function createMode<L extends string>(
102
103
 
103
104
  pi.appendEntry(customType, { level });
104
105
  try {
105
- saveOptValue(name, level);
106
+ await saveOptValue(name, level);
106
107
  } catch (error) {
107
- const detail = error instanceof Error ? error.message : String(error);
108
+ const detail = getErrorMessage(error);
108
109
  showTransientError(ctx.ui, `optimizer: failed to save ${name}: ${detail}`);
109
110
  }
110
111
  syncStatus(ctx);
package/src/opt.ts CHANGED
@@ -16,6 +16,7 @@
16
16
 
17
17
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
18
18
  import { type KeybindingsManager, matchesKey } from "@earendil-works/pi-tui";
19
+ import { icon } from "@xynogen/pix-pretty/icon-catalog";
19
20
  import {
20
21
  frameModal,
21
22
  MIN_MODAL_HEIGHT,
@@ -153,7 +154,7 @@ export function registerOptCommand(
153
154
  width,
154
155
  maxHeight: terminalModalHeight(tui.terminal?.rows),
155
156
  minHeight: MIN_MODAL_HEIGHT,
156
- header: [theme.fg("accent", theme.bold("󱎫 Optimizer")), ""],
157
+ header: [theme.fg("accent", theme.bold(`${icon("opt.title")} Optimizer`)), ""],
157
158
  body: rows,
158
159
  footer: [
159
160
  "",
package/src/persist.ts CHANGED
@@ -1,54 +1,27 @@
1
1
  /**
2
- * persist.ts — disk-backed persistence for the /optimizer tool states.
3
- *
4
- * caveman/ponytail previously saved only to the session log (lost on a fresh
5
- * session); rtk never persisted at all. This stores every tool's current
6
- * value in one file under the agent dir so the picker survives a full quit and
7
- * restart. Each tool reads its value on session_start and writes on run().
8
- *
9
- * ~/.pi/agent/optimizer.json → { "caveman": "lite", "rtk": "on", ... }
2
+ * persist.ts — /optimizer tool states live in pix.json `optimizer` (owned by
3
+ * pix-runtime). Replaces the old `optimizer.json` sidecar, which pix-runtime
4
+ * imports and archives on init; writing it again made two sources of truth
5
+ * that overwrote each other.
10
6
  */
11
7
 
12
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
13
- import { dirname, join } from "node:path";
14
- import { getAgentDir } from "@earendil-works/pi-coding-agent";
8
+ import { config, updateConfig } from "@xynogen/pix-runtime/config";
9
+ import { type OptimizerConfig, optimizerSection } from "@xynogen/pix-runtime/sections";
15
10
  import type { OptimizerTool } from "./status.ts";
16
11
 
17
- type OptimizerFileConfig = Partial<Record<OptimizerTool, string>>;
18
-
19
- const OPTIMIZER_TOOLS: readonly OptimizerTool[] = ["caveman", "rtk", "ponytail"];
20
-
21
- function getStatePath(): string {
22
- return join(getAgentDir(), "optimizer.json");
23
- }
24
-
25
- function readFile(): OptimizerFileConfig {
26
- try {
27
- const sp = getStatePath();
28
- if (!existsSync(sp)) return {};
29
- const raw = JSON.parse(readFileSync(sp, "utf-8")) as unknown;
30
- if (!raw || typeof raw !== "object" || Array.isArray(raw)) return {};
31
-
32
- const config: OptimizerFileConfig = {};
33
- for (const tool of OPTIMIZER_TOOLS) {
34
- const value = (raw as Record<string, unknown>)[tool];
35
- if (typeof value === "string") config[tool] = value;
36
- }
37
- return config;
38
- } catch {
39
- return {};
40
- }
12
+ /** Read a tool's value from pix.json (section defaults when unset). */
13
+ export function loadOptValue(tool: OptimizerTool): string {
14
+ return config(optimizerSection)[tool];
41
15
  }
42
16
 
43
- /** Read a single tool's persisted value from optimizer.json. */
44
- export function loadOptValue(tool: OptimizerTool): string | undefined {
45
- return readFile()[tool];
46
- }
47
-
48
- /** Persist a single tool's value, merging into the shared config file. */
49
- export function saveOptValue(tool: OptimizerTool, value: string): void {
50
- const sp = getStatePath();
51
- mkdirSync(dirname(sp), { recursive: true });
52
- const next = { ...readFile(), [tool]: value };
53
- writeFileSync(sp, JSON.stringify(next, null, 2), "utf-8");
17
+ /**
18
+ * Persist a tool's value into pix.json. Invalid values are rejected by the
19
+ * section parser and snap to the default. Rejects on write failure so the UI
20
+ * caller can render it.
21
+ */
22
+ export async function saveOptValue(tool: OptimizerTool, value: string): Promise<void> {
23
+ await updateConfig(optimizerSection, { [tool]: value } as Partial<OptimizerConfig>, {
24
+ origin: "command",
25
+ source: "pix-optimizer",
26
+ });
54
27
  }
package/src/ponytail.ts CHANGED
@@ -94,8 +94,7 @@ error handling that prevents data loss, security, accessibility, or anything the
94
94
  The hardware is never the spec ideal. Leave the calibration knob.
95
95
  Lazy code without its check is unfinished. Non-trivial logic leaves ONE runnable check behind \
96
96
  (an assert-based self-check or one small test file, no frameworks). A trivial one-liner needs no test.
97
- Output: write the code first, then at most three short lines — what you skipped, and when to add it. \
98
- Write these lines in Simplified Technical English: short common words, the active voice, and simple tenses.
97
+ Output: write the code first, then at most three short lines — what you skipped, and when to add it.
99
98
  Boundaries: ponytail governs what you build, not how you talk. "stop ponytail" or "normal mode" reverts.`;
100
99
 
101
100
  /**
package/src/rtk.ts CHANGED
@@ -11,8 +11,10 @@ import type {
11
11
  ExtensionCommandContext,
12
12
  ExtensionContext,
13
13
  } from "@earendil-works/pi-coding-agent";
14
+ import { reportToolStatus } from "@xynogen/pix-pretty/tool-status";
14
15
  import { showTransientError } from "@xynogen/pix-pretty/transient-error";
15
- import { canExecute } from "./capability.ts";
16
+ import { getErrorMessage } from "@xynogen/pix-pretty/utils";
17
+ import { ensureTool, type ResolvedTool, resolveTool } from "@xynogen/pix-runtime/binaries";
16
18
  import { loadOptValue, saveOptValue } from "./persist.ts";
17
19
  import type { OptimizerHandle, OptimizerStatus } from "./status.ts";
18
20
 
@@ -75,7 +77,7 @@ export function buildSudoBlockReason(sudoCmds: string[], hasSudoRunTool: boolean
75
77
 
76
78
  export function applyRtkRewrite(
77
79
  event: BashCallEvent,
78
- opts: { enabled: boolean; rtkAvailable: boolean },
80
+ opts: { enabled: boolean; rtkAvailable: boolean; rtkCommand?: string },
79
81
  ): boolean {
80
82
  if (!opts.enabled) return false;
81
83
  if (!opts.rtkAvailable) return false;
@@ -84,7 +86,7 @@ export function applyRtkRewrite(
84
86
  const command = event.input?.command;
85
87
  if (typeof command !== "string" || !command) return false;
86
88
 
87
- const rewritten = rewriteChain(command);
89
+ const rewritten = rewriteChain(command, opts.rtkCommand);
88
90
  if (rewritten === command) return false;
89
91
 
90
92
  event.input.command = rewritten;
@@ -128,12 +130,31 @@ const RTK_COMMANDS = new Set([
128
130
 
129
131
  interface RtkStatus {
130
132
  available: boolean;
133
+ /** Command prefix to inject: bare `rtk`, or the quoted path for a binary.json pin. */
134
+ command: string;
131
135
  checkedAt: number;
132
136
  }
133
137
 
134
- /** Probe the command we actually use instead of relying on a platform-specific locator. */
135
- export function probeRtkAvailability(pi: Pick<ExtensionAPI, "exec">): Promise<boolean> {
136
- return canExecute(pi, "rtk", ["--version"]);
138
+ /**
139
+ * Prefix used in rewritten commands. The bash tool puts `<agentDir>/bin` on
140
+ * PATH and PATH hits resolve by name, so those stay a readable bare `rtk`;
141
+ * only a user-chosen path (binary.json) must be spelled out, single-quoted
142
+ * with forward slashes so Git Bash on Windows runs it too.
143
+ */
144
+ export function rtkCommandFor(tool: Pick<ResolvedTool, "path" | "source">): string {
145
+ if (tool.source !== "user") return "rtk";
146
+ const posix = tool.path.replaceAll("\\", "/");
147
+ return `'${posix.replaceAll("'", "'\\''")}'`;
148
+ }
149
+
150
+ /** Locate rtk (binary.json → agent bin → PATH) without running anything. */
151
+ export function probeRtk(): RtkStatus {
152
+ const tool = resolveTool("rtk");
153
+ return {
154
+ available: tool !== undefined,
155
+ command: tool ? rtkCommandFor(tool) : "rtk",
156
+ checkedAt: Date.now(),
157
+ };
137
158
  }
138
159
 
139
160
  /**
@@ -195,7 +216,7 @@ const CHAIN_OPERATORS = new Set(["&&", "||", ";", "|"]);
195
216
  * RTK command and it is not already prefixed. Operators are preserved.
196
217
  * Returns the rewritten command, or the original if nothing changed.
197
218
  */
198
- export function rewriteChain(command: string): string {
219
+ export function rewriteChain(command: string, rtkCommand = "rtk"): string {
199
220
  const parts = splitChain(command);
200
221
  if (!parts) return command; // unparseable — leave untouched
201
222
 
@@ -208,11 +229,11 @@ export function rewriteChain(command: string): string {
208
229
  if (!body) return part;
209
230
 
210
231
  const firstWord = body.split(/\s+/)[0] ?? "";
211
- if (firstWord === "rtk") return part;
232
+ if (firstWord === "rtk" || firstWord === rtkCommand) return part;
212
233
  if (!RTK_COMMANDS.has(firstWord)) return part;
213
234
 
214
235
  changed = true;
215
- return `${leading}rtk ${body}`;
236
+ return `${leading}${rtkCommand} ${body}`;
216
237
  });
217
238
 
218
239
  return changed ? rewritten.join("") : command;
@@ -239,15 +260,33 @@ export function rtk(pi: ExtensionAPI, status: OptimizerStatus): OptimizerHandle
239
260
  return rtkStatus;
240
261
  }
241
262
 
242
- const available = await probeRtkAvailability(pi);
243
- rtkStatus = {
244
- available,
245
- checkedAt: Date.now(),
246
- };
247
- if (available) warnedMissing = false;
263
+ rtkStatus = probeRtk();
264
+ if (rtkStatus.available) warnedMissing = false;
248
265
  return rtkStatus;
249
266
  };
250
267
 
268
+ // First load without rtk: download it once, visibly (footer status + one
269
+ // transient line naming repo and version). Never blocks startup or a tool
270
+ // call; PI_OFFLINE, a broken binary.json path, or RTK off skip it.
271
+ let installing: Promise<void> | undefined;
272
+ const installRtk = (ctx: Pick<ExtensionContext, "ui">) => {
273
+ installing ??= ensureTool("rtk", { onStatus: reportToolStatus(ctx.ui) })
274
+ .then(() => {
275
+ rtkStatus = null;
276
+ })
277
+ .catch((error: unknown) => {
278
+ if (warnedMissing) return;
279
+ warnedMissing = true;
280
+ const detail = getErrorMessage(error);
281
+ ctx.ui.notify(`RTK rewriting disabled: ${detail}`, "warning");
282
+ })
283
+ .then(async () => {
284
+ await checkRtkAvailability();
285
+ syncStatus(ctx);
286
+ });
287
+ return installing;
288
+ };
289
+
251
290
  // Detect sudo_run tool availability. No system-prompt injection — the
252
291
  // tool_call rewrite hook adds the rtk prefix on its own, so we keep zero
253
292
  // always-on context cost (ponytail: manual `rtk err`/`rtk summary`/`rtk
@@ -266,14 +305,8 @@ export function rtk(pi: ExtensionAPI, status: OptimizerStatus): OptimizerHandle
266
305
  const saved = loadOptValue("rtk");
267
306
  if (saved === "on" || saved === "off") enabled = saved === "on";
268
307
  const probe = await checkRtkAvailability();
269
- if (!probe.available && !warnedMissing) {
270
- ctx.ui.notify(
271
- "rtk not found — RTK rewriting disabled. Install: cargo install rtk-ai",
272
- "warning",
273
- );
274
- warnedMissing = true;
275
- }
276
308
  syncStatus(ctx);
309
+ if (!probe.available && enabled) void installRtk(ctx);
277
310
  });
278
311
  pi.on("agent_start", async (_event, ctx) => {
279
312
  syncStatus(ctx);
@@ -287,15 +320,16 @@ export function rtk(pi: ExtensionAPI, status: OptimizerStatus): OptimizerHandle
287
320
  async function run(value: string, ctx: ExtensionCommandContext): Promise<void> {
288
321
  enabled = value === "on";
289
322
  try {
290
- saveOptValue("rtk", enabled ? "on" : "off");
323
+ await saveOptValue("rtk", enabled ? "on" : "off");
291
324
  } catch (error) {
292
- const detail = error instanceof Error ? error.message : String(error);
325
+ const detail = getErrorMessage(error);
293
326
  showTransientError(ctx.ui, `optimizer: failed to save rtk: ${detail}`);
294
327
  }
295
328
 
296
- await checkRtkAvailability();
329
+ const probe = await checkRtkAvailability();
297
330
  syncStatus(ctx);
298
331
  ctx.ui.notify(`RTK rewriting ${enabled ? "on" : "off"}.`, "info");
332
+ if (enabled && !probe.available) void installRtk(ctx);
299
333
  }
300
334
 
301
335
  // Rewrite bash commands to add rtk prefix.
@@ -339,7 +373,7 @@ export function rtk(pi: ExtensionAPI, status: OptimizerStatus): OptimizerHandle
339
373
  // Rewrite every segment in the command chain that uses a known RTK
340
374
  // command (e.g. `git add . && git push` -> `rtk git add . && rtk git push`).
341
375
  // Mutates `event.input.command` in place — the SDK's supported patch path.
342
- applyRtkRewrite(event, { enabled, rtkAvailable: probe.available });
376
+ applyRtkRewrite(event, { enabled, rtkAvailable: probe.available, rtkCommand: probe.command });
343
377
  return undefined;
344
378
  });
345
379
 
package/src/capability.ts DELETED
@@ -1,16 +0,0 @@
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
- }