@xynogen/pix-optimizer 1.0.6 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +48 -17
- package/package.json +3 -2
- package/src/index.ts +7 -4
- package/src/json.test.ts +0 -8
- package/src/json.ts +6 -23
- package/src/opt.test.ts +13 -2
- package/src/opt.ts +1 -1
- package/src/ponytail.test.ts +236 -0
- package/src/ponytail.ts +439 -0
- package/src/status.test.ts +20 -10
- package/src/status.ts +13 -7
- package/src/skills/toon-json/SKILL.md +0 -101
package/README.md
CHANGED
|
@@ -4,9 +4,10 @@ Token-optimization suite for Pi Coding Agent. Three tools wired into one
|
|
|
4
4
|
extension via `src/index.ts`, fronted by a single `/opt` command and one
|
|
5
5
|
shared status-bar cell:
|
|
6
6
|
|
|
7
|
-
- **Caveman** (
|
|
8
|
-
- **RTK** (
|
|
9
|
-
- **TOON** (
|
|
7
|
+
- **Caveman** (``) — terse-output system prompt
|
|
8
|
+
- **RTK** (``) — prefixes shell commands with `rtk` + injects RTK prompt
|
|
9
|
+
- **TOON** (``) — jq + TOON guidance for dense JSON (skill lives in pix-skills)
|
|
10
|
+
- **Ponytail** (``) — lazy-senior-dev system prompt (minimal code, YAGNI)
|
|
10
11
|
|
|
11
12
|
## Command
|
|
12
13
|
|
|
@@ -17,16 +18,17 @@ One command routes to every tool:
|
|
|
17
18
|
/opt caveman <level> → set caveman level (1/2/3/lite/full/ultra/micro/off/config)
|
|
18
19
|
/opt rtk [on|off] → toggle RTK rewriting
|
|
19
20
|
/opt toon [on|off] → toggle jq+TOON guidance
|
|
21
|
+
/opt ponytail <level> → set ponytail level (1/2/3/lite/full/ultra/off/config)
|
|
20
22
|
```
|
|
21
23
|
|
|
22
24
|
## Status bar
|
|
23
25
|
|
|
24
|
-
A single cell always shows
|
|
26
|
+
A single cell always shows the enabled icons in a fixed order (` `), color-
|
|
25
27
|
coded by state: **accent** when the tool is enabled, **dim** when disabled.
|
|
26
28
|
|
|
27
29
|
## Features
|
|
28
30
|
|
|
29
|
-
### Caveman Mode (
|
|
31
|
+
### Caveman Mode (``)
|
|
30
32
|
|
|
31
33
|
Cuts ~75% of output tokens while keeping full technical accuracy.
|
|
32
34
|
|
|
@@ -40,7 +42,7 @@ Cuts ~75% of output tokens while keeping full technical accuracy.
|
|
|
40
42
|
`/opt caveman config` opens a settings dialog. Default level for new sessions
|
|
41
43
|
and status-bar visibility are saved to `~/.pi/agent/caveman.json`.
|
|
42
44
|
|
|
43
|
-
### RTK Tool Rewriting (
|
|
45
|
+
### RTK Tool Rewriting (``)
|
|
44
46
|
|
|
45
47
|
Two layers, both active automatically:
|
|
46
48
|
|
|
@@ -61,13 +63,16 @@ Two layers, both active automatically:
|
|
|
61
63
|
cargo install rtk-ai
|
|
62
64
|
```
|
|
63
65
|
|
|
64
|
-
### TOON / JSON Compression (
|
|
66
|
+
### TOON / JSON Compression (``)
|
|
65
67
|
|
|
66
|
-
Guidance
|
|
67
|
-
`
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
68
|
+
Guidance for handling information-dense JSON via `jq` (query/reshape) and
|
|
69
|
+
`toon` (compress). The system-prompt nudge is injected **only when the user
|
|
70
|
+
prompt mentions JSON** (`json`/`jsonl`/`jq`/`toon`/`openapi`/…). TOON shines
|
|
71
|
+
on uniform/tabular arrays; deeply nested or array-of-arrays data and API
|
|
72
|
+
contracts stay as JSON.
|
|
73
|
+
|
|
74
|
+
The `toon-json` skill (full workflow + when-NOT-to-use guidance) is bundled in
|
|
75
|
+
`pix-skills` and auto-discovered from there.
|
|
71
76
|
|
|
72
77
|
**Requirement:** `jq` and `toon` on `PATH`.
|
|
73
78
|
|
|
@@ -75,6 +80,26 @@ array-of-arrays data and API contracts stay as JSON.
|
|
|
75
80
|
npm i -g @toon-format/cli
|
|
76
81
|
```
|
|
77
82
|
|
|
83
|
+
### Ponytail Mode (``)
|
|
84
|
+
|
|
85
|
+
"Lazy senior dev" mode. Governs **what** the agent builds (minimal code,
|
|
86
|
+
YAGNI), orthogonal to Caveman which governs **how** it talks — they pair. Before
|
|
87
|
+
writing code the agent stops at the first rung that holds: does this need to
|
|
88
|
+
exist → stdlib → native platform → installed dep → one line → minimum that
|
|
89
|
+
works. Validation, error handling, security, and accessibility are never cut.
|
|
90
|
+
|
|
91
|
+
| # | Name | Description |
|
|
92
|
+
|---|-------|--------------------------------------|
|
|
93
|
+
| 1 | lite | Name the lazier alternative, you pick |
|
|
94
|
+
| 2 | full | The ladder enforced (default) |
|
|
95
|
+
| 3 | ultra | YAGNI extremist |
|
|
96
|
+
|
|
97
|
+
`/opt ponytail config` opens a settings dialog. Default level for new sessions
|
|
98
|
+
and status-bar visibility are saved to `~/.pi/agent/ponytail.json`.
|
|
99
|
+
|
|
100
|
+
**No install required** — pure prompt injection, no external binary or PATH
|
|
101
|
+
dependency (unlike RTK and TOON).
|
|
102
|
+
|
|
78
103
|
## Installation
|
|
79
104
|
|
|
80
105
|
```bash
|
|
@@ -85,15 +110,16 @@ pi install npm:@xynogen/pix-optimizer
|
|
|
85
110
|
|
|
86
111
|
| File | Role |
|
|
87
112
|
|-------------------|-----------------------------------------------------------|
|
|
88
|
-
| `src/index.ts` | Wires the
|
|
113
|
+
| `src/index.ts` | Wires the four tools + shared status, registers `/opt` |
|
|
89
114
|
| `src/opt.ts` | The `/opt` router: parse, complete, dispatch |
|
|
90
115
|
| `src/status.ts` | Shared status-bar cell + `OptimizerHandle` contract |
|
|
91
116
|
| `src/caveman.ts` | Caveman logic, levels, prompt, settings dialog |
|
|
92
117
|
| `src/rtk.ts` | RTK prompt + bash command rewriting |
|
|
93
|
-
| `src/json.ts` | jq+TOON guidance, heuristics,
|
|
118
|
+
| `src/json.ts` | jq+TOON guidance, heuristics, system-prompt injection |
|
|
119
|
+
| `src/ponytail.ts` | Ponytail logic, levels, prompt, settings dialog |
|
|
94
120
|
|
|
95
121
|
Each tool registers its own lifecycle hooks and exposes an `OptimizerHandle`
|
|
96
|
-
that `/opt` dispatches to. All
|
|
122
|
+
that `/opt` dispatches to. All four share one `OptimizerStatus`.
|
|
97
123
|
|
|
98
124
|
## Development
|
|
99
125
|
|
|
@@ -113,9 +139,14 @@ This package was built by merging two upstream Pi community packages:
|
|
|
113
139
|
approach: prompt injection + live bash command rewriting that handles chained commands
|
|
114
140
|
(`&&`, `||`, `;`, `|`).
|
|
115
141
|
|
|
116
|
-
|
|
142
|
+
- **Ponytail mode** — ruleset adapted from [`git:github.com/DietrichGebert/ponytail`](https://github.com/DietrichGebert/ponytail),
|
|
143
|
+
the "lazy senior dev" skill. Reimplemented here as a native `/opt` tool with three intensity
|
|
144
|
+
levels, a settings dialog, and per-session persistence — no external hooks or files. The
|
|
145
|
+
ruleset (the YAGNI ladder + safety carve-outs) is rewritten as a system-prompt fragment.
|
|
146
|
+
|
|
147
|
+
All upstreams are MIT licensed. No codebase was copied directly — the logic was
|
|
117
148
|
rewritten and combined into a single extension with a unified `/opt` command and shared status bar.
|
|
118
|
-
This package does not sync back to
|
|
149
|
+
This package does not sync back to any upstream.
|
|
119
150
|
|
|
120
151
|
## Full distro
|
|
121
152
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xynogen/pix-optimizer",
|
|
3
|
-
"version": "1.0
|
|
4
|
-
"description": "Performance optimization suite for Pi Coding Agent - caveman mode + RTK tool rewriting + jq/TOON JSON compression",
|
|
3
|
+
"version": "1.1.0",
|
|
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",
|
|
7
7
|
"scripts": {
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
"toon",
|
|
31
31
|
"jq",
|
|
32
32
|
"json",
|
|
33
|
+
"ponytail",
|
|
33
34
|
"performance"
|
|
34
35
|
],
|
|
35
36
|
"author": "xynogen",
|
package/src/index.ts
CHANGED
|
@@ -2,11 +2,12 @@
|
|
|
2
2
|
* pix-optimizer — token-optimization suite for Pi Coding Agent.
|
|
3
3
|
*
|
|
4
4
|
* Three tools, combined into one extension + one command:
|
|
5
|
-
* - caveman:
|
|
6
|
-
* - rtk:
|
|
7
|
-
* - toon:
|
|
5
|
+
* - caveman: terse-output system prompt
|
|
6
|
+
* - rtk: prefixes shell commands with `rtk` + injects RTK prompt
|
|
7
|
+
* - toon: jq + TOON guidance for dense JSON (+ bundled skill)
|
|
8
|
+
* - ponytail: lazy-senior-dev system prompt (minimal code, YAGNI)
|
|
8
9
|
*
|
|
9
|
-
* They share ONE status-bar cell (
|
|
10
|
+
* They share ONE status-bar cell ( , only enabled tools shown) and ONE
|
|
10
11
|
* command (/opt <tool> [args]). index.ts wires lifecycle hooks via each
|
|
11
12
|
* module, then registers the merged command from their handles.
|
|
12
13
|
*/
|
|
@@ -15,6 +16,7 @@ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
|
15
16
|
import { caveman } from "./caveman.ts";
|
|
16
17
|
import { json } from "./json.ts";
|
|
17
18
|
import { registerOptCommand } from "./opt.ts";
|
|
19
|
+
import { ponytail } from "./ponytail.ts";
|
|
18
20
|
import { rtk } from "./rtk.ts";
|
|
19
21
|
import {
|
|
20
22
|
type OptimizerHandle,
|
|
@@ -32,6 +34,7 @@ export default function optimizer(pi: ExtensionAPI) {
|
|
|
32
34
|
caveman: caveman(pi, status),
|
|
33
35
|
rtk: rtk(pi, status),
|
|
34
36
|
toon: json(pi, status),
|
|
37
|
+
ponytail: ponytail(pi, status),
|
|
35
38
|
};
|
|
36
39
|
|
|
37
40
|
registerOptCommand(pi, handles);
|
package/src/json.test.ts
CHANGED
|
@@ -5,7 +5,6 @@ import {
|
|
|
5
5
|
JSON_SYSTEM_PROMPT,
|
|
6
6
|
mentionsJson,
|
|
7
7
|
objectDepth,
|
|
8
|
-
skillDir,
|
|
9
8
|
} from "./json.ts";
|
|
10
9
|
|
|
11
10
|
describe("JSON_SYSTEM_PROMPT", () => {
|
|
@@ -168,10 +167,3 @@ describe("adviseToon", () => {
|
|
|
168
167
|
expect(adviseToon(d2, 2).useToon).toBe(true);
|
|
169
168
|
});
|
|
170
169
|
});
|
|
171
|
-
|
|
172
|
-
describe("skillDir", () => {
|
|
173
|
-
it("points at the bundled toon-json skill", () => {
|
|
174
|
-
const dir = skillDir();
|
|
175
|
-
expect(dir.endsWith("skills/toon-json")).toBe(true);
|
|
176
|
-
});
|
|
177
|
-
});
|
package/src/json.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* json.ts — JSON token-optimization via jq + TOON.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
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
10
|
*
|
|
11
11
|
* TOON = Token-Oriented Object Notation (https://github.com/toon-format/spec).
|
|
12
12
|
* It shines on uniform/tabular arrays of objects (declare keys once, stream
|
|
@@ -17,8 +17,6 @@
|
|
|
17
17
|
* by index.ts alongside caveman(pi) and rtk(pi).
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
|
-
import { join } from "node:path";
|
|
21
|
-
import { fileURLToPath } from "node:url";
|
|
22
20
|
import type {
|
|
23
21
|
ExtensionAPI,
|
|
24
22
|
ExtensionCommandContext,
|
|
@@ -207,15 +205,6 @@ export function objectDepth(value: unknown): number {
|
|
|
207
205
|
|
|
208
206
|
// ── Bundled skill path ────────────────────────────────────────────────────────
|
|
209
207
|
|
|
210
|
-
/**
|
|
211
|
-
* Absolute path to the bundled `toon-json` skill directory, resolved relative
|
|
212
|
-
* to this module so it works regardless of the extension's install location.
|
|
213
|
-
*/
|
|
214
|
-
export function skillDir(): string {
|
|
215
|
-
const here = fileURLToPath(new URL(".", import.meta.url));
|
|
216
|
-
return join(here, "skills", "toon-json");
|
|
217
|
-
}
|
|
218
|
-
|
|
219
208
|
// ── Pi extension ──────────────────────────────────────────────────────────────
|
|
220
209
|
|
|
221
210
|
export function json(
|
|
@@ -245,12 +234,6 @@ export function json(
|
|
|
245
234
|
syncStatus(ctx);
|
|
246
235
|
});
|
|
247
236
|
|
|
248
|
-
// Surface the bundled skill so the model can load the full workflow.
|
|
249
|
-
pi.on("resources_discover", async () => {
|
|
250
|
-
if (!enabled) return undefined;
|
|
251
|
-
return { skillPaths: [skillDir()] };
|
|
252
|
-
});
|
|
253
|
-
|
|
254
237
|
// Inject the JSON-handling nudge into the system prompt, but ONLY when the
|
|
255
238
|
// user prompt actually mentions JSON / a related token — otherwise it's dead
|
|
256
239
|
// weight in every turn. Probe jq/toon lazily here (not at session_start) so
|
package/src/opt.test.ts
CHANGED
|
@@ -13,7 +13,12 @@ function fakeHandles(): Record<OptimizerTool, OptimizerHandle> {
|
|
|
13
13
|
.filter((v) => v.startsWith(prefix.trim().toLowerCase()))
|
|
14
14
|
.map((v) => ({ value: v, label: v, description: v })),
|
|
15
15
|
});
|
|
16
|
-
return {
|
|
16
|
+
return {
|
|
17
|
+
caveman: mk("caveman"),
|
|
18
|
+
rtk: mk("rtk"),
|
|
19
|
+
toon: mk("toon"),
|
|
20
|
+
ponytail: mk("ponytail"),
|
|
21
|
+
};
|
|
17
22
|
}
|
|
18
23
|
|
|
19
24
|
describe("parseInvocation", () => {
|
|
@@ -49,7 +54,12 @@ describe("completeInvocation", () => {
|
|
|
49
54
|
it("returns all tool names for empty prefix", () => {
|
|
50
55
|
const handles = fakeHandles();
|
|
51
56
|
const out = completeInvocation("", handles);
|
|
52
|
-
expect(out?.map((i) => i.value)).toEqual([
|
|
57
|
+
expect(out?.map((i) => i.value)).toEqual([
|
|
58
|
+
"caveman",
|
|
59
|
+
"rtk",
|
|
60
|
+
"toon",
|
|
61
|
+
"ponytail",
|
|
62
|
+
]);
|
|
53
63
|
});
|
|
54
64
|
|
|
55
65
|
it("delegates to the tool completer after the name", () => {
|
|
@@ -70,6 +80,7 @@ describe("buildOptHelp", () => {
|
|
|
70
80
|
expect(help).toContain("/opt caveman help");
|
|
71
81
|
expect(help).toContain("/opt rtk help");
|
|
72
82
|
expect(help).toContain("/opt toon help");
|
|
83
|
+
expect(help).toContain("/opt ponytail help");
|
|
73
84
|
expect(help).toContain("Usage: /opt <tool> [args]");
|
|
74
85
|
});
|
|
75
86
|
});
|
package/src/opt.ts
CHANGED
|
@@ -81,7 +81,7 @@ export function registerOptCommand(
|
|
|
81
81
|
handles: Record<OptimizerTool, OptimizerHandle>,
|
|
82
82
|
): void {
|
|
83
83
|
pi.registerCommand("opt", {
|
|
84
|
-
description: "pix-optimizer: caveman / rtk / toon
|
|
84
|
+
description: "pix-optimizer: caveman / rtk / toon / ponytail tools",
|
|
85
85
|
getArgumentCompletions: (prefix: string) =>
|
|
86
86
|
completeInvocation(prefix, handles),
|
|
87
87
|
handler: async (args, ctx) => {
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
buildHelp,
|
|
4
|
+
buildPrompt,
|
|
5
|
+
DEFAULT_CONFIG,
|
|
6
|
+
LEVEL_NUMBERS,
|
|
7
|
+
LEVELS,
|
|
8
|
+
type Level,
|
|
9
|
+
parseConfig,
|
|
10
|
+
resolveLevel,
|
|
11
|
+
STATUS_LABELS,
|
|
12
|
+
STOP_ALIASES,
|
|
13
|
+
toggleLevel,
|
|
14
|
+
} from "./ponytail.ts";
|
|
15
|
+
|
|
16
|
+
// ── LEVELS ────────────────────────────────────────────────────────────────────
|
|
17
|
+
|
|
18
|
+
describe("LEVELS", () => {
|
|
19
|
+
it("contains off as first entry", () => {
|
|
20
|
+
expect(LEVELS[0]).toBe("off");
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("contains all expected levels", () => {
|
|
24
|
+
const expected: Level[] = ["off", "lite", "full", "ultra"];
|
|
25
|
+
for (const l of expected) expect(LEVELS).toContain(l);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("has no micro level (caveman-only)", () => {
|
|
29
|
+
expect(LEVELS).not.toContain("micro" as Level);
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
// ── STOP_ALIASES ──────────────────────────────────────────────────────────────
|
|
34
|
+
|
|
35
|
+
describe("STOP_ALIASES", () => {
|
|
36
|
+
it("includes off, stop, quit", () => {
|
|
37
|
+
expect(STOP_ALIASES.has("off")).toBe(true);
|
|
38
|
+
expect(STOP_ALIASES.has("stop")).toBe(true);
|
|
39
|
+
expect(STOP_ALIASES.has("quit")).toBe(true);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("does not include active levels", () => {
|
|
43
|
+
expect(STOP_ALIASES.has("full")).toBe(false);
|
|
44
|
+
expect(STOP_ALIASES.has("ultra")).toBe(false);
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
// ── STATUS_LABELS ─────────────────────────────────────────────────────────────
|
|
49
|
+
|
|
50
|
+
describe("STATUS_LABELS", () => {
|
|
51
|
+
it("has a label for every non-off level", () => {
|
|
52
|
+
const nonOff = LEVELS.filter((l) => l !== "off") as Exclude<Level, "off">[];
|
|
53
|
+
for (const l of nonOff) {
|
|
54
|
+
expect(STATUS_LABELS[l]).toBeTruthy();
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("levels are uppercase", () => {
|
|
59
|
+
expect(STATUS_LABELS.lite).toBe("LITE");
|
|
60
|
+
expect(STATUS_LABELS.full).toBe("FULL");
|
|
61
|
+
expect(STATUS_LABELS.ultra).toBe("ULTRA");
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// ── resolveLevel ──────────────────────────────────────────────────────────────
|
|
66
|
+
|
|
67
|
+
describe("resolveLevel", () => {
|
|
68
|
+
it("resolves valid levels", () => {
|
|
69
|
+
expect(resolveLevel("lite")).toBe("lite");
|
|
70
|
+
expect(resolveLevel("full")).toBe("full");
|
|
71
|
+
expect(resolveLevel("ultra")).toBe("ultra");
|
|
72
|
+
expect(resolveLevel("off")).toBe("off");
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("maps stop aliases to off", () => {
|
|
76
|
+
expect(resolveLevel("stop")).toBe("off");
|
|
77
|
+
expect(resolveLevel("quit")).toBe("off");
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("is case-insensitive", () => {
|
|
81
|
+
expect(resolveLevel("FULL")).toBe("full");
|
|
82
|
+
expect(resolveLevel("Ultra")).toBe("ultra");
|
|
83
|
+
expect(resolveLevel("STOP")).toBe("off");
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("trims whitespace", () => {
|
|
87
|
+
expect(resolveLevel(" full ")).toBe("full");
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("returns null for unknown input", () => {
|
|
91
|
+
expect(resolveLevel("unknown")).toBeNull();
|
|
92
|
+
expect(resolveLevel("")).toBeNull();
|
|
93
|
+
expect(resolveLevel("config")).toBeNull(); // config handled separately
|
|
94
|
+
expect(resolveLevel("micro")).toBeNull(); // no micro level
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
// ── numeric levels ────────────────────────────────────────────────────────────
|
|
99
|
+
|
|
100
|
+
describe("numeric levels", () => {
|
|
101
|
+
it("maps 1/2/3 to lite/full/ultra", () => {
|
|
102
|
+
expect(resolveLevel("1")).toBe("lite");
|
|
103
|
+
expect(resolveLevel("2")).toBe("full");
|
|
104
|
+
expect(resolveLevel("3")).toBe("ultra");
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("maps 0 to off", () => {
|
|
108
|
+
expect(resolveLevel("0")).toBe("off");
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("LEVEL_NUMBERS only covers 1-3", () => {
|
|
112
|
+
expect(Object.keys(LEVEL_NUMBERS).sort()).toEqual(["1", "2", "3"]);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("rejects out-of-range numbers", () => {
|
|
116
|
+
expect(resolveLevel("4")).toBeNull();
|
|
117
|
+
expect(resolveLevel("9")).toBeNull();
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it("trims whitespace around numbers", () => {
|
|
121
|
+
expect(resolveLevel(" 2 ")).toBe("full");
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// ── buildHelp ─────────────────────────────────────────────────────────────────
|
|
126
|
+
|
|
127
|
+
describe("buildHelp", () => {
|
|
128
|
+
it("lists numeric shortcuts", () => {
|
|
129
|
+
const help = buildHelp("off");
|
|
130
|
+
expect(help).toContain("1");
|
|
131
|
+
expect(help).toContain("lite");
|
|
132
|
+
expect(help).toContain("2");
|
|
133
|
+
expect(help).toContain("full");
|
|
134
|
+
expect(help).toContain("3");
|
|
135
|
+
expect(help).toContain("ultra");
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it("shows current level when active", () => {
|
|
139
|
+
expect(buildHelp("ultra")).toContain("ULTRA");
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("shows off when disabled", () => {
|
|
143
|
+
expect(buildHelp("off")).toContain("off");
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it("mentions config", () => {
|
|
147
|
+
expect(buildHelp("off")).toContain("config");
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
// ── toggleLevel ───────────────────────────────────────────────────────────────
|
|
152
|
+
|
|
153
|
+
describe("toggleLevel", () => {
|
|
154
|
+
it("off → full", () => {
|
|
155
|
+
expect(toggleLevel("off")).toBe("full");
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it("full → off", () => {
|
|
159
|
+
expect(toggleLevel("full")).toBe("off");
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it("any non-off level → off", () => {
|
|
163
|
+
const nonOff = LEVELS.filter((l) => l !== "off") as Level[];
|
|
164
|
+
for (const l of nonOff) {
|
|
165
|
+
expect(toggleLevel(l)).toBe("off");
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
// ── buildPrompt ───────────────────────────────────────────────────────────────
|
|
171
|
+
|
|
172
|
+
describe("buildPrompt", () => {
|
|
173
|
+
it("returns empty string for off", () => {
|
|
174
|
+
expect(buildPrompt("off")).toBe("");
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it("includes BASE ladder for all active levels", () => {
|
|
178
|
+
for (const l of ["lite", "full", "ultra"] as Level[]) {
|
|
179
|
+
expect(buildPrompt(l)).toContain("PONYTAIL MODE ACTIVE");
|
|
180
|
+
expect(buildPrompt(l)).toContain("first rung that holds");
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it("includes SAFETY clause for all active levels", () => {
|
|
185
|
+
for (const l of ["lite", "full", "ultra"] as Level[]) {
|
|
186
|
+
expect(buildPrompt(l)).toContain("When NOT to be lazy");
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it("each level has distinct intensity instructions", () => {
|
|
191
|
+
const lite = buildPrompt("lite");
|
|
192
|
+
const ultra = buildPrompt("ultra");
|
|
193
|
+
expect(lite).toContain("name the lazier alternative");
|
|
194
|
+
expect(ultra).toContain("YAGNI extremist");
|
|
195
|
+
expect(lite).not.toContain("YAGNI extremist");
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
// ── parseConfig ───────────────────────────────────────────────────────────────
|
|
200
|
+
|
|
201
|
+
describe("parseConfig", () => {
|
|
202
|
+
it("parses valid config", () => {
|
|
203
|
+
const cfg = parseConfig({ defaultLevel: "ultra", showStatus: false });
|
|
204
|
+
expect(cfg.defaultLevel).toBe("ultra");
|
|
205
|
+
expect(cfg.showStatus).toBe(false);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it("falls back to defaults for invalid defaultLevel", () => {
|
|
209
|
+
const cfg = parseConfig({ defaultLevel: "invalid", showStatus: true });
|
|
210
|
+
expect(cfg.defaultLevel).toBe(DEFAULT_CONFIG.defaultLevel);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it("default level is full", () => {
|
|
214
|
+
expect(DEFAULT_CONFIG.defaultLevel).toBe("full");
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it("falls back to defaults for missing fields", () => {
|
|
218
|
+
const cfg = parseConfig({});
|
|
219
|
+
expect(cfg.defaultLevel).toBe(DEFAULT_CONFIG.defaultLevel);
|
|
220
|
+
expect(cfg.showStatus).toBe(DEFAULT_CONFIG.showStatus);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it("falls back to defaults for null/undefined", () => {
|
|
224
|
+
const cfg = parseConfig(null);
|
|
225
|
+
expect(cfg.defaultLevel).toBe(DEFAULT_CONFIG.defaultLevel);
|
|
226
|
+
expect(cfg.showStatus).toBe(DEFAULT_CONFIG.showStatus);
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it("accepts all valid levels as defaultLevel", () => {
|
|
230
|
+
for (const l of LEVELS) {
|
|
231
|
+
expect(
|
|
232
|
+
parseConfig({ defaultLevel: l, showStatus: true }).defaultLevel,
|
|
233
|
+
).toBe(l);
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
});
|
package/src/ponytail.ts
ADDED
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ponytail.ts — pure logic + Pi extension
|
|
3
|
+
*
|
|
4
|
+
* "Lazy senior dev" mode: governs WHAT the agent builds (minimal code, YAGNI),
|
|
5
|
+
* orthogonal to caveman which governs HOW it talks. Pure helpers exported for
|
|
6
|
+
* tests; ponytail(pi, status) is the extension entry, wired by index.ts.
|
|
7
|
+
*
|
|
8
|
+
* Ruleset adapted from DietrichGebert/ponytail (MIT), the "lazy senior dev"
|
|
9
|
+
* skill. We inject it as a system-prompt fragment — no external hooks/files.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
13
|
+
import { homedir } from "node:os";
|
|
14
|
+
import { join } from "node:path";
|
|
15
|
+
import type {
|
|
16
|
+
ExtensionAPI,
|
|
17
|
+
ExtensionCommandContext,
|
|
18
|
+
ExtensionContext,
|
|
19
|
+
} from "@earendil-works/pi-coding-agent";
|
|
20
|
+
import { getSettingsListTheme } from "@earendil-works/pi-coding-agent";
|
|
21
|
+
import {
|
|
22
|
+
Container,
|
|
23
|
+
type SettingItem,
|
|
24
|
+
SettingsList,
|
|
25
|
+
Text,
|
|
26
|
+
} from "@earendil-works/pi-tui";
|
|
27
|
+
import type { OptimizerHandle, OptimizerStatus } from "./status.ts";
|
|
28
|
+
|
|
29
|
+
// ── Levels ────────────────────────────────────────────────────────────────────
|
|
30
|
+
|
|
31
|
+
export const LEVELS = ["off", "lite", "full", "ultra"] as const;
|
|
32
|
+
|
|
33
|
+
export type Level = (typeof LEVELS)[number];
|
|
34
|
+
|
|
35
|
+
export const STOP_ALIASES = new Set(["off", "stop", "quit", "0"]);
|
|
36
|
+
|
|
37
|
+
// Numeric shortcuts: /opt ponytail 1|2|3
|
|
38
|
+
export const LEVEL_NUMBERS: Record<string, Level> = {
|
|
39
|
+
"1": "lite",
|
|
40
|
+
"2": "full",
|
|
41
|
+
"3": "ultra",
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const PONYTAIL_COMMAND_OPTIONS = [
|
|
45
|
+
{ value: "1", label: "1 (lite)", description: "Name the lazier alternative" },
|
|
46
|
+
{ value: "2", label: "2 (full)", description: "The ladder enforced" },
|
|
47
|
+
{ value: "3", label: "3 (ultra)", description: "YAGNI extremist" },
|
|
48
|
+
{ value: "lite", label: "lite", description: "Name the lazier alternative" },
|
|
49
|
+
{ value: "full", label: "full", description: "The ladder enforced" },
|
|
50
|
+
{ value: "ultra", label: "ultra", description: "YAGNI extremist" },
|
|
51
|
+
{ value: "off", label: "off", description: "Disable ponytail mode" },
|
|
52
|
+
{ value: "stop", label: "stop", description: "Disable ponytail mode" },
|
|
53
|
+
{ value: "quit", label: "quit", description: "Disable ponytail mode" },
|
|
54
|
+
{ value: "config", label: "config", description: "Open settings dialog" },
|
|
55
|
+
] as const;
|
|
56
|
+
|
|
57
|
+
// ── Status labels ─────────────────────────────────────────────────────────────
|
|
58
|
+
|
|
59
|
+
export const STATUS_LABELS: Record<Exclude<Level, "off">, string> = {
|
|
60
|
+
lite: "LITE",
|
|
61
|
+
full: "FULL",
|
|
62
|
+
ultra: "ULTRA",
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
// ── Prompt fragments ──────────────────────────────────────────────────────────
|
|
66
|
+
|
|
67
|
+
const BASE = `\
|
|
68
|
+
PONYTAIL MODE ACTIVE. You are a lazy senior developer. Lazy means efficient, \
|
|
69
|
+
not careless. The best code is the code never written.
|
|
70
|
+
|
|
71
|
+
Before writing any code, stop at the first rung that holds:
|
|
72
|
+
1. Does this need to exist at all? Speculative need = skip it, say so in one line. (YAGNI)
|
|
73
|
+
2. Stdlib does it? Use it.
|
|
74
|
+
3. Native platform feature covers it? Use it (\`<input type="date">\` over a picker lib, CSS over JS, DB constraint over app code).
|
|
75
|
+
4. Already-installed dependency solves it? Use it. Never add a new one for what a few lines can do.
|
|
76
|
+
5. Can it be one line? One line.
|
|
77
|
+
6. Only then: the minimum code that works.
|
|
78
|
+
|
|
79
|
+
The ladder is a reflex, not a research project. Two rungs work → take the higher one and move on.
|
|
80
|
+
|
|
81
|
+
Rules:
|
|
82
|
+
- No unrequested abstractions: no interface with one impl, no factory for one product, no config for a value that never changes.
|
|
83
|
+
- No boilerplate, no scaffolding "for later". Deletion over addition. Boring over clever. Fewest files possible.
|
|
84
|
+
- Complex request? Ship the lazy version and question it in the same response. Never stall on an answer you can default.
|
|
85
|
+
- Two same-size stdlib options? Take the one correct on edge cases. Lazy means less code, not the flimsier algorithm.
|
|
86
|
+
- Mark deliberate simplifications with a \`ponytail:\` comment. A shortcut with a known ceiling names the ceiling and the upgrade path.`;
|
|
87
|
+
|
|
88
|
+
const INTENSITY: Record<Exclude<Level, "off">, string> = {
|
|
89
|
+
lite: `\
|
|
90
|
+
Build what's asked, but name the lazier alternative in one line. User picks.
|
|
91
|
+
Example: "Done, cache added. FYI: \`functools.lru_cache\` covers this in one line if you'd rather not own a cache class."`,
|
|
92
|
+
|
|
93
|
+
full: `\
|
|
94
|
+
The ladder enforced. Stdlib and native first. Shortest diff, shortest explanation.
|
|
95
|
+
Example: "\`@lru_cache(maxsize=1000)\` on the fetch function. Skipped custom cache class, add when lru_cache measurably falls short."`,
|
|
96
|
+
|
|
97
|
+
ultra: `\
|
|
98
|
+
YAGNI extremist. Deletion before addition. Ship the one-liner and challenge the rest of the requirement in the same breath.
|
|
99
|
+
Example: "No cache until a profiler says so. When it does: \`@lru_cache\`. A hand-rolled TTL cache class is a bug farm with a hit rate."`,
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const SAFETY = `\
|
|
103
|
+
When NOT to be lazy: never simplify away input validation at trust boundaries, \
|
|
104
|
+
error handling that prevents data loss, security, accessibility, or anything \
|
|
105
|
+
explicitly requested. Hardware is never the spec ideal — leave the calibration knob.
|
|
106
|
+
Lazy code without its check is unfinished: non-trivial logic leaves ONE runnable check behind \
|
|
107
|
+
(an assert-based self-check or one small test file; no frameworks). Trivial one-liners need no test.
|
|
108
|
+
Output: code first, then at most three short lines — what was skipped, when to add it.
|
|
109
|
+
Boundaries: ponytail governs what you build, not how you talk. "stop ponytail" / "normal mode" reverts.`;
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Build the system prompt injection for a given level.
|
|
113
|
+
* Returns empty string when level is "off".
|
|
114
|
+
*/
|
|
115
|
+
export function buildPrompt(level: Level): string {
|
|
116
|
+
if (level === "off") return "";
|
|
117
|
+
return [BASE, "", `Intensity: ${INTENSITY[level]}`, "", SAFETY].join("\n");
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ── Level resolution ──────────────────────────────────────────────────────────
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Resolve a raw command arg to a Level, or return null if unrecognised.
|
|
124
|
+
* Handles stop aliases (stop/quit → "off") and valid level names.
|
|
125
|
+
*/
|
|
126
|
+
export function resolveLevel(arg: string): Level | null {
|
|
127
|
+
const a = arg.trim().toLowerCase();
|
|
128
|
+
if (STOP_ALIASES.has(a)) return "off";
|
|
129
|
+
if (LEVEL_NUMBERS[a]) return LEVEL_NUMBERS[a];
|
|
130
|
+
if (LEVELS.includes(a as Level)) return a as Level;
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Help text shown when /opt ponytail is run with no argument.
|
|
136
|
+
*/
|
|
137
|
+
export function buildHelp(current: Level): string {
|
|
138
|
+
const statusLine =
|
|
139
|
+
current === "off" ? "off" : `${STATUS_LABELS[current]} (${current})`;
|
|
140
|
+
return [
|
|
141
|
+
`Ponytail mode: ${statusLine}`,
|
|
142
|
+
"",
|
|
143
|
+
"Usage: /opt ponytail <level>",
|
|
144
|
+
" 1 lite - name the lazier alternative, you pick",
|
|
145
|
+
" 2 full - the ladder enforced (default)",
|
|
146
|
+
" 3 ultra - YAGNI extremist",
|
|
147
|
+
" 0 off - disable (aliases: off, stop, quit)",
|
|
148
|
+
"",
|
|
149
|
+
" config - open settings dialog",
|
|
150
|
+
].join("\n");
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Toggle: off → full, anything else → off.
|
|
155
|
+
*/
|
|
156
|
+
export function toggleLevel(current: Level): Level {
|
|
157
|
+
return current === "off" ? "full" : "off";
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// ── Config ────────────────────────────────────────────────────────────────────
|
|
161
|
+
|
|
162
|
+
export interface PonytailConfig {
|
|
163
|
+
/** Level to apply on new sessions. "off" means don't auto-enable. */
|
|
164
|
+
defaultLevel: Level;
|
|
165
|
+
/** Whether to show the status bar indicator. */
|
|
166
|
+
showStatus: boolean;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export const DEFAULT_CONFIG: PonytailConfig = {
|
|
170
|
+
defaultLevel: "full",
|
|
171
|
+
showStatus: true,
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
const CONFIG_PATH = join(homedir(), ".pi", "agent", "ponytail.json");
|
|
175
|
+
|
|
176
|
+
export function parseConfig(raw: unknown): PonytailConfig {
|
|
177
|
+
const parsed = raw as Record<string, unknown>;
|
|
178
|
+
return {
|
|
179
|
+
defaultLevel: LEVELS.includes(parsed?.defaultLevel as Level)
|
|
180
|
+
? (parsed.defaultLevel as Level)
|
|
181
|
+
: DEFAULT_CONFIG.defaultLevel,
|
|
182
|
+
showStatus:
|
|
183
|
+
typeof parsed?.showStatus === "boolean"
|
|
184
|
+
? parsed.showStatus
|
|
185
|
+
: DEFAULT_CONFIG.showStatus,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
let _saveQueue: Promise<void> = Promise.resolve();
|
|
190
|
+
|
|
191
|
+
async function loadConfig(): Promise<PonytailConfig> {
|
|
192
|
+
try {
|
|
193
|
+
const raw = await readFile(CONFIG_PATH, "utf8");
|
|
194
|
+
return parseConfig(JSON.parse(raw));
|
|
195
|
+
} catch {
|
|
196
|
+
return { ...DEFAULT_CONFIG };
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
async function saveConfig(config: PonytailConfig): Promise<void> {
|
|
201
|
+
const snapshot = `${JSON.stringify(config, null, 2)}\n`;
|
|
202
|
+
_saveQueue = _saveQueue.then(async () => {
|
|
203
|
+
await mkdir(join(homedir(), ".pi", "agent"), { recursive: true });
|
|
204
|
+
await writeFile(CONFIG_PATH, snapshot, "utf8");
|
|
205
|
+
});
|
|
206
|
+
return _saveQueue;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// ── Pi extension ────────────────────────────────────────────────────────────
|
|
210
|
+
|
|
211
|
+
export function ponytail(
|
|
212
|
+
pi: ExtensionAPI,
|
|
213
|
+
status: OptimizerStatus,
|
|
214
|
+
): OptimizerHandle {
|
|
215
|
+
let level: Level = "off";
|
|
216
|
+
let config: PonytailConfig = { ...DEFAULT_CONFIG };
|
|
217
|
+
let configLoadPromise: Promise<void> | null = null;
|
|
218
|
+
|
|
219
|
+
const ensureConfigLoaded = async () => {
|
|
220
|
+
if (!configLoadPromise) {
|
|
221
|
+
configLoadPromise = (async () => {
|
|
222
|
+
config = await loadConfig();
|
|
223
|
+
if (level === "off" && config.defaultLevel !== "off") {
|
|
224
|
+
level = config.defaultLevel;
|
|
225
|
+
}
|
|
226
|
+
})();
|
|
227
|
+
}
|
|
228
|
+
await configLoadPromise;
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
// -- Status: report into the shared optimizer indicator. --
|
|
232
|
+
|
|
233
|
+
function syncStatus(ctx: Pick<ExtensionContext, "ui">) {
|
|
234
|
+
const on = level !== "off" && config.showStatus;
|
|
235
|
+
status.set("ponytail", on, ctx);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Inject ponytail prompt via before_agent_start
|
|
239
|
+
pi.on("before_agent_start", async (event, _ctx) => {
|
|
240
|
+
const prompt = buildPrompt(level);
|
|
241
|
+
if (!prompt) return undefined;
|
|
242
|
+
const existing = event.systemPrompt ?? "";
|
|
243
|
+
return { systemPrompt: `${prompt}\n\n${existing}` };
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
// -- Restore state on session load --
|
|
247
|
+
|
|
248
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
249
|
+
await ensureConfigLoaded();
|
|
250
|
+
|
|
251
|
+
let sessionLevel: Level | null = null;
|
|
252
|
+
for (const entry of ctx.sessionManager.getEntries()) {
|
|
253
|
+
if (entry.type === "custom" && entry.customType === "ponytail-level") {
|
|
254
|
+
sessionLevel = (entry.data as { level: Level })?.level ?? null;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (sessionLevel !== null) {
|
|
259
|
+
level = sessionLevel;
|
|
260
|
+
} else if (config.defaultLevel !== "off") {
|
|
261
|
+
level = config.defaultLevel;
|
|
262
|
+
pi.appendEntry("ponytail-level", { level });
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
syncStatus(ctx);
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
pi.on("agent_start", async (_event, ctx) => {
|
|
269
|
+
syncStatus(ctx);
|
|
270
|
+
});
|
|
271
|
+
pi.on("agent_end", async (_event, ctx) => {
|
|
272
|
+
syncStatus(ctx);
|
|
273
|
+
});
|
|
274
|
+
pi.on("session_shutdown", async () => {});
|
|
275
|
+
|
|
276
|
+
// -- Subcommand handler (dispatched by the merged /opt router) --
|
|
277
|
+
|
|
278
|
+
async function run(
|
|
279
|
+
args: string,
|
|
280
|
+
ctx: ExtensionCommandContext,
|
|
281
|
+
): Promise<void> {
|
|
282
|
+
const arg = args.trim().toLowerCase();
|
|
283
|
+
|
|
284
|
+
// No argument → show help
|
|
285
|
+
if (!arg) {
|
|
286
|
+
ctx.ui.notify(buildHelp(level), "info");
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (arg === "config") {
|
|
291
|
+
await openConfig(ctx);
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const resolved = resolveLevel(arg);
|
|
296
|
+
if (resolved === null) {
|
|
297
|
+
ctx.ui.notify(
|
|
298
|
+
`Unknown: "${arg}". Use 1/2/3, ${LEVELS.join(", ")}, stop, quit, or config`,
|
|
299
|
+
"error",
|
|
300
|
+
);
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
level = resolved;
|
|
304
|
+
|
|
305
|
+
pi.appendEntry("ponytail-level", { level });
|
|
306
|
+
syncStatus(ctx);
|
|
307
|
+
|
|
308
|
+
ctx.ui.notify(
|
|
309
|
+
level === "off"
|
|
310
|
+
? "Ponytail mode off."
|
|
311
|
+
: `Ponytail: ${STATUS_LABELS[level]}`,
|
|
312
|
+
"info",
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function complete(prefix: string) {
|
|
317
|
+
const normalized = prefix.trim().toLowerCase();
|
|
318
|
+
const items = PONYTAIL_COMMAND_OPTIONS.filter((item) =>
|
|
319
|
+
item.value.startsWith(normalized),
|
|
320
|
+
);
|
|
321
|
+
return items.length > 0 ? items.map((i) => ({ ...i })) : null;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// -- config: interactive SettingsList --
|
|
325
|
+
|
|
326
|
+
async function openConfig(ctx: ExtensionContext) {
|
|
327
|
+
await ensureConfigLoaded();
|
|
328
|
+
|
|
329
|
+
await ctx.ui.custom((_tui, theme, _kb, done) => {
|
|
330
|
+
const items: SettingItem[] = [
|
|
331
|
+
{
|
|
332
|
+
id: "defaultLevel",
|
|
333
|
+
label: "Default level for new sessions",
|
|
334
|
+
currentValue: config.defaultLevel,
|
|
335
|
+
values: [...LEVELS],
|
|
336
|
+
},
|
|
337
|
+
{
|
|
338
|
+
id: "showStatus",
|
|
339
|
+
label: "Show status bar",
|
|
340
|
+
currentValue: config.showStatus ? "on" : "off",
|
|
341
|
+
values: ["on", "off"],
|
|
342
|
+
},
|
|
343
|
+
];
|
|
344
|
+
|
|
345
|
+
const container = new Container();
|
|
346
|
+
container.addChild(
|
|
347
|
+
new Text(theme.fg("accent", theme.bold(" Ponytail Config")), 0, 0),
|
|
348
|
+
);
|
|
349
|
+
container.addChild(
|
|
350
|
+
new Text(theme.fg("dim", " Saved to ~/.pi/agent/ponytail.json"), 0, 0),
|
|
351
|
+
);
|
|
352
|
+
container.addChild(
|
|
353
|
+
new Text(
|
|
354
|
+
theme.fg("dim", " Default level applies to future sessions."),
|
|
355
|
+
0,
|
|
356
|
+
0,
|
|
357
|
+
),
|
|
358
|
+
);
|
|
359
|
+
container.addChild(new Text("", 0, 0));
|
|
360
|
+
|
|
361
|
+
const applySettingChange = (id: string, newValue: string) => {
|
|
362
|
+
if (id === "defaultLevel" && LEVELS.includes(newValue as Level)) {
|
|
363
|
+
config.defaultLevel = newValue as Level;
|
|
364
|
+
} else if (id === "showStatus") {
|
|
365
|
+
config.showStatus = newValue === "on";
|
|
366
|
+
}
|
|
367
|
+
saveConfig(config);
|
|
368
|
+
syncStatus(ctx);
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
const settingsList = new SettingsList(
|
|
372
|
+
items,
|
|
373
|
+
Math.min(items.length + 2, 10),
|
|
374
|
+
getSettingsListTheme(),
|
|
375
|
+
applySettingChange,
|
|
376
|
+
() => done(undefined),
|
|
377
|
+
);
|
|
378
|
+
|
|
379
|
+
container.addChild(settingsList);
|
|
380
|
+
container.addChild(
|
|
381
|
+
new Text(
|
|
382
|
+
theme.fg("dim", " ←→/hl/tab change • ↑↓/jk move • esc close"),
|
|
383
|
+
0,
|
|
384
|
+
0,
|
|
385
|
+
),
|
|
386
|
+
);
|
|
387
|
+
|
|
388
|
+
const cycleSelectedValue = (direction: -1 | 1) => {
|
|
389
|
+
const selectedIndex = (
|
|
390
|
+
settingsList as unknown as { selectedIndex: number }
|
|
391
|
+
).selectedIndex;
|
|
392
|
+
const item = items[selectedIndex];
|
|
393
|
+
if (!item?.values?.length) return;
|
|
394
|
+
const currentIndex = item.values.indexOf(item.currentValue);
|
|
395
|
+
const nextIndex =
|
|
396
|
+
(currentIndex + direction + item.values.length) % item.values.length;
|
|
397
|
+
const newValue = item.values[nextIndex]!;
|
|
398
|
+
item.currentValue = newValue;
|
|
399
|
+
settingsList.updateValue(item.id, newValue);
|
|
400
|
+
applySettingChange(item.id, newValue);
|
|
401
|
+
};
|
|
402
|
+
|
|
403
|
+
return {
|
|
404
|
+
render: (w: number) => container.render(w),
|
|
405
|
+
invalidate: () => container.invalidate(),
|
|
406
|
+
handleInput: (data: string) => {
|
|
407
|
+
if (data === "j") data = "\u001b[B";
|
|
408
|
+
else if (data === "k") data = "\u001b[A";
|
|
409
|
+
else if (data === "h") {
|
|
410
|
+
cycleSelectedValue(-1);
|
|
411
|
+
_tui.requestRender();
|
|
412
|
+
return;
|
|
413
|
+
} else if (data === "l" || data === "\u001b[C" || data === "\t") {
|
|
414
|
+
cycleSelectedValue(1);
|
|
415
|
+
_tui.requestRender();
|
|
416
|
+
return;
|
|
417
|
+
} else if (data === "\u001b[D") {
|
|
418
|
+
cycleSelectedValue(-1);
|
|
419
|
+
_tui.requestRender();
|
|
420
|
+
return;
|
|
421
|
+
} else if (data === "\u001b" || data === "q") {
|
|
422
|
+
done(undefined);
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
settingsList.handleInput(data);
|
|
427
|
+
_tui.requestRender();
|
|
428
|
+
},
|
|
429
|
+
};
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
return {
|
|
434
|
+
name: "ponytail",
|
|
435
|
+
help: "ponytail <1|2|3|lite|full|ultra|off|config> — lazy senior dev (minimal code)",
|
|
436
|
+
run,
|
|
437
|
+
complete,
|
|
438
|
+
};
|
|
439
|
+
}
|
package/src/status.test.ts
CHANGED
|
@@ -11,26 +11,36 @@ const tag = (c: string, t: string) => `<${c}>${t}</${c}>`;
|
|
|
11
11
|
|
|
12
12
|
describe("renderStatus", () => {
|
|
13
13
|
it("shows ALL icons in order, accent when enabled", () => {
|
|
14
|
-
expect(
|
|
15
|
-
|
|
14
|
+
expect(
|
|
15
|
+
renderStatus(
|
|
16
|
+
{ caveman: true, rtk: true, toon: true, ponytail: true },
|
|
17
|
+
tag,
|
|
18
|
+
),
|
|
19
|
+
).toBe(
|
|
20
|
+
`<accent>${TOOL_ICONS.caveman}</accent> <accent>${TOOL_ICONS.rtk}</accent> <accent>${TOOL_ICONS.toon}</accent> <accent>${TOOL_ICONS.ponytail}</accent> `,
|
|
16
21
|
);
|
|
17
22
|
});
|
|
18
23
|
|
|
19
24
|
it("dims disabled tools but still shows them", () => {
|
|
20
|
-
expect(
|
|
21
|
-
|
|
25
|
+
expect(
|
|
26
|
+
renderStatus(
|
|
27
|
+
{ caveman: false, rtk: true, toon: true, ponytail: true },
|
|
28
|
+
tag,
|
|
29
|
+
),
|
|
30
|
+
).toBe(
|
|
31
|
+
`<dim>${TOOL_ICONS.caveman}</dim> <accent>${TOOL_ICONS.rtk}</accent> <accent>${TOOL_ICONS.toon}</accent> <accent>${TOOL_ICONS.ponytail}</accent> `,
|
|
22
32
|
);
|
|
23
33
|
});
|
|
24
34
|
|
|
25
35
|
it("all dim when nothing enabled (cell never empty)", () => {
|
|
26
36
|
expect(renderStatus({}, tag)).toBe(
|
|
27
|
-
`<dim>${TOOL_ICONS.caveman}</dim> <dim>${TOOL_ICONS.rtk}</dim> <dim>${TOOL_ICONS.toon}</dim> `,
|
|
37
|
+
`<dim>${TOOL_ICONS.caveman}</dim> <dim>${TOOL_ICONS.rtk}</dim> <dim>${TOOL_ICONS.toon}</dim> <dim>${TOOL_ICONS.ponytail}</dim> `,
|
|
28
38
|
);
|
|
29
39
|
});
|
|
30
40
|
|
|
31
41
|
it("preserves fixed order regardless of insertion order", () => {
|
|
32
42
|
expect(renderStatus({ toon: true, caveman: true }, tag)).toBe(
|
|
33
|
-
`<accent>${TOOL_ICONS.caveman}</accent> <dim>${TOOL_ICONS.rtk}</dim> <accent>${TOOL_ICONS.toon}</accent> `,
|
|
43
|
+
`<accent>${TOOL_ICONS.caveman}</accent> <dim>${TOOL_ICONS.rtk}</dim> <accent>${TOOL_ICONS.toon}</accent> <dim>${TOOL_ICONS.ponytail}</dim> `,
|
|
34
44
|
);
|
|
35
45
|
});
|
|
36
46
|
});
|
|
@@ -55,9 +65,9 @@ describe("OptimizerStatus", () => {
|
|
|
55
65
|
status.set("rtk", true, ctx as never);
|
|
56
66
|
const last = ctx.calls.at(-1)!;
|
|
57
67
|
expect(last.key).toBe(STATUS_KEY);
|
|
58
|
-
// caveman + toon still unset (dim), rtk accent.
|
|
68
|
+
// caveman + toon + ponytail still unset (dim), rtk accent.
|
|
59
69
|
expect(last.text).toBe(
|
|
60
|
-
`<dim>${TOOL_ICONS.caveman}</dim> <accent>${TOOL_ICONS.rtk}</accent> <dim>${TOOL_ICONS.toon}</dim> `,
|
|
70
|
+
`<dim>${TOOL_ICONS.caveman}</dim> <accent>${TOOL_ICONS.rtk}</accent> <dim>${TOOL_ICONS.toon}</dim> <dim>${TOOL_ICONS.ponytail}</dim> `,
|
|
61
71
|
);
|
|
62
72
|
});
|
|
63
73
|
|
|
@@ -68,7 +78,7 @@ describe("OptimizerStatus", () => {
|
|
|
68
78
|
status.set("toon", true, ctx as never);
|
|
69
79
|
const last = ctx.calls.at(-1)!;
|
|
70
80
|
expect(last.text).toBe(
|
|
71
|
-
`<accent>${TOOL_ICONS.caveman}</accent> <dim>${TOOL_ICONS.rtk}</dim> <accent>${TOOL_ICONS.toon}</accent> `,
|
|
81
|
+
`<accent>${TOOL_ICONS.caveman}</accent> <dim>${TOOL_ICONS.rtk}</dim> <accent>${TOOL_ICONS.toon}</accent> <dim>${TOOL_ICONS.ponytail}</dim> `,
|
|
72
82
|
);
|
|
73
83
|
});
|
|
74
84
|
|
|
@@ -79,7 +89,7 @@ describe("OptimizerStatus", () => {
|
|
|
79
89
|
status.set("rtk", false, ctx as never);
|
|
80
90
|
const last = ctx.calls.at(-1)!;
|
|
81
91
|
expect(last.text).toBe(
|
|
82
|
-
`<dim>${TOOL_ICONS.caveman}</dim> <dim>${TOOL_ICONS.rtk}</dim> <dim>${TOOL_ICONS.toon}</dim> `,
|
|
92
|
+
`<dim>${TOOL_ICONS.caveman}</dim> <dim>${TOOL_ICONS.rtk}</dim> <dim>${TOOL_ICONS.toon}</dim> <dim>${TOOL_ICONS.ponytail}</dim> `,
|
|
83
93
|
);
|
|
84
94
|
});
|
|
85
95
|
});
|
package/src/status.ts
CHANGED
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
* the cell renders only the icons whose tool is currently enabled, in a fixed
|
|
8
8
|
* order, accent-colored. When everything is off the cell is empty.
|
|
9
9
|
*
|
|
10
|
-
* all on:
|
|
11
|
-
* caveman off:
|
|
10
|
+
* all on:
|
|
11
|
+
* caveman off:
|
|
12
12
|
* all off: (empty)
|
|
13
13
|
*/
|
|
14
14
|
|
|
@@ -44,16 +44,22 @@ export interface OptimizerHandle {
|
|
|
44
44
|
export const STATUS_KEY = "pix-optimizer";
|
|
45
45
|
|
|
46
46
|
/** Tools that participate in the shared indicator, in render order. */
|
|
47
|
-
export type OptimizerTool = "caveman" | "rtk" | "toon";
|
|
47
|
+
export type OptimizerTool = "caveman" | "rtk" | "toon" | "ponytail";
|
|
48
48
|
|
|
49
49
|
export const TOOL_ICONS: Record<OptimizerTool, string> = {
|
|
50
|
-
caveman: "
|
|
51
|
-
rtk: "
|
|
52
|
-
toon: "
|
|
50
|
+
caveman: "",
|
|
51
|
+
rtk: "",
|
|
52
|
+
toon: "",
|
|
53
|
+
ponytail: "",
|
|
53
54
|
};
|
|
54
55
|
|
|
55
56
|
/** Fixed left-to-right order of icons in the cell. */
|
|
56
|
-
const TOOL_ORDER: readonly OptimizerTool[] = [
|
|
57
|
+
const TOOL_ORDER: readonly OptimizerTool[] = [
|
|
58
|
+
"caveman",
|
|
59
|
+
"rtk",
|
|
60
|
+
"toon",
|
|
61
|
+
"ponytail",
|
|
62
|
+
];
|
|
57
63
|
|
|
58
64
|
/** Theme color for enabled icons. */
|
|
59
65
|
const ENABLED_COLOR: ThemeColor = "accent";
|
|
@@ -1,101 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: toon-json
|
|
3
|
-
description: Manipulate information-dense JSON efficiently with jq + TOON. Use when fetching/reading large or repetitive JSON (LLM schemas, OpenAPI specs, API responses, datasets, config dumps) into context. Query/reshape with jq, compress to TOON to cut tokens, decode back to JSON only when a strict contract needs it.
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# TOON + jq: Dense JSON Workflow
|
|
7
|
-
|
|
8
|
-
## Goal
|
|
9
|
-
Carry only the JSON slice you need, in the cheapest encoding. Query with `jq`,
|
|
10
|
-
compress with `toon`, and round-trip back to JSON **only** when a contract
|
|
11
|
-
requires strict JSON.
|
|
12
|
-
|
|
13
|
-
TOON (Token-Oriented Object Notation, https://github.com/toon-format/spec) is a
|
|
14
|
-
line-oriented encoding of the JSON data model. Uniform arrays of objects declare
|
|
15
|
-
their keys once and stream bare rows, so token cost drops sharply on tabular and
|
|
16
|
-
dense data.
|
|
17
|
-
|
|
18
|
-
## The pipeline
|
|
19
|
-
|
|
20
|
-
```bash
|
|
21
|
-
# Fetch → reshape → compress (most common)
|
|
22
|
-
curl -s https://api.example.com/models | jq '.data' | toon
|
|
23
|
-
|
|
24
|
-
# Local file, show token savings
|
|
25
|
-
cat openapi.json | jq '.paths' | toon --stats
|
|
26
|
-
|
|
27
|
-
# Just compress, no query
|
|
28
|
-
cat data.json | toon
|
|
29
|
-
|
|
30
|
-
# Convert TOON back to JSON (strict contract / downstream parser)
|
|
31
|
-
echo "$TOON_BLOB" | toon -d
|
|
32
|
-
cat data.toon | toon --decode
|
|
33
|
-
```
|
|
34
|
-
|
|
35
|
-
`toon` auto-detects direction from input. Force it with `-e` (encode JSON→TOON)
|
|
36
|
-
or `-d` (decode TOON→JSON).
|
|
37
|
-
|
|
38
|
-
### Useful flags
|
|
39
|
-
- `--stats` — print token/byte statistics for the conversion
|
|
40
|
-
- `--delimiter=,|\t|"|"` — array delimiter (comma default; tab/pipe can tokenize better)
|
|
41
|
-
- `--keyFolding=safe` — collapse single-key nesting chains
|
|
42
|
-
- `--no-strict` — lenient decode
|
|
43
|
-
|
|
44
|
-
## Decide before you compress
|
|
45
|
-
|
|
46
|
-
TOON is not always smaller. Pick based on shape:
|
|
47
|
-
|
|
48
|
-
| Shape | Action | Why |
|
|
49
|
-
|---|---|---|
|
|
50
|
-
| Uniform array of objects (same keys, primitive values) | **TOON** | Sweet spot — keys declared once, rows streamed; savings scale with rows × fields |
|
|
51
|
-
| Flat object / primitive array | **TOON** | Drops quotes and braces |
|
|
52
|
-
| Shallow nesting | **TOON** | Indentation cheaper than braces at low depth |
|
|
53
|
-
| Deeply nested / non-uniform | **JSON** | Indentation cost grows; compact JSON can win |
|
|
54
|
-
| Array of arrays | **JSON** | TOON's one structurally-worse case (explicit list markers + inner headers) |
|
|
55
|
-
| API contract / payload to send or store | **JSON** | Must stay valid JSON for the consumer |
|
|
56
|
-
|
|
57
|
-
Rule of thumb: **TOON for reading dense data into context. JSON for contracts.**
|
|
58
|
-
|
|
59
|
-
## When NOT to use TOON
|
|
60
|
-
- Anything sent to or stored by an API that expects JSON.
|
|
61
|
-
- Data a downstream tool/parser must consume as strict JSON.
|
|
62
|
-
- Deeply nested config trees or highly irregular structures.
|
|
63
|
-
- Tiny payloads where the conversion overhead isn't worth it.
|
|
64
|
-
|
|
65
|
-
In those cases, still use `jq` to slice down to what you need — just skip the
|
|
66
|
-
`| toon` step.
|
|
67
|
-
|
|
68
|
-
## Worked examples
|
|
69
|
-
|
|
70
|
-
LLM model list (uniform array → great TOON candidate):
|
|
71
|
-
|
|
72
|
-
```bash
|
|
73
|
-
curl -s https://api.example.com/v1/models | jq '.data | map({id, owned_by, context})' | toon
|
|
74
|
-
```
|
|
75
|
-
```
|
|
76
|
-
[3]{id,owned_by,context}:
|
|
77
|
-
gpt-x,acme,128000
|
|
78
|
-
gpt-y,acme,200000
|
|
79
|
-
gpt-z,acme,1000000
|
|
80
|
-
```
|
|
81
|
-
|
|
82
|
-
OpenAPI paths summary (reshape first, then compress):
|
|
83
|
-
|
|
84
|
-
```bash
|
|
85
|
-
cat openapi.json \
|
|
86
|
-
| jq '[.paths | to_entries[] | {path: .key, methods: (.value | keys)}]' \
|
|
87
|
-
| toon --stats
|
|
88
|
-
```
|
|
89
|
-
|
|
90
|
-
Round-trip back to JSON for an API call:
|
|
91
|
-
|
|
92
|
-
```bash
|
|
93
|
-
RESHAPED=$(cat payload.toon | toon -d)
|
|
94
|
-
curl -s -X POST https://api.example.com/ingest -d "$RESHAPED"
|
|
95
|
-
```
|
|
96
|
-
|
|
97
|
-
## Checklist
|
|
98
|
-
1. **Query** — narrow with `jq` to the exact slice needed.
|
|
99
|
-
2. **Decide** — uniform/tabular/shallow → TOON; nested/array-of-arrays/contract → JSON.
|
|
100
|
-
3. **Compress** — `| toon` (add `--stats` to confirm savings).
|
|
101
|
-
4. **Round-trip** — `toon -d` only when strict JSON is required downstream.
|