pi-caveman 1.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,91 @@
1
+ # 🪨 pi-caveman
2
+
3
+ **Why use many token when few do trick.**
4
+
5
+ A [pi](https://github.com/mariozechner/pi) extension that cuts **~75% of output tokens** while keeping full technical accuracy. Based on [caveman](https://github.com/JuliusBrussee/caveman) by [Julius Brussee](https://github.com/JuliusBrussee).
6
+
7
+ <table>
8
+ <tr>
9
+ <td width="50%">
10
+
11
+ ### 🗣️ Normal (69 tokens)
12
+
13
+ > "The reason your React component is re-rendering is likely because you're creating a new object reference on each render cycle. When you pass an inline object as a prop, React's shallow comparison sees it as a different object every time, which triggers a re-render. I'd recommend using useMemo to memoize the object."
14
+
15
+ </td>
16
+ <td width="50%">
17
+
18
+ ### 🪨 Caveman (19 tokens)
19
+
20
+ > "New object ref each render. Inline object prop = new ref = re-render. Wrap in `useMemo`."
21
+
22
+ </td>
23
+ </tr>
24
+ </table>
25
+
26
+ ## Install
27
+
28
+ ```bash
29
+ pi install git:github.com/jonjonrankin/pi-caveman
30
+ ```
31
+
32
+ ## Usage
33
+
34
+ ### Toggle Mode
35
+
36
+ ```
37
+ /caveman Toggle on (full) / off
38
+ /caveman lite Professional, no fluff
39
+ /caveman full Classic caveman (default)
40
+ /caveman ultra Maximum compression
41
+ /caveman wenyan-lite Semi-classical Chinese
42
+ /caveman wenyan Full 文言文
43
+ /caveman wenyan-ultra Extreme 文言文
44
+ /caveman off Disable
45
+ ```
46
+
47
+ ### Settings
48
+
49
+ ```
50
+ /caveman config Open settings dialog
51
+ ```
52
+
53
+ The config dialog lets you:
54
+
55
+ - **Default level** — Set a level that activates automatically on every new session (e.g. `full` to always start in caveman mode)
56
+ - **Show status bar** — Toggle the animated campfire indicator in the footer
57
+
58
+ Settings are saved to `~/.pi/agent/caveman.json` and persist across all sessions.
59
+
60
+ ### Status Bar
61
+
62
+ When active, an animated campfire flickers in the footer using colored braille characters:
63
+
64
+ ```
65
+ ⠠⠄ → ⠔⠂ → ⠊⠑ → ... colored campfire embers (speed varies by level)
66
+ ```
67
+
68
+ Colors cycle through red → orange → yellow → white-hot → ember using ANSI 256-color. Speed scales with intensity: 300ms for lite, 200ms for full, 100ms for ultra.
69
+
70
+ ## Levels
71
+
72
+ | Level | Style | Example |
73
+ |-------|-------|---------|
74
+ | **Lite** | No filler. Full sentences. Professional but tight. | "Your component re-renders because you create a new object reference each render." |
75
+ | **Full** | Drop articles, fragments OK. Classic caveman. | "New object ref each render. Wrap in `useMemo`." |
76
+ | **Ultra** | Abbreviations, arrows, maximum compression. | "Inline obj prop → new ref → re-render. `useMemo`." |
77
+ | **文言文 Lite** | Semi-classical Chinese, grammar intact. | "組件頻重繪,以每繪新生對象參照故。" |
78
+ | **文言文** | Full classical terseness. | "物出新參照,致重繪。useMemo Wrap之。" |
79
+ | **文言文 Ultra** | Extreme classical compression. | "新參照→重繪。useMemo Wrap。" |
80
+
81
+ ## How It Works
82
+
83
+ The extension hooks `before_agent_start` to append caveman communication rules to the system prompt at the selected intensity. Within a session, the active level is stored as a custom session entry and restored on resume. Across sessions, persistent config (`~/.pi/agent/caveman.json`) provides the default level and status bar preference. Auto-clarity rules tell the model to drop caveman mode for security warnings or irreversible actions.
84
+
85
+ ## Credits
86
+
87
+ Based on [caveman](https://github.com/JuliusBrussee/caveman) by [Julius Brussee](https://github.com/JuliusBrussee).
88
+
89
+ ## License
90
+
91
+ MIT
@@ -0,0 +1,310 @@
1
+ /**
2
+ * pi-caveman — why use many token when few do trick
3
+ *
4
+ * A pi extension that cuts ~75% of output tokens while keeping full technical
5
+ * accuracy. Based on https://github.com/JuliusBrussee/caveman
6
+ *
7
+ * Commands:
8
+ * /caveman [level] Toggle caveman mode or set intensity
9
+ * /caveman config Open settings dialog (default level, status bar toggle)
10
+ */
11
+
12
+ import { readFile, writeFile, mkdir } from "node:fs/promises";
13
+ import { homedir } from "node:os";
14
+ import { join } from "node:path";
15
+ import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
16
+ import { getSettingsListTheme } from "@mariozechner/pi-coding-agent";
17
+ import { Container, type SettingItem, SettingsList, Text } from "@mariozechner/pi-tui";
18
+
19
+ // ---------------------------------------------------------------------------
20
+ // Levels
21
+ // ---------------------------------------------------------------------------
22
+
23
+ const LEVELS = ["off", "lite", "full", "ultra", "wenyan-lite", "wenyan", "wenyan-ultra"] as const;
24
+ type Level = (typeof LEVELS)[number];
25
+
26
+ // ---------------------------------------------------------------------------
27
+ // Persistent config (survives across sessions)
28
+ // ---------------------------------------------------------------------------
29
+
30
+ interface CavemanConfig {
31
+ /** Level to apply on new sessions. "off" means don't auto-enable. */
32
+ defaultLevel: Level;
33
+ /** Whether to show the animated footer status. */
34
+ showStatus: boolean;
35
+ }
36
+
37
+ const CONFIG_PATH = join(homedir(), ".pi", "agent", "caveman.json");
38
+ const DEFAULT_CONFIG: CavemanConfig = { defaultLevel: "off", showStatus: true };
39
+
40
+ async function loadConfig(): Promise<CavemanConfig> {
41
+ try {
42
+ const raw = await readFile(CONFIG_PATH, "utf8");
43
+ const parsed = JSON.parse(raw);
44
+ return {
45
+ defaultLevel: LEVELS.includes(parsed.defaultLevel) ? parsed.defaultLevel : DEFAULT_CONFIG.defaultLevel,
46
+ showStatus: typeof parsed.showStatus === "boolean" ? parsed.showStatus : DEFAULT_CONFIG.showStatus,
47
+ };
48
+ } catch {
49
+ return { ...DEFAULT_CONFIG };
50
+ }
51
+ }
52
+
53
+ async function saveConfig(config: CavemanConfig): Promise<void> {
54
+ await mkdir(join(homedir(), ".pi", "agent"), { recursive: true });
55
+ await writeFile(CONFIG_PATH, JSON.stringify(config, null, 2) + "\n", "utf8");
56
+ }
57
+
58
+ // ---------------------------------------------------------------------------
59
+ // Animated status bar — campfire with 256-color fire palette
60
+ // ---------------------------------------------------------------------------
61
+
62
+ interface Animation {
63
+ frames: string[];
64
+ label: string;
65
+ /** ms between frames */
66
+ interval: number;
67
+ }
68
+
69
+ const R = "\x1b[38;5;196m"; // red
70
+ const O = "\x1b[38;5;208m"; // orange
71
+ const Y = "\x1b[38;5;220m"; // yellow
72
+ const W = "\x1b[38;5;230m"; // white-hot
73
+ const E = "\x1b[38;5;52m"; // ember (dark red)
74
+ const X = "\x1b[0m"; // reset
75
+
76
+ const FIRE_FRAMES = [
77
+ `${R}⠠${O}⠄${X}`,
78
+ `${O}⠔${Y}⠂${X}`,
79
+ `${Y}⠊${W}⠑${X}`,
80
+ `${W}⠑${Y}⠊${X}`,
81
+ `${Y}⠂${O}⠔${X}`,
82
+ `${O}⠄${R}⠠${X}`,
83
+ `${R}⠠${E}⠄${X}`,
84
+ `${E}⠔${R}⠂${X}`,
85
+ ];
86
+
87
+ const ANIMATIONS: Record<Exclude<Level, "off">, Animation> = {
88
+ lite: { frames: FIRE_FRAMES, label: "LITE", interval: 300 },
89
+ full: { frames: FIRE_FRAMES, label: "CAVEMAN", interval: 200 },
90
+ ultra: { frames: FIRE_FRAMES, label: "ULTRA", interval: 100 },
91
+ "wenyan-lite": { frames: FIRE_FRAMES, label: "文言", interval: 300 },
92
+ wenyan: { frames: FIRE_FRAMES, label: "文言文", interval: 200 },
93
+ "wenyan-ultra":{ frames: FIRE_FRAMES, label: "文言文極", interval: 100 },
94
+ };
95
+
96
+ // ---------------------------------------------------------------------------
97
+ // System prompt fragments
98
+ // ---------------------------------------------------------------------------
99
+
100
+ const BASE = `\
101
+ IMPORTANT: You are in CAVEMAN MODE. Respond terse like smart caveman. \
102
+ All technical substance stay. Only fluff die.
103
+
104
+ Rules:
105
+ - Drop articles (a/an/the), filler (just/really/basically/actually/simply), \
106
+ pleasantries, hedging
107
+ - Fragments OK. Short synonyms preferred. Technical terms exact
108
+ - Code blocks unchanged. Errors quoted exact
109
+ - Pattern: [thing] [action] [reason]. [next step].
110
+
111
+ Bad: "Sure! I'd be happy to help you with that. The issue you're experiencing is likely caused by..."
112
+ Good: "Bug in auth middleware. Token expiry check use \`<\` not \`<=\`. Fix:"`;
113
+
114
+ const INTENSITY: Record<Exclude<Level, "off">, string> = {
115
+ lite: `\
116
+ Level: LITE — No filler/hedging. Keep articles + full sentences. Professional but tight.
117
+ Example: "Your component re-renders because you create a new object reference each render. Wrap it in \`useMemo\`."`,
118
+
119
+ full: `\
120
+ Level: FULL — Drop articles, fragments OK, short synonyms. Classic caveman.
121
+ Example: "New object ref each render. Inline object prop = new ref = re-render. Wrap in \`useMemo\`."`,
122
+
123
+ ultra: `\
124
+ Level: ULTRA — Abbreviate (DB/auth/config/req/res/fn/impl), strip conjunctions, arrows for causality (X → Y).
125
+ Example: "Inline obj prop → new ref → re-render. \`useMemo\`."`,
126
+
127
+ "wenyan-lite": `\
128
+ Level: 文言文 LITE — Semi-classical Chinese. Grammar intact, filler gone. Technical terms in English.
129
+ Example: "組件頻重繪,以每繪新生對象參照故。以 useMemo 包之。"`,
130
+
131
+ wenyan: `\
132
+ Level: 文言文 FULL — Maximum classical terseness. 80-90% character reduction. Technical terms in English.
133
+ Example: "物出新參照,致重繪。useMemo Wrap之。"`,
134
+
135
+ "wenyan-ultra": `\
136
+ Level: 文言文 ULTRA — Extreme classical compression. Technical terms in English.
137
+ Example: "新參照→重繪。useMemo Wrap。"`,
138
+ };
139
+
140
+ const SAFETY = `\
141
+ Auto-clarity: drop caveman for security warnings, irreversible action confirmations, \
142
+ or when user is confused. Resume after.
143
+ Boundaries: write normal code. Only compress explanations. "stop caveman" or "normal mode" reverts.`;
144
+
145
+ // ---------------------------------------------------------------------------
146
+ // Extension
147
+ // ---------------------------------------------------------------------------
148
+
149
+ export default function caveman(pi: ExtensionAPI) {
150
+ let level: Level = "off";
151
+ let config: CavemanConfig = { ...DEFAULT_CONFIG };
152
+ let timer: ReturnType<typeof setInterval> | null = null;
153
+ let frameIndex = 0;
154
+
155
+ // -- Animation helpers --
156
+
157
+ function stopAnimation() {
158
+ if (timer) {
159
+ clearInterval(timer);
160
+ timer = null;
161
+ }
162
+ frameIndex = 0;
163
+ }
164
+
165
+ function syncStatus(ctx: Pick<ExtensionContext, "ui">) {
166
+ stopAnimation();
167
+ const theme = ctx.ui.theme;
168
+
169
+ if (level === "off" || !config.showStatus) {
170
+ ctx.ui.setStatus("caveman", "");
171
+ return;
172
+ }
173
+
174
+ const anim = ANIMATIONS[level];
175
+
176
+ const renderFrame = () => {
177
+ const icon = anim.frames[frameIndex % anim.frames.length]!;
178
+ ctx.ui.setStatus("caveman", icon + " " + theme.fg("muted", "caveman level: ") + theme.fg("text", anim.label));
179
+ frameIndex++;
180
+ };
181
+
182
+ renderFrame();
183
+ timer = setInterval(renderFrame, anim.interval);
184
+ }
185
+
186
+ // -- Restore state on session load --
187
+
188
+ pi.on("session_start", async (_event, ctx) => {
189
+ config = await loadConfig();
190
+
191
+ // Check for session-level override first (resuming a session)
192
+ let sessionLevel: Level | null = null;
193
+ for (const entry of ctx.sessionManager.getEntries()) {
194
+ if (entry.type === "custom" && entry.customType === "caveman-level") {
195
+ sessionLevel = (entry.data as { level: Level })?.level ?? null;
196
+ }
197
+ }
198
+
199
+ if (sessionLevel !== null) {
200
+ // Resuming — use session state
201
+ level = sessionLevel;
202
+ } else if (config.defaultLevel !== "off") {
203
+ // New session — apply default from config
204
+ level = config.defaultLevel;
205
+ pi.appendEntry("caveman-level", { level });
206
+ }
207
+
208
+ syncStatus(ctx);
209
+ });
210
+
211
+ pi.on("session_shutdown", async () => {
212
+ stopAnimation();
213
+ });
214
+
215
+ // -- /caveman command --
216
+
217
+ pi.registerCommand("caveman", {
218
+ description: "Toggle caveman mode, set level, or 'config' to open settings",
219
+ handler: async (args, ctx) => {
220
+ const arg = args?.trim().toLowerCase();
221
+
222
+ // Open config dialog
223
+ if (arg === "config") {
224
+ await openConfig(ctx);
225
+ return;
226
+ }
227
+
228
+ if (!arg) {
229
+ level = level === "off" ? "full" : "off";
230
+ } else if (LEVELS.includes(arg as Level)) {
231
+ level = arg as Level;
232
+ } else {
233
+ ctx.ui.notify(`Unknown: "${arg}". Use: ${LEVELS.join(", ")} or config`, "error");
234
+ return;
235
+ }
236
+
237
+ pi.appendEntry("caveman-level", { level });
238
+ syncStatus(ctx);
239
+
240
+ ctx.ui.notify(
241
+ level === "off" ? "Caveman mode off." : `Caveman: ${ANIMATIONS[level].label}`,
242
+ "info",
243
+ );
244
+ },
245
+ });
246
+
247
+ // -- /caveman config: interactive SettingsList --
248
+
249
+ async function openConfig(ctx: ExtensionContext) {
250
+ config = await loadConfig();
251
+
252
+ await ctx.ui.custom((_tui, theme, _kb, done) => {
253
+ const items: SettingItem[] = [
254
+ {
255
+ id: "defaultLevel",
256
+ label: "Default level for new sessions",
257
+ currentValue: config.defaultLevel,
258
+ values: [...LEVELS],
259
+ },
260
+ {
261
+ id: "showStatus",
262
+ label: "Show animated status bar",
263
+ currentValue: config.showStatus ? "on" : "off",
264
+ values: ["on", "off"],
265
+ },
266
+ ];
267
+
268
+ const container = new Container();
269
+ container.addChild(new Text(theme.fg("accent", theme.bold(" Caveman Config")), 0, 0));
270
+ container.addChild(new Text(theme.fg("dim", " Saved to ~/.pi/agent/caveman.json"), 0, 0));
271
+ container.addChild(new Text("", 0, 0));
272
+
273
+ const settingsList = new SettingsList(
274
+ items,
275
+ Math.min(items.length + 2, 10),
276
+ getSettingsListTheme(),
277
+ (id, newValue) => {
278
+ if (id === "defaultLevel" && LEVELS.includes(newValue as Level)) {
279
+ config.defaultLevel = newValue as Level;
280
+ } else if (id === "showStatus") {
281
+ config.showStatus = newValue === "on";
282
+ }
283
+ saveConfig(config);
284
+ syncStatus(ctx);
285
+ },
286
+ () => done(undefined),
287
+ );
288
+
289
+ container.addChild(settingsList);
290
+ container.addChild(new Text(theme.fg("dim", " ←→ change • esc close"), 0, 0));
291
+
292
+ return {
293
+ render: (w: number) => container.render(w),
294
+ invalidate: () => container.invalidate(),
295
+ handleInput: (data: string) => {
296
+ settingsList.handleInput?.(data);
297
+ },
298
+ };
299
+ });
300
+ }
301
+
302
+ // -- Inject caveman rules into system prompt --
303
+
304
+ pi.on("before_agent_start", async (event) => {
305
+ if (level === "off") return;
306
+ return {
307
+ systemPrompt: `${event.systemPrompt}\n\n${BASE}\n\n${INTENSITY[level]}\n\n${SAFETY}`,
308
+ };
309
+ });
310
+ }
package/package.json ADDED
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "pi-caveman",
3
+ "version": "1.0.0",
4
+ "description": "Why use many token when few do trick. Caveman mode for pi — cuts ~75% output tokens while keeping full technical accuracy.",
5
+ "keywords": ["pi-package", "pi-extension", "caveman", "token-saving"],
6
+ "license": "MIT",
7
+ "author": "jonjonrankin",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/jonjonrankin/pi-caveman.git"
11
+ },
12
+ "homepage": "https://github.com/jonjonrankin/pi-caveman",
13
+ "files": [
14
+ "extensions",
15
+ "README.md",
16
+ "LICENSE"
17
+ ],
18
+ "pi": {
19
+ "extensions": ["./extensions"]
20
+ },
21
+ "peerDependencies": {
22
+ "@mariozechner/pi-coding-agent": "*"
23
+ }
24
+ }