@astrofoundry/pi-astro 0.8.0 → 0.9.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 +3 -2
- package/extensions/caveman/README.md +80 -0
- package/extensions/caveman/index.test.ts +253 -0
- package/extensions/caveman/index.ts +169 -0
- package/package.json +1 -1
- package/skills/caveman/SKILL.md +115 -0
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @astrofoundry/pi-astro
|
|
2
2
|
|
|
3
|
-
Personal customizations for the [pi coding agent](https://github.com/badlogic/pi-mono) — registers an `astro_agent` tool, a `grimoire` docs tool, a `claude-globals` rules-injection extension, 8 curated subagents,
|
|
3
|
+
Personal customizations for the [pi coding agent](https://github.com/badlogic/pi-mono) — registers an `astro_agent` tool, a `grimoire` docs tool, a `claude-globals` rules-injection extension, a `caveman` compressed-output toggle, 8 curated subagents, 4 skills, and an `astro` theme.
|
|
4
4
|
|
|
5
5
|
## Full setup
|
|
6
6
|
|
|
@@ -66,12 +66,13 @@ pi # launch; confirm [Extensions] lists astro-agents, claude-glob
|
|
|
66
66
|
- **macOS voice install (required once for the default):** open **System Settings → Accessibility → Spoken Content → System Voice → Manage Voices…**, expand **English**, check **Samantha (Enhanced)**, click **Done** to download (~500 MB – 1 GB). Verify with `say -v "Samantha (Enhanced)" hi`. If the voice is missing, `say` errors silently and you'll only hear the Glass sound.
|
|
67
67
|
- `vscode-image` — only active inside VS Code's integrated terminal (`TERM_PROGRAM=vscode`). Switches pi-tui's image output to the **Kitty graphics protocol** so images returned by tools like `gemini_image` render as real pixels instead of the `[Image: …]` text fallback. **Requires enabling `Terminal › Integrated: Enable Images` in VS Code settings** (off by default); restart the integrated terminal after flipping it. Disable with `PI_VSCODE_IMAGE_OFF=1`. `/vscode-image` prints current status.
|
|
68
68
|
- `claude-globals` — auto-injects `~/.claude/CLAUDE.md` into every pi session's system prompt
|
|
69
|
+
- `caveman` — `/caveman [lite|full|ultra|wenyan-lite|wenyan-full|wenyan-ultra|off|status]` toggles a persistent compressed-output mode. No argument toggles between off and the default level (`full`). Active level is shown as a footer badge and survives `/reload`. The skill body at `skills/caveman/SKILL.md` is also available as a one-shot via `/skill:caveman`.
|
|
69
70
|
|
|
70
71
|
**Bundled subagents** (callable via `astro_agent`):
|
|
71
72
|
- `code-reviewer`, `google-tech-lead`, `spec-writer`, `tester-api`, `tester-ui`, `ui-architect`, `ui-design-system`, `ui-frontend-developer`
|
|
72
73
|
|
|
73
74
|
**Skills** (loadable via `/skill:<name>`):
|
|
74
|
-
- `playwright-cli`, `postman-cli`, `raycast-script-creator`
|
|
75
|
+
- `playwright-cli`, `postman-cli`, `raycast-script-creator`, `caveman`
|
|
75
76
|
|
|
76
77
|
**Theme:**
|
|
77
78
|
- `astro` — dark, gold accent, GitHub-dark-inspired
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# caveman
|
|
2
|
+
|
|
3
|
+
Persistent compressed-output mode for the [pi coding agent](https://github.com/badlogic/pi-mono). Strips fillers, hedging, and connectives from assistant prose to cut output tokens by ~70-75% while preserving every technical fact. Code, error strings, command lines, paths, identifiers, and version numbers stay untouched.
|
|
4
|
+
|
|
5
|
+
Ships as part of [`@astrofoundry/pi-astro`](../../README.md). No extra install.
|
|
6
|
+
|
|
7
|
+
## Quick start
|
|
8
|
+
|
|
9
|
+
In any pi session:
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
/caveman
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Caveman is now on at level `full`. Type the same command again to switch it off. The active level is shown as a footer badge and survives `/reload`.
|
|
16
|
+
|
|
17
|
+
## Commands
|
|
18
|
+
|
|
19
|
+
| Command | Effect |
|
|
20
|
+
|---|---|
|
|
21
|
+
| `/caveman` | Toggle: off → `full`, on → off |
|
|
22
|
+
| `/caveman lite` | Activate at level `lite` |
|
|
23
|
+
| `/caveman full` | Activate at level `full` (default) |
|
|
24
|
+
| `/caveman ultra` | Activate at level `ultra` |
|
|
25
|
+
| `/caveman wenyan-lite` | Activate at level `wenyan-lite` |
|
|
26
|
+
| `/caveman wenyan-full` | Activate at level `wenyan-full` |
|
|
27
|
+
| `/caveman wenyan-ultra` | Activate at level `wenyan-ultra` |
|
|
28
|
+
| `/caveman off` | Deactivate |
|
|
29
|
+
| `/caveman status` | Print the current level |
|
|
30
|
+
|
|
31
|
+
Argument autocomplete is wired — type `/caveman ` and tab-cycle the levels.
|
|
32
|
+
|
|
33
|
+
You can also deactivate by saying "stop caveman" or "normal mode" in plain prose.
|
|
34
|
+
|
|
35
|
+
## Levels
|
|
36
|
+
|
|
37
|
+
Pick the trade-off between compression and readability:
|
|
38
|
+
|
|
39
|
+
| Level | Behaviour | Example answer to "Why does this React component re-render?" |
|
|
40
|
+
|---|---|---|
|
|
41
|
+
| `lite` | Drop fillers and pleasantries only. Full sentences, articles kept. | "Your component re-renders because every render creates a new object reference for the prop. Wrap it in `useMemo`." |
|
|
42
|
+
| `full` | Drop articles, fragments allowed. Default. | "New object ref each render. Inline obj prop = new ref = re-render. Wrap in `useMemo`." |
|
|
43
|
+
| `ultra` | Aggressive abbreviation, arrows for causality. | "Inline obj prop → new ref → re-render. `useMemo`." |
|
|
44
|
+
| `wenyan-lite` | Classical Chinese register, modern grammar. | "組件每次重繪皆生新對象參照。以 `useMemo` 包之。" |
|
|
45
|
+
| `wenyan-full` | Full 文言文, classical particles. | "每繪生新參照,故重繪。以 `useMemo` 包之。" |
|
|
46
|
+
| `wenyan-ultra` | Maximum classical compression. | "新參照→重繪。`useMemo` 包。" |
|
|
47
|
+
|
|
48
|
+
## What stays normal
|
|
49
|
+
|
|
50
|
+
Caveman never applies to:
|
|
51
|
+
|
|
52
|
+
- **Source code, commit messages, PR descriptions, code review comments** — written in normal prose.
|
|
53
|
+
- **Documentation files** (`.md`, `.mdx`, `.rst`) — caveman is for chat, not artifacts.
|
|
54
|
+
- **Quoted log lines, stack traces, error messages** — reproduced exactly.
|
|
55
|
+
|
|
56
|
+
Caveman also drops itself temporarily for safety:
|
|
57
|
+
|
|
58
|
+
- Security warnings.
|
|
59
|
+
- Confirmations for destructive or irreversible operations.
|
|
60
|
+
- Multi-step procedures whose order matters.
|
|
61
|
+
- When you ask a clarifying question or repeat yourself.
|
|
62
|
+
|
|
63
|
+
The mode resumes immediately after the clarified step is complete.
|
|
64
|
+
|
|
65
|
+
## One-shot vs persistent
|
|
66
|
+
|
|
67
|
+
Two ways to use it:
|
|
68
|
+
|
|
69
|
+
- **Persistent** — `/caveman` (or `/caveman <level>`). Stays on until you turn it off. Restored across `/reload` and session resume.
|
|
70
|
+
- **One-shot** — `/skill:caveman`. Loads the skill content for a single response, then the next reply returns to normal. Useful when you want a single terse answer without committing to a mode.
|
|
71
|
+
|
|
72
|
+
## State and persistence
|
|
73
|
+
|
|
74
|
+
State lives in the pi session itself — no flag files in your home directory, no shell scripts, no symlink risk. Each session keeps its own toggle, so two pi sessions in different projects can run different modes.
|
|
75
|
+
|
|
76
|
+
## Tips
|
|
77
|
+
|
|
78
|
+
- If you find caveman too clipped while debugging tricky logic, switch to `lite` for the duration: `/caveman lite`.
|
|
79
|
+
- Use `/caveman ultra` for short Q&A and debugging chains. Pair it with thinking levels and small models to keep latency low.
|
|
80
|
+
- The wenyan levels are useful when you genuinely want the classical register — they are not a joke language and require both you and the model to be comfortable reading 文言文.
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
type Handler = (event: unknown, ctx: unknown) => unknown | Promise<unknown>;
|
|
4
|
+
|
|
5
|
+
interface CommandOptions {
|
|
6
|
+
description?: string;
|
|
7
|
+
handler: (args: string, ctx: unknown) => Promise<void> | void;
|
|
8
|
+
getArgumentCompletions?: (prefix: string) => unknown;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface FakePi {
|
|
12
|
+
handlers: Record<string, Handler>;
|
|
13
|
+
commands: Map<string, CommandOptions>;
|
|
14
|
+
appendEntry: ReturnType<typeof vi.fn>;
|
|
15
|
+
on: (event: string, h: Handler) => void;
|
|
16
|
+
registerCommand: (name: string, opts: CommandOptions) => void;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function makePi(): FakePi {
|
|
20
|
+
const handlers: Record<string, Handler> = {};
|
|
21
|
+
const commands = new Map<string, CommandOptions>();
|
|
22
|
+
return {
|
|
23
|
+
handlers,
|
|
24
|
+
commands,
|
|
25
|
+
appendEntry: vi.fn(),
|
|
26
|
+
on(event, h) {
|
|
27
|
+
handlers[event] = h;
|
|
28
|
+
},
|
|
29
|
+
registerCommand(name, opts) {
|
|
30
|
+
commands.set(name, opts);
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface FakeCtx {
|
|
36
|
+
ui: {
|
|
37
|
+
notify: ReturnType<typeof vi.fn>;
|
|
38
|
+
setStatus: ReturnType<typeof vi.fn>;
|
|
39
|
+
};
|
|
40
|
+
sessionManager: { getEntries: () => unknown[] };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function makeCtx(entries: unknown[] = []): FakeCtx {
|
|
44
|
+
return {
|
|
45
|
+
ui: {
|
|
46
|
+
notify: vi.fn(),
|
|
47
|
+
setStatus: vi.fn(),
|
|
48
|
+
},
|
|
49
|
+
sessionManager: {
|
|
50
|
+
getEntries: () => entries,
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
describe("caveman extension", () => {
|
|
56
|
+
beforeEach(() => {
|
|
57
|
+
vi.resetModules();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
afterEach(() => {
|
|
61
|
+
vi.restoreAllMocks();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
async function load(): Promise<typeof import("./index.ts")> {
|
|
65
|
+
return await import("./index.ts");
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function install(): Promise<{ pi: FakePi; mod: Awaited<ReturnType<typeof load>> }> {
|
|
69
|
+
const mod = await load();
|
|
70
|
+
const pi = makePi();
|
|
71
|
+
mod.default(pi as unknown as Parameters<typeof mod.default>[0]);
|
|
72
|
+
return { pi, mod };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
it("registers /caveman command and the two lifecycle handlers", async () => {
|
|
76
|
+
const mod = await load();
|
|
77
|
+
const pi = makePi();
|
|
78
|
+
mod.default(pi as unknown as Parameters<typeof mod.default>[0]);
|
|
79
|
+
expect(pi.commands.has("caveman")).toBe(true);
|
|
80
|
+
expect(pi.handlers.session_start).toBeDefined();
|
|
81
|
+
expect(pi.handlers.before_agent_start).toBeDefined();
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("/caveman with no args toggles on to default level (full)", async () => {
|
|
85
|
+
const { pi } = await install();
|
|
86
|
+
const ctx = makeCtx();
|
|
87
|
+
await pi.commands.get("caveman")!.handler("", ctx);
|
|
88
|
+
expect(pi.appendEntry).toHaveBeenCalledWith("caveman-mode", { level: "full" });
|
|
89
|
+
expect(ctx.ui.setStatus).toHaveBeenCalledWith("caveman", expect.stringContaining("full"));
|
|
90
|
+
expect(ctx.ui.notify).toHaveBeenCalledWith("caveman: full", "info");
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("/caveman called twice with no args toggles back off", async () => {
|
|
94
|
+
const { pi } = await install();
|
|
95
|
+
const ctx = makeCtx();
|
|
96
|
+
await pi.commands.get("caveman")!.handler("", ctx);
|
|
97
|
+
await pi.commands.get("caveman")!.handler("", ctx);
|
|
98
|
+
expect(pi.appendEntry).toHaveBeenLastCalledWith("caveman-mode", { level: null });
|
|
99
|
+
expect(ctx.ui.setStatus).toHaveBeenLastCalledWith("caveman", undefined);
|
|
100
|
+
expect(ctx.ui.notify).toHaveBeenLastCalledWith("caveman: off", "info");
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it.each(["lite", "full", "ultra", "wenyan-lite", "wenyan-full", "wenyan-ultra"])(
|
|
104
|
+
"/caveman %s activates that level",
|
|
105
|
+
async (level) => {
|
|
106
|
+
const { pi } = await install();
|
|
107
|
+
const ctx = makeCtx();
|
|
108
|
+
await pi.commands.get("caveman")!.handler(level, ctx);
|
|
109
|
+
expect(pi.appendEntry).toHaveBeenCalledWith("caveman-mode", { level });
|
|
110
|
+
expect(ctx.ui.setStatus).toHaveBeenCalledWith("caveman", `🪨 caveman:${level}`);
|
|
111
|
+
expect(ctx.ui.notify).toHaveBeenCalledWith(`caveman: ${level}`, "info");
|
|
112
|
+
},
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
it("/caveman off clears status and persists null level", async () => {
|
|
116
|
+
const { pi } = await install();
|
|
117
|
+
const ctx = makeCtx();
|
|
118
|
+
await pi.commands.get("caveman")!.handler("ultra", ctx);
|
|
119
|
+
await pi.commands.get("caveman")!.handler("off", ctx);
|
|
120
|
+
expect(pi.appendEntry).toHaveBeenLastCalledWith("caveman-mode", { level: null });
|
|
121
|
+
expect(ctx.ui.setStatus).toHaveBeenLastCalledWith("caveman", undefined);
|
|
122
|
+
expect(ctx.ui.notify).toHaveBeenLastCalledWith("caveman: off", "info");
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("/caveman status reports current level without changing state", async () => {
|
|
126
|
+
const { pi } = await install();
|
|
127
|
+
const ctx = makeCtx();
|
|
128
|
+
await pi.commands.get("caveman")!.handler("ultra", ctx);
|
|
129
|
+
const persistedBefore = pi.appendEntry.mock.calls.length;
|
|
130
|
+
await pi.commands.get("caveman")!.handler("status", ctx);
|
|
131
|
+
expect(pi.appendEntry.mock.calls.length).toBe(persistedBefore);
|
|
132
|
+
expect(ctx.ui.notify).toHaveBeenLastCalledWith("caveman: ultra", "info");
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("/caveman status reports off when inactive", async () => {
|
|
136
|
+
const { pi } = await install();
|
|
137
|
+
const ctx = makeCtx();
|
|
138
|
+
await pi.commands.get("caveman")!.handler("status", ctx);
|
|
139
|
+
expect(pi.appendEntry).not.toHaveBeenCalled();
|
|
140
|
+
expect(ctx.ui.notify).toHaveBeenCalledWith("caveman: off", "info");
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("/caveman with an unknown argument warns and does not change state", async () => {
|
|
144
|
+
const { pi } = await install();
|
|
145
|
+
const ctx = makeCtx();
|
|
146
|
+
await pi.commands.get("caveman")!.handler("nonsense", ctx);
|
|
147
|
+
expect(pi.appendEntry).not.toHaveBeenCalled();
|
|
148
|
+
expect(ctx.ui.setStatus).not.toHaveBeenCalled();
|
|
149
|
+
expect(ctx.ui.notify).toHaveBeenCalledWith(expect.stringMatching(/unknown level/), "warning");
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it("argument autocomplete returns wenyan completions for 'wen'", async () => {
|
|
153
|
+
const { pi } = await install();
|
|
154
|
+
const completions = pi.commands.get("caveman")!.getArgumentCompletions!("wen");
|
|
155
|
+
expect(completions).toEqual(
|
|
156
|
+
expect.arrayContaining([
|
|
157
|
+
{ value: "wenyan-lite", label: "wenyan-lite" },
|
|
158
|
+
{ value: "wenyan-full", label: "wenyan-full" },
|
|
159
|
+
{ value: "wenyan-ultra", label: "wenyan-ultra" },
|
|
160
|
+
]),
|
|
161
|
+
);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it("argument autocomplete returns null when no candidate matches", async () => {
|
|
165
|
+
const { pi } = await install();
|
|
166
|
+
const completions = pi.commands.get("caveman")!.getArgumentCompletions!("zzz");
|
|
167
|
+
expect(completions).toBeNull();
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it("argument autocomplete includes off and status", async () => {
|
|
171
|
+
const { pi } = await install();
|
|
172
|
+
const completions = pi.commands.get("caveman")!.getArgumentCompletions!("");
|
|
173
|
+
expect(completions).toEqual(
|
|
174
|
+
expect.arrayContaining([
|
|
175
|
+
{ value: "off", label: "off" },
|
|
176
|
+
{ value: "status", label: "status" },
|
|
177
|
+
]),
|
|
178
|
+
);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it("session_start restores activeLevel from the latest custom entry", async () => {
|
|
182
|
+
const { pi } = await install();
|
|
183
|
+
const ctx = makeCtx([
|
|
184
|
+
{ type: "custom", customType: "caveman-mode", data: { level: "full" } },
|
|
185
|
+
{ type: "custom", customType: "caveman-mode", data: { level: "ultra" } },
|
|
186
|
+
]);
|
|
187
|
+
await pi.handlers.session_start({}, ctx);
|
|
188
|
+
expect(ctx.ui.setStatus).toHaveBeenCalledWith("caveman", "🪨 caveman:ultra");
|
|
189
|
+
const result = (await pi.handlers.before_agent_start(
|
|
190
|
+
{ systemPrompt: "ORIG" },
|
|
191
|
+
makeCtx(),
|
|
192
|
+
)) as { systemPrompt: string };
|
|
193
|
+
expect(result.systemPrompt).toContain("CAVEMAN MODE ACTIVE — level: ultra");
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it("session_start clears status when the latest entry persisted off", async () => {
|
|
197
|
+
const { pi } = await install();
|
|
198
|
+
const ctx = makeCtx([
|
|
199
|
+
{ type: "custom", customType: "caveman-mode", data: { level: "full" } },
|
|
200
|
+
{ type: "custom", customType: "caveman-mode", data: { level: null } },
|
|
201
|
+
]);
|
|
202
|
+
await pi.handlers.session_start({}, ctx);
|
|
203
|
+
expect(ctx.ui.setStatus).toHaveBeenCalledWith("caveman", undefined);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it("session_start ignores unrelated custom entries and corrupt level data", async () => {
|
|
207
|
+
const { pi } = await install();
|
|
208
|
+
const ctx = makeCtx([
|
|
209
|
+
{ type: "custom", customType: "other-extension", data: { level: "full" } },
|
|
210
|
+
{ type: "custom", customType: "caveman-mode", data: { level: "not-a-real-level" } },
|
|
211
|
+
]);
|
|
212
|
+
await pi.handlers.session_start({}, ctx);
|
|
213
|
+
expect(ctx.ui.setStatus).toHaveBeenCalledWith("caveman", undefined);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it("before_agent_start returns void when caveman is inactive", async () => {
|
|
217
|
+
const { pi } = await install();
|
|
218
|
+
const result = await pi.handlers.before_agent_start(
|
|
219
|
+
{ systemPrompt: "ORIG" },
|
|
220
|
+
makeCtx(),
|
|
221
|
+
);
|
|
222
|
+
expect(result).toBeUndefined();
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it("before_agent_start appends caveman ruleset filtered to the active level", async () => {
|
|
226
|
+
const { pi } = await install();
|
|
227
|
+
const ctx = makeCtx();
|
|
228
|
+
await pi.commands.get("caveman")!.handler("ultra", ctx);
|
|
229
|
+
const result = (await pi.handlers.before_agent_start(
|
|
230
|
+
{ systemPrompt: "ORIG" },
|
|
231
|
+
makeCtx(),
|
|
232
|
+
)) as { systemPrompt: string };
|
|
233
|
+
expect(result.systemPrompt.startsWith("ORIG\n\n")).toBe(true);
|
|
234
|
+
expect(result.systemPrompt).toContain("CAVEMAN MODE ACTIVE — level: ultra");
|
|
235
|
+
expect(result.systemPrompt).toContain("**ultra**");
|
|
236
|
+
expect(result.systemPrompt).not.toContain("**lite**");
|
|
237
|
+
expect(result.systemPrompt).not.toContain("**wenyan-lite**");
|
|
238
|
+
expect(result.systemPrompt).not.toContain("**wenyan-full**");
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it("before_agent_start keeps only the active level's worked-example bullets", async () => {
|
|
242
|
+
const { pi } = await install();
|
|
243
|
+
const ctx = makeCtx();
|
|
244
|
+
await pi.commands.get("caveman")!.handler("lite", ctx);
|
|
245
|
+
const result = (await pi.handlers.before_agent_start(
|
|
246
|
+
{ systemPrompt: "ORIG" },
|
|
247
|
+
makeCtx(),
|
|
248
|
+
)) as { systemPrompt: string };
|
|
249
|
+
expect(result.systemPrompt).toMatch(/^- lite:/m);
|
|
250
|
+
expect(result.systemPrompt).not.toMatch(/^- ultra:/m);
|
|
251
|
+
expect(result.systemPrompt).not.toMatch(/^- wenyan-full:/m);
|
|
252
|
+
});
|
|
253
|
+
});
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
5
|
+
|
|
6
|
+
const LEVELS = [
|
|
7
|
+
"lite",
|
|
8
|
+
"full",
|
|
9
|
+
"ultra",
|
|
10
|
+
"wenyan-lite",
|
|
11
|
+
"wenyan-full",
|
|
12
|
+
"wenyan-ultra",
|
|
13
|
+
] as const;
|
|
14
|
+
|
|
15
|
+
type Level = (typeof LEVELS)[number];
|
|
16
|
+
|
|
17
|
+
const DEFAULT_LEVEL: Level = "full";
|
|
18
|
+
const ENTRY_TYPE = "caveman-mode";
|
|
19
|
+
const STATUS_KEY = "caveman";
|
|
20
|
+
|
|
21
|
+
const extensionDir = path.dirname(fileURLToPath(import.meta.url));
|
|
22
|
+
const skillPath = path.resolve(
|
|
23
|
+
extensionDir,
|
|
24
|
+
"..",
|
|
25
|
+
"..",
|
|
26
|
+
"skills",
|
|
27
|
+
"caveman",
|
|
28
|
+
"SKILL.md",
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
interface PersistedState {
|
|
32
|
+
level: Level | null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function isLevel(value: unknown): value is Level {
|
|
36
|
+
return typeof value === "string" && (LEVELS as readonly string[]).includes(value);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function parseLevel(arg: string): Level | null {
|
|
40
|
+
return isLevel(arg) ? arg : null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function readSkillBody(): string {
|
|
44
|
+
const raw = fs.readFileSync(skillPath, "utf8");
|
|
45
|
+
return raw.replace(/^---[\s\S]*?---\s*/, "");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function filterForLevel(body: string, level: Level): string {
|
|
49
|
+
return body
|
|
50
|
+
.split("\n")
|
|
51
|
+
.reduce<string[]>((acc, line) => {
|
|
52
|
+
const tableRow = line.match(/^\|\s*\*\*([^*|]+?)\*\*\s*\|/);
|
|
53
|
+
if (tableRow) {
|
|
54
|
+
if (tableRow[1] === level) acc.push(line);
|
|
55
|
+
return acc;
|
|
56
|
+
}
|
|
57
|
+
const exampleBullet = line.match(/^- (\S+?):\s/);
|
|
58
|
+
if (exampleBullet) {
|
|
59
|
+
if (exampleBullet[1] === level) acc.push(line);
|
|
60
|
+
return acc;
|
|
61
|
+
}
|
|
62
|
+
acc.push(line);
|
|
63
|
+
return acc;
|
|
64
|
+
}, [])
|
|
65
|
+
.join("\n");
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function buildInjection(level: Level, body: string): string {
|
|
69
|
+
return `CAVEMAN MODE ACTIVE — level: ${level}\n\n${filterForLevel(body, level)}`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function statusText(level: Level): string {
|
|
73
|
+
return `🪨 caveman:${level}`;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const HELP =
|
|
77
|
+
"Usage: /caveman [lite|full|ultra|wenyan-lite|wenyan-full|wenyan-ultra|off|status]. " +
|
|
78
|
+
"No argument toggles between off and the default level (full).";
|
|
79
|
+
|
|
80
|
+
export default function cavemanExtension(pi: ExtensionAPI): void {
|
|
81
|
+
let activeLevel: Level | null = null;
|
|
82
|
+
let cachedBody: string | null = null;
|
|
83
|
+
|
|
84
|
+
function loadBody(): string {
|
|
85
|
+
if (cachedBody === null) {
|
|
86
|
+
cachedBody = readSkillBody();
|
|
87
|
+
}
|
|
88
|
+
return cachedBody;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function persist(): void {
|
|
92
|
+
pi.appendEntry<PersistedState>(ENTRY_TYPE, { level: activeLevel });
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
96
|
+
let restored: Level | null = null;
|
|
97
|
+
for (const entry of ctx.sessionManager.getEntries()) {
|
|
98
|
+
if (entry.type === "custom" && entry.customType === ENTRY_TYPE) {
|
|
99
|
+
const data = entry.data as PersistedState | undefined;
|
|
100
|
+
restored = data && isLevel(data.level) ? data.level : null;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
activeLevel = restored;
|
|
104
|
+
ctx.ui.setStatus(STATUS_KEY, activeLevel ? statusText(activeLevel) : undefined);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
pi.on("before_agent_start", async (event) => {
|
|
108
|
+
if (!activeLevel) return;
|
|
109
|
+
const body = loadBody();
|
|
110
|
+
return {
|
|
111
|
+
systemPrompt: `${event.systemPrompt}\n\n${buildInjection(activeLevel, body)}`,
|
|
112
|
+
};
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
pi.registerCommand("caveman", {
|
|
116
|
+
description: `Toggle compressed-output mode. ${HELP}`,
|
|
117
|
+
getArgumentCompletions: (prefix) => {
|
|
118
|
+
const items = [...LEVELS, "off", "status"];
|
|
119
|
+
const lower = prefix.toLowerCase();
|
|
120
|
+
const matches = items
|
|
121
|
+
.filter((value) => value.startsWith(lower))
|
|
122
|
+
.map((value) => ({ value, label: value }));
|
|
123
|
+
return matches.length > 0 ? matches : null;
|
|
124
|
+
},
|
|
125
|
+
handler: async (args, ctx) => {
|
|
126
|
+
const arg = args.trim().toLowerCase();
|
|
127
|
+
|
|
128
|
+
if (arg === "status") {
|
|
129
|
+
ctx.ui.notify(
|
|
130
|
+
activeLevel ? `caveman: ${activeLevel}` : "caveman: off",
|
|
131
|
+
"info",
|
|
132
|
+
);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (arg === "off") {
|
|
137
|
+
activeLevel = null;
|
|
138
|
+
persist();
|
|
139
|
+
ctx.ui.setStatus(STATUS_KEY, undefined);
|
|
140
|
+
ctx.ui.notify("caveman: off", "info");
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (arg === "") {
|
|
145
|
+
activeLevel = activeLevel ? null : DEFAULT_LEVEL;
|
|
146
|
+
} else {
|
|
147
|
+
const level = parseLevel(arg);
|
|
148
|
+
if (!level) {
|
|
149
|
+
ctx.ui.notify(
|
|
150
|
+
`caveman: unknown level "${arg}". ${HELP}`,
|
|
151
|
+
"warning",
|
|
152
|
+
);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
activeLevel = level;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
persist();
|
|
159
|
+
ctx.ui.setStatus(
|
|
160
|
+
STATUS_KEY,
|
|
161
|
+
activeLevel ? statusText(activeLevel) : undefined,
|
|
162
|
+
);
|
|
163
|
+
ctx.ui.notify(
|
|
164
|
+
activeLevel ? `caveman: ${activeLevel}` : "caveman: off",
|
|
165
|
+
"info",
|
|
166
|
+
);
|
|
167
|
+
},
|
|
168
|
+
});
|
|
169
|
+
}
|
package/package.json
CHANGED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: caveman
|
|
3
|
+
description: >
|
|
4
|
+
Persistent compressed-output mode. Strips fillers, hedging, and connectives
|
|
5
|
+
from assistant prose to cut output tokens by ~70-75% while preserving every
|
|
6
|
+
technical fact. Six intensity levels: lite, full (default), ultra, wenyan-lite,
|
|
7
|
+
wenyan-full, wenyan-ultra. Activate with /caveman or /skill:caveman; deactivate
|
|
8
|
+
with /caveman off, "stop caveman", or "normal mode".
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# Caveman Mode
|
|
12
|
+
|
|
13
|
+
Output compression via principled prose-stripping. Substance is preserved
|
|
14
|
+
verbatim — only redundancy and social padding are removed. Code, error
|
|
15
|
+
strings, command lines, paths, identifiers, and version numbers are reproduced
|
|
16
|
+
unchanged.
|
|
17
|
+
|
|
18
|
+
## Persistence
|
|
19
|
+
|
|
20
|
+
While active, **every** assistant response uses caveman compression. Do not
|
|
21
|
+
drift back to verbose prose mid-conversation, even after long context, after
|
|
22
|
+
context compaction, or after style instructions injected by other extensions.
|
|
23
|
+
Resume caveman immediately after any unavoidable break (security warning,
|
|
24
|
+
destructive-action confirmation, multi-step procedure).
|
|
25
|
+
|
|
26
|
+
The level set by `/caveman <level>` persists until changed or session end.
|
|
27
|
+
Switch off only on explicit user request: "stop caveman", "normal mode", or
|
|
28
|
+
`/caveman off`.
|
|
29
|
+
|
|
30
|
+
## Compression rules
|
|
31
|
+
|
|
32
|
+
Drop:
|
|
33
|
+
|
|
34
|
+
- Articles when grammatically optional (a, an, the).
|
|
35
|
+
- Filler adverbs and intensifiers (just, really, basically, actually, simply, very).
|
|
36
|
+
- Pleasantries (sure, certainly, of course, happy to, let me, I'd be glad to).
|
|
37
|
+
- Hedging phrases (it seems, perhaps, you might want to, I think).
|
|
38
|
+
- Restating the question before answering it.
|
|
39
|
+
|
|
40
|
+
Keep:
|
|
41
|
+
|
|
42
|
+
- Every technical fact, parameter name, file path, command, and version number.
|
|
43
|
+
- Code blocks verbatim. Error messages quoted exactly.
|
|
44
|
+
- Cause-and-effect order — never reorder steps for terseness.
|
|
45
|
+
- Punctuation needed for parsing (commas separating list items, periods between independent claims).
|
|
46
|
+
|
|
47
|
+
Sentence pattern: `<subject> <action> <reason>. <next step>.`
|
|
48
|
+
|
|
49
|
+
Anti-example: "Sure! I'd be happy to help you with that. The issue you're seeing is most likely caused by..."
|
|
50
|
+
Example: "Bug in auth middleware. Token expiry uses `<` not `<=`. Fix:"
|
|
51
|
+
|
|
52
|
+
## Intensity levels
|
|
53
|
+
|
|
54
|
+
| Level | Behaviour |
|
|
55
|
+
|---|---|
|
|
56
|
+
| **lite** | Drop fillers and pleasantries only. Keep articles + full sentences. Professional but tight. |
|
|
57
|
+
| **full** | Drop articles. Sentence fragments allowed. Short synonyms (fix not "implement a solution for"). Default. |
|
|
58
|
+
| **ultra** | Abbreviate (DB, auth, cfg, req, res, fn, impl). Replace conjunctions with arrows for causality (X → Y). Single word when sufficient. |
|
|
59
|
+
| **wenyan-lite** | Classical Chinese register, modern grammar. Drop fillers and hedging. |
|
|
60
|
+
| **wenyan-full** | Full 文言文 — verb precedes object, omit subject when implied, classical particles (之 / 乃 / 為 / 其). 80–90% character reduction. |
|
|
61
|
+
| **wenyan-ultra** | Maximum classical compression. One classical phrase per thought. |
|
|
62
|
+
|
|
63
|
+
### Worked example — "Why does this React component re-render on every parent update?"
|
|
64
|
+
|
|
65
|
+
- lite: "Your component re-renders because every render creates a new object reference for the prop. Wrap it in `useMemo`."
|
|
66
|
+
- full: "New object ref each render. Inline obj prop = new ref = re-render. Wrap in `useMemo`."
|
|
67
|
+
- ultra: "Inline obj prop → new ref → re-render. `useMemo`."
|
|
68
|
+
- wenyan-lite: "組件每次重繪皆生新對象參照。以 `useMemo` 包之。"
|
|
69
|
+
- wenyan-full: "每繪生新參照,故重繪。以 `useMemo` 包之。"
|
|
70
|
+
- wenyan-ultra: "新參照→重繪。`useMemo` 包。"
|
|
71
|
+
|
|
72
|
+
### Worked example — "Explain database connection pooling."
|
|
73
|
+
|
|
74
|
+
- lite: "Connection pooling reuses already-open connections instead of opening a new one per request, avoiding handshake overhead."
|
|
75
|
+
- full: "Pool reuse open connections. No new handshake per request."
|
|
76
|
+
- ultra: "Pool = reuse conn. Skip handshake → fast under load."
|
|
77
|
+
- wenyan-lite: "連接池重用已開連接,免逐請求重握之耗。"
|
|
78
|
+
- wenyan-full: "池存連接而重用,無逐請求重握之耗。"
|
|
79
|
+
- wenyan-ultra: "池連,免握。"
|
|
80
|
+
|
|
81
|
+
## Auto-clarity (override caveman temporarily)
|
|
82
|
+
|
|
83
|
+
Drop caveman compression when ambiguity would harm correctness:
|
|
84
|
+
|
|
85
|
+
- Security warnings.
|
|
86
|
+
- Confirmations for destructive or irreversible operations.
|
|
87
|
+
- Multi-step procedures whose order matters and where fragments would invite misreading.
|
|
88
|
+
- The user repeats a question or asks for clarification.
|
|
89
|
+
|
|
90
|
+
Use full prose for the duration of the override. Resume caveman immediately
|
|
91
|
+
after the clarified step is complete.
|
|
92
|
+
|
|
93
|
+
Example — destructive operation:
|
|
94
|
+
|
|
95
|
+
> **Warning:** the next command permanently deletes every row in `users` and
|
|
96
|
+
> cannot be undone:
|
|
97
|
+
>
|
|
98
|
+
> ```sql
|
|
99
|
+
> DROP TABLE users;
|
|
100
|
+
> ```
|
|
101
|
+
>
|
|
102
|
+
> Verify a current backup exists before running. Caveman resume.
|
|
103
|
+
|
|
104
|
+
## Boundaries (caveman never applies)
|
|
105
|
+
|
|
106
|
+
- Source code, commit messages, PR descriptions, code review comments — write in normal prose.
|
|
107
|
+
- Documentation files (`.md`, `.mdx`, `.rst`) — caveman is for chat, not artifacts.
|
|
108
|
+
- Quoted log lines, stack traces, error messages — reproduce exactly.
|
|
109
|
+
|
|
110
|
+
## Deactivation
|
|
111
|
+
|
|
112
|
+
- `/caveman off` — explicit slash command.
|
|
113
|
+
- "stop caveman" / "normal mode" / "disable caveman" — natural-language requests.
|
|
114
|
+
|
|
115
|
+
After deactivation, return to normal prose immediately on the next response.
|