@xynogen/pix-optimizer 1.1.19 → 1.1.22
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 +14 -35
- package/package.json +2 -8
- package/src/index.ts +2 -5
- package/src/opt.ts +43 -19
- package/src/persist.ts +12 -3
- package/src/status.ts +13 -18
- package/src/json.ts +0 -284
package/README.md
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
# pix-optimizer
|
|
2
2
|
|
|
3
|
-
Token-optimization suite for Pi Coding Agent.
|
|
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/
|
|
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
|
|
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` | `♤ ♡
|
|
41
|
-
| `ascii` | `Cv Rk
|
|
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
|
|
48
|
-
|
|
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
|
|
119
|
-
dependency
|
|
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
|
|
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
|
|
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.
|
|
4
|
-
"description": "Performance optimization suite for Pi Coding Agent - caveman mode + RTK tool rewriting +
|
|
3
|
+
"version": "1.1.22",
|
|
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
|
],
|
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
|
-
*
|
|
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
|
|
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 /
|
|
6
|
-
*
|
|
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
|
|
@@ -15,8 +15,13 @@
|
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
18
|
-
import { matchesKey } from "@earendil-works/pi-tui";
|
|
19
|
-
import {
|
|
18
|
+
import { type KeybindingsManager, matchesKey } from "@earendil-works/pi-tui";
|
|
19
|
+
import {
|
|
20
|
+
frameModal,
|
|
21
|
+
MIN_MODAL_HEIGHT,
|
|
22
|
+
ModalPager,
|
|
23
|
+
terminalModalHeight,
|
|
24
|
+
} from "@xynogen/pix-pretty/modal-frame";
|
|
20
25
|
import type { OptimizerHandle, OptimizerStatus, OptimizerTool } from "./status.ts";
|
|
21
26
|
import { toolIcon } from "./status.ts";
|
|
22
27
|
|
|
@@ -25,7 +30,7 @@ const MIN_CONTENT = 28;
|
|
|
25
30
|
const MAX_CONTENT = 60;
|
|
26
31
|
|
|
27
32
|
/** Fixed render order — matches the status-bar cell. */
|
|
28
|
-
const TOOL_ORDER: readonly OptimizerTool[] = ["caveman", "rtk", "
|
|
33
|
+
const TOOL_ORDER: readonly OptimizerTool[] = ["caveman", "rtk", "ponytail"];
|
|
29
34
|
|
|
30
35
|
/** Strip the leading "name [args] — " prefix from a handle's help string. */
|
|
31
36
|
function helpSummary(help: string): string {
|
|
@@ -69,7 +74,7 @@ export function registerOptCommand(
|
|
|
69
74
|
_status: OptimizerStatus,
|
|
70
75
|
): void {
|
|
71
76
|
pi.registerCommand("optimizer", {
|
|
72
|
-
description: "pix-optimizer: caveman / rtk /
|
|
77
|
+
description: "pix-optimizer: caveman / rtk / ponytail tools",
|
|
73
78
|
handler: async (_args, ctx) => {
|
|
74
79
|
const ui = ctx.ui as unknown as {
|
|
75
80
|
theme: {
|
|
@@ -114,12 +119,16 @@ export function registerOptCommand(
|
|
|
114
119
|
|
|
115
120
|
await ui.custom<null>(
|
|
116
121
|
(
|
|
117
|
-
tui: { requestRender(): void },
|
|
122
|
+
tui: { requestRender(): void; terminal?: { rows?: number } },
|
|
118
123
|
theme: typeof ui.theme,
|
|
119
|
-
|
|
124
|
+
kb: KeybindingsManager,
|
|
120
125
|
done: (v: null) => void,
|
|
121
126
|
) => {
|
|
127
|
+
const guide = (key: string, action: string) =>
|
|
128
|
+
theme.fg("text", key) + theme.fg("dim", ` ${action}`);
|
|
129
|
+
const guideSep = theme.fg("dim", " · ");
|
|
122
130
|
let selected = 0;
|
|
131
|
+
const pager = new ModalPager();
|
|
123
132
|
|
|
124
133
|
const cycle = (direction: -1 | 1) => {
|
|
125
134
|
const tool = TOOL_ORDER[selected] as OptimizerTool;
|
|
@@ -131,6 +140,7 @@ export function registerOptCommand(
|
|
|
131
140
|
|
|
132
141
|
const move = (direction: -1 | 1) => {
|
|
133
142
|
selected = (selected + direction + TOOL_ORDER.length) % TOOL_ORDER.length;
|
|
143
|
+
pager.followSelection();
|
|
134
144
|
};
|
|
135
145
|
|
|
136
146
|
return {
|
|
@@ -149,24 +159,38 @@ export function registerOptCommand(
|
|
|
149
159
|
const bar = theme.fg(on ? "accent" : "dim", levelBar(cur, handles[tool].values));
|
|
150
160
|
return `${cursor} ${glyph} ${name} ${value} ${bar}`;
|
|
151
161
|
});
|
|
152
|
-
const
|
|
153
|
-
theme.fg("accent", theme.bold(" Optimizer")),
|
|
154
|
-
"",
|
|
155
|
-
...rows,
|
|
156
|
-
"",
|
|
157
|
-
theme.fg("dim", helpSummary(handles[TOOL_ORDER[selected] as OptimizerTool].help)),
|
|
158
|
-
"",
|
|
159
|
-
theme.fg("dim", "←→ change · ↑↓ move · esc close"),
|
|
160
|
-
];
|
|
161
|
-
return frameLines({
|
|
162
|
+
const result = frameModal({
|
|
162
163
|
width: boxW,
|
|
163
|
-
|
|
164
|
+
maxHeight: terminalModalHeight(tui.terminal?.rows),
|
|
165
|
+
minHeight: MIN_MODAL_HEIGHT,
|
|
166
|
+
header: [theme.fg("accent", theme.bold(" Optimizer")), ""],
|
|
167
|
+
body: rows,
|
|
168
|
+
footer: [
|
|
169
|
+
"",
|
|
170
|
+
theme.fg("dim", helpSummary(handles[TOOL_ORDER[selected] as OptimizerTool].help)),
|
|
171
|
+
"",
|
|
172
|
+
guide("←→", "change") +
|
|
173
|
+
guideSep +
|
|
174
|
+
guide("↑↓", "move") +
|
|
175
|
+
guideSep +
|
|
176
|
+
guide("PgUp/PgDn", "inspect") +
|
|
177
|
+
guideSep +
|
|
178
|
+
guide("esc", "close"),
|
|
179
|
+
],
|
|
180
|
+
bodyOffset: pager.bodyOffset,
|
|
181
|
+
selectedBodyLine: pager.selectedLine(selected),
|
|
164
182
|
color: (s) => theme.fg("accent", s),
|
|
165
183
|
bg: (s) => theme.bg("customMessageBg", s),
|
|
166
184
|
});
|
|
185
|
+
pager.sync(result);
|
|
186
|
+
return result.lines;
|
|
167
187
|
},
|
|
168
188
|
invalidate: () => {},
|
|
169
189
|
handleInput: (data: string) => {
|
|
190
|
+
if (pager.handleInput(data, kb)) {
|
|
191
|
+
tui.requestRender();
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
170
194
|
// matchesKey handles both legacy bytes and Kitty CSI-u encodings
|
|
171
195
|
// for letters and special keys alike — raw string compares like
|
|
172
196
|
// `data === "k"` silently fail under the Kitty keyboard protocol.
|
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
|
|
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
|
|
28
|
-
|
|
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 /
|
|
5
|
-
* class of thing (token-optimization tools), so they share ONE status cell
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
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/
|
|
24
|
-
*
|
|
25
|
-
*
|
|
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" / "
|
|
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" | "
|
|
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", "
|
|
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
|
|
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/
|
|
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
|
-
}
|