@xynogen/pix-optimizer 1.1.20 → 1.1.24

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
@@ -1,14 +1,16 @@
1
1
  # pix-optimizer
2
2
 
3
- Token-optimization suite for Pi Coding Agent. Four tools wired into one
3
+ Token-optimization suite for Pi Coding Agent. Three tools wired into one
4
4
  extension via `src/index.ts`, fronted by a single `/optimizer` command and one
5
5
  shared status-bar cell:
6
6
 
7
7
  - **Caveman** (`Cv`) — terse-output system prompt
8
8
  - **RTK** (`Rk`) — prefixes shell commands with `rtk` + injects RTK prompt
9
- - **TOON** (`Tn`) — jq + TOON guidance for dense JSON (skill lives in pix-skills)
10
9
  - **Ponytail** (`Pt`) — lazy-senior-dev system prompt (minimal code, YAGNI)
11
10
 
11
+ TOON guidance lives exclusively in the on-demand `toon-json` skill bundled by
12
+ `@xynogen/pix-skills`; it is no longer optimizer state or prompt injection.
13
+
12
14
  ## Command
13
15
 
14
16
  One command opens an interactive overlay that fronts every tool:
@@ -21,11 +23,11 @@ There is no text-arg form: the overlay is the only UI. Selecting a value calls
21
23
  the tool's `run()` handler, which persists the new value and repaints the
22
24
  shared status cell. Headless/test fallbacks print a plain status summary.
23
25
 
24
- State persists to `~/.pi/agent/optimizer.json` (caveman/rtk/toon/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 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.
25
27
 
26
28
  ## Status bar
27
29
 
28
- A single cell always shows all four tool icons in a fixed order, color-coded by
30
+ A single cell always shows all three tool icons in a fixed order, color-coded by
29
31
  state: **accent** when the tool is enabled, **dim** when disabled.
30
32
 
31
33
  ### Icon style (Nerd Font, Unicode, or ASCII)
@@ -37,15 +39,11 @@ font (e.g. MesloLGS NF). Terminals without one render them as missing-glyph
37
39
  | Mode | Glyphs | Needs Nerd Font? |
38
40
  |---|---|---|
39
41
  | `nerd` (default) | Nerd Font PUA glyphs | yes |
40
- | `unicode` | `♤ ♡ ♢ ♧` (outline card suits) | no |
41
- | `ascii` | `Cv Rk Tn Pt` | no |
42
-
43
- Switch the style live from the **`/optimizer` overlay** — a fifth `icons` row
44
- at the bottom cycles `nerd → unicode → ascii` with `←→`; the choice persists to
45
- `~/.pi/agent/optimizer.json`.
42
+ | `unicode` | `♤ ♡ ♧` (outline card suits) | no |
43
+ | `ascii` | `Cv Rk Pt` | no |
46
44
 
47
- Icons follow the **global** `pix-pretty` mode — set via `/pix` or `PRETTY_ICONS`
48
- env var. The optimizer no longer has its own toggle.
45
+ Icons follow the **global** `pix-pretty` mode. Set it via `/pix` or the
46
+ `PRETTY_ICONS` environment variable; the optimizer has no separate icon toggle.
49
47
 
50
48
  ## Features
51
49
 
@@ -84,23 +82,6 @@ Two layers, both active automatically:
84
82
  cargo install rtk-ai
85
83
  ```
86
84
 
87
- ### TOON / JSON Compression (`Tn`)
88
-
89
- Guidance for handling information-dense JSON via `jq` (query/reshape) and
90
- `toon` (compress). The system-prompt nudge is injected **only when the user
91
- prompt mentions JSON** (`json`/`jsonl`/`jq`/`toon`/`openapi`/…). TOON shines
92
- on uniform/tabular arrays; deeply nested or array-of-arrays data and API
93
- contracts stay as JSON.
94
-
95
- The `toon-json` skill (full workflow + when-NOT-to-use guidance) is bundled in
96
- `pix-skills` and auto-discovered from there.
97
-
98
- **Requirement:** `jq` and `toon` on `PATH`.
99
-
100
- ```bash
101
- npm i -g @toon-format/cli
102
- ```
103
-
104
85
  ### Ponytail Mode (`Pt`)
105
86
 
106
87
  "Lazy senior dev" mode. Governs **what** the agent builds (minimal code,
@@ -115,8 +96,8 @@ works. Validation, error handling, security, and accessibility are never cut.
115
96
  | full | The ladder enforced (default) |
116
97
  | ultra | YAGNI extremist |
117
98
 
118
- **No install required** — pure prompt injection, no external binary or PATH
119
- dependency (unlike RTK and TOON).
99
+ **No install required** — pure prompt injection, with no external binary or
100
+ PATH dependency.
120
101
 
121
102
  ## Configuration via `pix.json`
122
103
 
@@ -127,7 +108,6 @@ Set the initial optimizer state for new sessions in `~/.pi/agent/pix.json`. Thes
127
108
  "optimizer": {
128
109
  "caveman": "lite", // off | lite | full | ultra | micro
129
110
  "rtk": true,
130
- "toon": false,
131
111
  "ponytail": "off" // off | lite | full | ultra
132
112
  }
133
113
  }
@@ -149,18 +129,17 @@ pi install npm:@xynogen/pix-optimizer
149
129
 
150
130
  | File | Role |
151
131
  |-------------------|-----------------------------------------------------------|
152
- | `src/index.ts` | Wires the four tools + shared status, registers `/optimizer` |
132
+ | `src/index.ts` | Wires the three tools + shared status, registers `/optimizer` |
153
133
  | `src/opt.ts` | The `/optimizer` overlay UI (keyboard nav + cycling) |
154
134
  | `src/status.ts` | Shared status-bar cell (`toolIcon()` → shared `pix-pretty` catalog) |
155
135
  | `src/caveman.ts` | Caveman logic, levels, prompt |
156
136
  | `src/rtk.ts` | RTK prompt + bash command rewriting |
157
- | `src/json.ts` | jq+TOON guidance, heuristics, system-prompt injection |
158
137
  | `src/ponytail.ts` | Ponytail logic, levels, prompt |
159
138
  | `src/persist.ts` | Disk-backed `~/.pi/agent/optimizer.json` persistence; seeds initial state from `pix.json` |
160
139
  | `src/tool-result-filter.ts` | Strips model-guidance warnings from tool_result |
161
140
 
162
141
  Each tool registers its own lifecycle hooks and exposes an `OptimizerHandle`
163
- that `/optimizer` dispatches to. All four share one `OptimizerStatus`.
142
+ that `/optimizer` dispatches to. All three share one `OptimizerStatus`.
164
143
 
165
144
  ## Development
166
145
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@xynogen/pix-optimizer",
3
- "version": "1.1.20",
4
- "description": "Performance optimization suite for Pi Coding Agent - caveman mode + RTK tool rewriting + jq/TOON JSON compression + ponytail lazy-dev mode",
3
+ "version": "1.1.24",
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",
7
7
  "scripts": {
@@ -16,9 +16,6 @@
16
16
  "pi": {
17
17
  "extensions": [
18
18
  "./src/index.ts"
19
- ],
20
- "skills": [
21
- "./src/skills"
22
19
  ]
23
20
  },
24
21
  "keywords": [
@@ -28,9 +25,6 @@
28
25
  "optimizer",
29
26
  "caveman",
30
27
  "rtk",
31
- "toon",
32
- "jq",
33
- "json",
34
28
  "ponytail",
35
29
  "performance"
36
30
  ],
@@ -49,7 +43,7 @@
49
43
  "access": "public"
50
44
  },
51
45
  "dependencies": {
52
- "@xynogen/pix-pretty": "^1.7.22"
46
+ "@xynogen/pix-pretty": "^1.11.2"
53
47
  },
54
48
  "peerDependencies": {
55
49
  "@earendil-works/pi-coding-agent": "*",
package/src/index.ts CHANGED
@@ -1,13 +1,12 @@
1
1
  /**
2
2
  * pix-optimizer — token-optimization suite for Pi Coding Agent.
3
3
  *
4
- * Four tools, combined into one extension + one command:
4
+ * Three tools, combined into one extension + one command:
5
5
  * - caveman: terse-output system prompt
6
6
  * - rtk: prefixes shell commands with `rtk` + injects RTK prompt
7
- * - toon: jq + TOON guidance for dense JSON (+ bundled skill)
8
7
  * - ponytail: lazy-senior-dev system prompt (minimal code, YAGNI)
9
8
  *
10
- * They share ONE status-bar cell (all four icons always shown — dimmed when
9
+ * They share ONE status-bar cell (all three icons always shown — dimmed when
11
10
  * off, accented when on; icon style is nerd/unicode/ascii, set via /optimizer)
12
11
  * and ONE command (/optimizer — an interactive overlay). index.ts wires
13
12
  * lifecycle hooks via each module, then registers the overlay command.
@@ -15,7 +14,6 @@
15
14
 
16
15
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
17
16
  import { caveman } from "./caveman.ts";
18
- import { json } from "./json.ts";
19
17
  import { registerOptCommand } from "./opt.ts";
20
18
  import { ponytail } from "./ponytail.ts";
21
19
  import { rtk } from "./rtk.ts";
@@ -31,7 +29,6 @@ export default function optimizer(pi: ExtensionAPI) {
31
29
  const handles: Record<OptimizerTool, OptimizerHandle> = {
32
30
  caveman: caveman(pi, status),
33
31
  rtk: rtk(pi, status),
34
- toon: json(pi, status),
35
32
  ponytail: ponytail(pi, status),
36
33
  };
37
34
 
package/src/opt.ts CHANGED
@@ -2,8 +2,8 @@
2
2
  * opt.ts — the single `/optimizer` command: an interactive overlay that fronts
3
3
  * every optimizer tool.
4
4
  *
5
- * caveman / rtk / toon / ponytail each register their own lifecycle hooks but
6
- * expose an OptimizerHandle (name · values · current() · run()). This overlay
5
+ * caveman / rtk / ponytail each register their own lifecycle hooks but expose
6
+ * an OptimizerHandle (name · values · current() · run()). This overlay
7
7
  * renders one SettingsList row per tool:
8
8
  *
9
9
  * ↑↓ move between tools
@@ -30,7 +30,7 @@ const MIN_CONTENT = 28;
30
30
  const MAX_CONTENT = 60;
31
31
 
32
32
  /** Fixed render order — matches the status-bar cell. */
33
- const TOOL_ORDER: readonly OptimizerTool[] = ["caveman", "rtk", "toon", "ponytail"];
33
+ const TOOL_ORDER: readonly OptimizerTool[] = ["caveman", "rtk", "ponytail"];
34
34
 
35
35
  /** Strip the leading "name [args] — " prefix from a handle's help string. */
36
36
  function helpSummary(help: string): string {
@@ -74,7 +74,7 @@ export function registerOptCommand(
74
74
  _status: OptimizerStatus,
75
75
  ): void {
76
76
  pi.registerCommand("optimizer", {
77
- description: "pix-optimizer: caveman / rtk / toon / ponytail tools",
77
+ description: "pix-optimizer: caveman / rtk / ponytail tools",
78
78
  handler: async (_args, ctx) => {
79
79
  const ui = ctx.ui as unknown as {
80
80
  theme: {
@@ -124,6 +124,9 @@ export function registerOptCommand(
124
124
  kb: KeybindingsManager,
125
125
  done: (v: null) => void,
126
126
  ) => {
127
+ const guide = (key: string, action: string) =>
128
+ theme.fg("text", key) + theme.fg("dim", ` ${action}`);
129
+ const guideSep = theme.fg("dim", " · ");
127
130
  let selected = 0;
128
131
  const pager = new ModalPager();
129
132
 
@@ -166,7 +169,13 @@ export function registerOptCommand(
166
169
  "",
167
170
  theme.fg("dim", helpSummary(handles[TOOL_ORDER[selected] as OptimizerTool].help)),
168
171
  "",
169
- theme.fg("dim", "←→ change · ↑↓ move · PgUp/PgDn inspect · esc close"),
172
+ guide("←→", "change") +
173
+ guideSep +
174
+ guide("↑↓", "move") +
175
+ guideSep +
176
+ guide("PgUp/PgDn", "inspect") +
177
+ guideSep +
178
+ guide("esc", "close"),
170
179
  ],
171
180
  bodyOffset: pager.bodyOffset,
172
181
  selectedBodyLine: pager.selectedLine(selected),
package/src/persist.ts CHANGED
@@ -2,7 +2,7 @@
2
2
  * persist.ts — disk-backed persistence for the /optimizer tool states.
3
3
  *
4
4
  * caveman/ponytail previously saved only to the session log (lost on a fresh
5
- * session); rtk/toon never persisted at all. This stores every tool's current
5
+ * session); rtk never persisted at all. This stores every tool's current
6
6
  * value in one file under the agent dir so the picker survives a full quit and
7
7
  * restart. Each tool reads its value on session_start and writes on run().
8
8
  *
@@ -16,6 +16,8 @@ import type { OptimizerTool } from "./status.ts";
16
16
 
17
17
  type OptimizerFileConfig = Partial<Record<OptimizerTool, string>>;
18
18
 
19
+ const OPTIMIZER_TOOLS: readonly OptimizerTool[] = ["caveman", "rtk", "ponytail"];
20
+
19
21
  function getStatePath(): string {
20
22
  return join(getAgentDir(), "optimizer.json");
21
23
  }
@@ -24,8 +26,15 @@ function readFile(): OptimizerFileConfig {
24
26
  try {
25
27
  const sp = getStatePath();
26
28
  if (!existsSync(sp)) return {};
27
- const raw = JSON.parse(readFileSync(sp, "utf-8")) as OptimizerFileConfig;
28
- return raw && typeof raw === "object" ? raw : {};
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;
29
38
  } catch {
30
39
  return {};
31
40
  }
package/src/status.ts CHANGED
@@ -1,15 +1,11 @@
1
1
  /**
2
2
  * status.ts — shared status-bar indicator for the optimizer suite.
3
3
  *
4
- * caveman / rtk / toon / ponytail each toggle independently, but they're all the same
5
- * class of thing (token-optimization tools), so they share ONE status cell
6
- * instead of three. Each tool reports its on/off state into a single registry;
7
- * the cell renders ALL four icons in a fixed order — accent-colored when the
8
- * tool is enabled, dim when disabled. The cell is never empty.
9
- *
10
- * all on: all four accent
11
- * caveman off: caveman dim, rest accent
12
- * all off: all four dim
4
+ * caveman / rtk / ponytail each toggle independently, but they're all the same
5
+ * class of thing (token-optimization tools), so they share ONE status cell.
6
+ * Each tool reports its on/off state into a single registry; the cell renders
7
+ * all three icons in a fixed order — accent-colored when enabled, dim when
8
+ * disabled. The cell is never empty.
13
9
  */
14
10
 
15
11
  import type {
@@ -20,12 +16,12 @@ import type {
20
16
  import { type IconKey, icon, onIconModeChange } from "@xynogen/pix-pretty/icon-catalog";
21
17
 
22
18
  /**
23
- * Each optimizer tool (caveman/rtk/toon/ponytail) exposes a handle so the
24
- * single `/optimizer` overlay can render one row per tool and apply value
25
- * changes without knowing the tool's internals.
19
+ * Each optimizer tool (caveman/rtk/ponytail) exposes a handle so the single
20
+ * `/optimizer` overlay can render one row per tool and apply value changes
21
+ * without knowing the tool's internals.
26
22
  */
27
23
  export interface OptimizerHandle {
28
- /** Tool name, e.g. "caveman" / "rtk" / "toon". */
24
+ /** Tool name, e.g. "caveman" / "rtk" / "ponytail". */
29
25
  name: OptimizerTool;
30
26
  /** One-line summary shown in the overlay row. */
31
27
  help: string;
@@ -41,7 +37,7 @@ export interface OptimizerHandle {
41
37
  export const STATUS_KEY = "pix-optimizer";
42
38
 
43
39
  /** Tools that participate in the shared indicator, in render order. */
44
- export type OptimizerTool = "caveman" | "rtk" | "toon" | "ponytail";
40
+ export type OptimizerTool = "caveman" | "rtk" | "ponytail";
45
41
 
46
42
  /**
47
43
  * Icons come from the shared pix-pretty catalog and follow the ONE global icon
@@ -52,7 +48,6 @@ export type OptimizerTool = "caveman" | "rtk" | "toon" | "ponytail";
52
48
  const TOOL_ICON_KEY: Record<OptimizerTool, IconKey> = {
53
49
  caveman: "opt.caveman",
54
50
  rtk: "opt.rtk",
55
- toon: "opt.toon",
56
51
  ponytail: "opt.ponytail",
57
52
  };
58
53
 
@@ -62,7 +57,7 @@ export function toolIcon(tool: OptimizerTool): string {
62
57
  }
63
58
 
64
59
  /** Fixed left-to-right order of icons in the cell. */
65
- const TOOL_ORDER: readonly OptimizerTool[] = ["caveman", "rtk", "toon", "ponytail"];
60
+ const TOOL_ORDER: readonly OptimizerTool[] = ["caveman", "rtk", "ponytail"];
66
61
 
67
62
  /** Theme color for enabled icons. */
68
63
  const ENABLED_COLOR: ThemeColor = "accent";
@@ -74,7 +69,7 @@ export type Colorize = (color: ThemeColor, text: string) => string;
74
69
 
75
70
  /**
76
71
  * Build the colored status string for a set of tool states. ALL tool icons are
77
- * always shown, in TOOL_ORDER; each is accent-colored when its tool is enabled
72
+ * always shown in TOOL_ORDER; each is accent-colored when its tool is enabled
78
73
  * and dim when disabled. Glyphs resolve against the active global icon mode
79
74
  * (see /pix). `color` applies the theme color (e.g. theme.fg).
80
75
  *
@@ -93,7 +88,7 @@ export function renderStatus(
93
88
  /**
94
89
  * Shared registry: each tool calls `set(tool, enabled)` whenever its state
95
90
  * changes, then the combined cell is re-rendered. A single registry instance
96
- * is created per extension load and passed to caveman/rtk/json.
91
+ * is created per extension load and passed to caveman/rtk/ponytail.
97
92
  */
98
93
  export class OptimizerStatus {
99
94
  private states: Partial<Record<OptimizerTool, boolean>> = {};
package/src/json.ts DELETED
@@ -1,284 +0,0 @@
1
- /**
2
- * json.ts — JSON token-optimization via jq + TOON.
3
- *
4
- * A small system-prompt nudge teaching the model to run JSON through
5
- * `jq` (query/reshape) and `toon` (compress for context), and to convert
6
- * back to JSON only when a strict contract requires it.
7
- *
8
- * The bundled `toon-json` skill lives in pix-skills and is auto-discovered
9
- * from there — no resources_discover hook needed here.
10
- *
11
- * TOON = Token-Oriented Object Notation (https://github.com/toon-format/spec).
12
- * It shines on uniform/tabular arrays of objects (declare keys once, stream
13
- * rows) and loses to compact JSON on deeply nested / non-uniform / array-of-
14
- * arrays data. The prompt encodes that boundary so the model picks correctly.
15
- *
16
- * Pure helpers are exported for tests; json(pi) is the extension entry, called
17
- * by index.ts alongside caveman(pi) and rtk(pi).
18
- */
19
-
20
- import type {
21
- ExtensionAPI,
22
- ExtensionCommandContext,
23
- ExtensionContext,
24
- } from "@earendil-works/pi-coding-agent";
25
- import { canExecute } from "./capability.ts";
26
- import { loadOptValue, saveOptValue } from "./persist.ts";
27
- import type { OptimizerHandle, OptimizerStatus } from "./status.ts";
28
-
29
- // ── System prompt ───────────────────────────────────────────────────────────
30
-
31
- export const JSON_SYSTEM_PROMPT = `# JSON Handling — jq + TOON
32
-
33
- When working with information-dense JSON (LLM/OpenAPI schemas, API responses,
34
- config dumps, datasets), prefer this pipeline over dumping raw JSON into context:
35
-
36
- \`\`\`bash
37
- curl -s <url> | jq '<query>' | toon # fetch → reshape → compress
38
- cat data.json | jq '.items' | toon --stats # local file, show token savings
39
- echo "$TOON" | toon -d # convert TOON back to JSON
40
- \`\`\`
41
-
42
- ## Why
43
- - **jq** queries/reshapes so you only carry the slice you need.
44
- - **toon** re-encodes JSON as TOON: uniform arrays of objects declare their
45
- keys once (\`key[N]{a,b,c}:\`) then stream bare rows — large token savings on
46
- tabular/dense data. \`toon\` auto-detects direction; \`-d\` decodes back.
47
-
48
- ## When TOON helps (use it)
49
- - Uniform/tabular arrays of objects (TOON's sweet spot — savings scale with rows × fields)
50
- - Flat objects and primitive arrays
51
- - Shallow nesting
52
-
53
- ## When to SKIP TOON (keep JSON)
54
- - API-level contracts / payloads you must send or store verbatim
55
- - Deeply nested or non-uniform structures (compact JSON can win)
56
- - Arrays of arrays (TOON is less efficient here)
57
- - Anything a downstream parser requires as strict JSON
58
-
59
- Rule of thumb: TOON for **reading** dense data into context; JSON for **contracts**.
60
- See the \`toon-json\` skill for the full workflow.`;
61
-
62
- // ── Prompt relevance gate ─────────────────────────────────────────────────────
63
-
64
- /**
65
- * Tokens that signal the user prompt is about JSON / the jq+TOON workflow.
66
- * Matched as whole words (case-insensitive) so "adjust" / "jsx" don't trip "js".
67
- */
68
- const JSON_TRIGGERS = [
69
- "json",
70
- "jsonl",
71
- "ndjson",
72
- "js",
73
- "jq",
74
- "toon",
75
- "openapi",
76
- "swagger",
77
- ] as const;
78
-
79
- const JSON_TRIGGER_RE = new RegExp(`\\b(${JSON_TRIGGERS.join("|")})\\b`, "i");
80
-
81
- /**
82
- * True when the user prompt mentions JSON (or a related token), so the
83
- * jq+TOON guidance is only injected when actually relevant. Case-insensitive
84
- * and word-bounded ("JSON", "Json", "json" all match; "adjust" does not).
85
- */
86
- export function mentionsJson(prompt: string | undefined | null): boolean {
87
- if (!prompt) return false;
88
- return JSON_TRIGGER_RE.test(prompt);
89
- }
90
-
91
- // ── Pure decision helper ──────────────────────────────────────────────────────
92
-
93
- export interface ToonAdvice {
94
- /** Whether TOON is likely to reduce tokens for this shape. */
95
- useToon: boolean;
96
- /** Short human-readable reason. */
97
- reason: string;
98
- }
99
-
100
- /**
101
- * Heuristic guidance on whether a parsed JSON value is a good TOON candidate.
102
- *
103
- * This is intentionally a *recommendation* surfaced to the model / user, not an
104
- * enforcement. It encodes the efficiency boundary from the TOON spec:
105
- * - uniform arrays of objects → strongly yes (tabular sweet spot)
106
- * - flat objects / primitive arrays → yes
107
- * - arrays of arrays → no (TOON's one losing case)
108
- * - deeply nested / non-uniform → no (compact JSON can win)
109
- *
110
- * @param value parsed JSON (object/array/primitive)
111
- * @param maxDepth nesting depth at which we stop recommending TOON (default 4)
112
- */
113
- export function adviseToon(value: unknown, maxDepth = 4): ToonAdvice {
114
- if (Array.isArray(value)) {
115
- if (value.length === 0) {
116
- return { useToon: false, reason: "empty array — nothing to compress" };
117
- }
118
- // Array of arrays: TOON's only structurally-worse case.
119
- if (value.every((v) => Array.isArray(v))) {
120
- return {
121
- useToon: false,
122
- reason: "array of arrays — JSON is more compact",
123
- };
124
- }
125
- // Uniform array of flat objects → tabular sweet spot.
126
- if (isUniformObjectArray(value)) {
127
- return {
128
- useToon: true,
129
- reason: `uniform array of ${value.length} objects — TOON tabular sweet spot`,
130
- };
131
- }
132
- // Array of primitives.
133
- if (value.every((v) => !isObjectLike(v))) {
134
- return {
135
- useToon: true,
136
- reason: "primitive array — TOON omits quotes/braces",
137
- };
138
- }
139
- return {
140
- useToon: false,
141
- reason: "non-uniform array — savings uncertain, keep JSON",
142
- };
143
- }
144
-
145
- if (isObjectLike(value)) {
146
- const depth = objectDepth(value);
147
- if (depth > maxDepth) {
148
- return {
149
- useToon: false,
150
- reason: `nesting depth ${depth} > ${maxDepth} — compact JSON may win`,
151
- };
152
- }
153
- return {
154
- useToon: true,
155
- reason: "shallow object — TOON drops quotes/braces",
156
- };
157
- }
158
-
159
- return { useToon: false, reason: "primitive value — nothing to compress" };
160
- }
161
-
162
- /** True for plain objects/arrays (things with nested structure). */
163
- function isObjectLike(v: unknown): v is Record<string, unknown> | unknown[] {
164
- return typeof v === "object" && v !== null;
165
- }
166
-
167
- /**
168
- * A uniform array of objects: every element is a plain (non-array) object and
169
- * they all share the same set of keys, each holding a primitive value. This is
170
- * exactly the shape TOON encodes as a single header + bare rows.
171
- */
172
- export function isUniformObjectArray(arr: unknown[]): boolean {
173
- if (arr.length === 0) return false;
174
- const first = arr[0];
175
- if (!isPlainObject(first)) return false;
176
- const keys = Object.keys(first).sort();
177
- if (keys.length === 0) return false;
178
-
179
- return arr.every((el) => {
180
- if (!isPlainObject(el)) return false;
181
- const elKeys = Object.keys(el).sort();
182
- if (elKeys.length !== keys.length) return false;
183
- for (let i = 0; i < keys.length; i++) {
184
- if (elKeys[i] !== keys[i]) return false;
185
- // values must be primitive for a clean tabular row
186
- if (isObjectLike((el as Record<string, unknown>)[keys[i] as string])) return false;
187
- }
188
- return true;
189
- });
190
- }
191
-
192
- function isPlainObject(v: unknown): v is Record<string, unknown> {
193
- return typeof v === "object" && v !== null && !Array.isArray(v);
194
- }
195
-
196
- /** Max nesting depth of a JSON value (primitives = 0). */
197
- export function objectDepth(value: unknown): number {
198
- if (!isObjectLike(value)) return 0;
199
- const children = Array.isArray(value) ? value : Object.values(value);
200
- let max = 0;
201
- for (const child of children) {
202
- const d = objectDepth(child);
203
- if (d > max) max = d;
204
- }
205
- return max + 1;
206
- }
207
-
208
- // ── Bundled skill path ────────────────────────────────────────────────────────
209
-
210
- // ── Pi extension ──────────────────────────────────────────────────────────────
211
-
212
- export function json(pi: ExtensionAPI, status: OptimizerStatus): OptimizerHandle {
213
- let enabled = true;
214
- let jqAvailable: boolean | null = null;
215
- let toonAvailable: boolean | null = null;
216
-
217
- // Report into the shared optimizer indicator.
218
- function syncStatus(ctx: Pick<ExtensionContext, "ui">) {
219
- status.set("toon", enabled && jqAvailable !== false && toonAvailable !== false, ctx);
220
- }
221
-
222
- pi.on("session_start", async (_event, ctx) => {
223
- // Restore the user's on/off choice from disk (survives quit/restart).
224
- const saved = loadOptValue("toon");
225
- if (saved === "on" || saved === "off") enabled = saved === "on";
226
- syncStatus(ctx);
227
- });
228
- pi.on("agent_start", async (_event, ctx) => {
229
- syncStatus(ctx);
230
- });
231
- pi.on("agent_end", async (_event, ctx) => {
232
- syncStatus(ctx);
233
- });
234
-
235
- // Inject the JSON-handling nudge into the system prompt, but ONLY when the
236
- // user prompt actually mentions JSON / a related token — otherwise it's dead
237
- // weight in every turn. Probe jq/toon lazily here (not at session_start) so
238
- // we don't add startup overhead for a workflow the user may never trigger.
239
- pi.on("before_agent_start", async (event, ctx) => {
240
- if (!enabled) return undefined;
241
- if (!mentionsJson(event.prompt)) return undefined;
242
-
243
- // Probe once on first JSON-relevant prompt.
244
- if (jqAvailable === null || toonAvailable === null) {
245
- [jqAvailable, toonAvailable] = await Promise.all([
246
- canExecute(pi, "jq", ["--version"]),
247
- canExecute(pi, "toon", ["--version"]),
248
- ]);
249
- if (!jqAvailable)
250
- ctx.ui.notify(
251
- "jq not found — JSON/TOON guidance disabled. Install: sudo apt install jq or brew install jq",
252
- "warning",
253
- );
254
- if (!toonAvailable)
255
- ctx.ui.notify(
256
- "toon not found — JSON/TOON guidance disabled. Install: bun add -g @toon-format/cli or npm i -g @toon-format/cli",
257
- "warning",
258
- );
259
- syncStatus(ctx);
260
- }
261
-
262
- if (jqAvailable === false || toonAvailable === false) return undefined;
263
- const existing = event.systemPrompt ?? "";
264
- return { systemPrompt: `${JSON_SYSTEM_PROMPT}\n\n${existing}` };
265
- });
266
-
267
- // -- Overlay value handler (called by the /optimizer overlay) --
268
-
269
- async function run(value: string, ctx: ExtensionCommandContext): Promise<void> {
270
- enabled = value === "on";
271
- saveOptValue("toon", enabled ? "on" : "off");
272
-
273
- syncStatus(ctx);
274
- ctx.ui.notify(`JSON/TOON guidance ${enabled ? "on" : "off"}.`, "info");
275
- }
276
-
277
- return {
278
- name: "toon",
279
- help: "toon — jq+TOON guidance for dense JSON",
280
- values: ["off", "on"],
281
- current: () => (enabled ? "on" : "off"),
282
- run,
283
- };
284
- }