pi-caveman 1.0.2 → 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -45,7 +45,10 @@ pi install git:github.com/jonjonrankin/pi-caveman
45
45
  /caveman wenyan-lite Semi-classical Chinese
46
46
  /caveman wenyan Full 文言文
47
47
  /caveman wenyan-ultra Extreme 文言文
48
+ /caveman micro Experimental prompt-minimized mode
48
49
  /caveman off Disable
50
+ /caveman stop Disable (alias)
51
+ /caveman quit Disable (alias)
49
52
  ```
50
53
 
51
54
  ### Settings
@@ -63,24 +66,19 @@ Settings are saved to `~/.pi/agent/caveman.json` and persist across all sessions
63
66
 
64
67
  ### Status Bar
65
68
 
66
- When active, an animated campfire flickers in the footer using colored braille characters:
67
-
68
- ```
69
- ⠠⠄ → ⠔⠂ → ⠊⠑ → ... colored campfire embers (speed varies by level)
70
- ```
71
-
72
- 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
+ When active, a status bar displays caveman level and an animated campfire flickers in the footer using colored braille characters. This can be disabled in the `/caveman config` menu.
73
70
 
74
71
  ## Levels
75
72
 
76
- | Level | Style | Example |
77
- |-------|-------|---------|
78
- | **Lite** | No filler. Full sentences. Professional but tight. | "Your component re-renders because you create a new object reference each render." |
79
- | **Full** | Drop articles, fragments OK. Classic caveman. | "New object ref each render. Wrap in `useMemo`." |
80
- | **Ultra** | Abbreviations, arrows, maximum compression. | "Inline obj prop → new ref → re-render. `useMemo`." |
81
- | **文言文 Lite** | Semi-classical Chinese, grammar intact. | "組件頻重繪,以每繪新生對象參照故。" |
82
- | **文言文** | Full classical terseness. | "物出新參照,致重繪。useMemo Wrap之。" |
83
- | **文言文 Ultra** | Extreme classical compression. | "新參照→重繪。useMemo Wrap。" |
73
+ | Level | Style | Example |
74
+ | -------------------------- | -------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- |
75
+ | **Lite** | No filler. Full sentences. Professional but tight. | "Your component re-renders because you create a new object reference each render." |
76
+ | **Full** | Drop articles, fragments OK. Classic caveman. | "New object ref each render. Wrap in `useMemo`." |
77
+ | **Ultra** | Abbreviations, arrows, maximum compression. | "Inline obj prop → new ref → re-render. `useMemo`." |
78
+ | **文言文 Lite** | Semi-classical Chinese, grammar intact. | "組件頻重繪,以每繪新生對象參照故。" |
79
+ | **文言文** | Full classical terseness. | "物出新參照,致重繪。useMemo Wrap之。" |
80
+ | **文言文 Ultra** | Extreme classical compression. | "新參照→重繪。useMemo Wrap。" |
81
+ | **Micro** _(experimental)_ | Minimal prompt that reduces size of caveman prompt itself. Drops filler, pleasantries, hedging, keeps technical substance. | "Bug in auth middleware. Token expiry check use `<` not `<=`. Fix:" |
84
82
 
85
83
  ## How It Works
86
84
 
@@ -90,6 +88,8 @@ The extension hooks `before_agent_start` to append caveman communication rules t
90
88
 
91
89
  Based on [caveman](https://github.com/JuliusBrussee/caveman) by [Julius Brussee](https://github.com/JuliusBrussee).
92
90
 
91
+ `micro` mode prompt based on [caveman-micro](https://github.com/kuba-guzik/caveman-micro) by [Kuba Guzik](https://github.com/kuba-guzik).
92
+
93
93
  ## License
94
94
 
95
95
  MIT
@@ -6,6 +6,7 @@
6
6
  *
7
7
  * Commands:
8
8
  * /caveman [level] Toggle caveman mode or set intensity
9
+ * /caveman stop Disable caveman mode (aliases: off, quit)
9
10
  * /caveman config Open settings dialog (default level, status bar toggle)
10
11
  */
11
12
 
@@ -20,9 +21,24 @@ import { Container, type SettingItem, SettingsList, Text } from "@mariozechner/p
20
21
  // Levels
21
22
  // ---------------------------------------------------------------------------
22
23
 
23
- const LEVELS = ["off", "lite", "full", "ultra", "wenyan-lite", "wenyan", "wenyan-ultra"] as const;
24
+ const LEVELS = ["off", "lite", "full", "ultra", "wenyan-lite", "wenyan", "wenyan-ultra", "micro"] as const;
25
+ const STOP_ALIASES = new Set(["off", "stop", "quit"]);
24
26
  type Level = (typeof LEVELS)[number];
25
27
 
28
+ const CAVEMAN_COMMAND_OPTIONS = [
29
+ { value: "lite", label: "lite", description: "Professional, no fluff" },
30
+ { value: "full", label: "full", description: "Classic caveman" },
31
+ { value: "ultra", label: "ultra", description: "Maximum compression" },
32
+ { value: "wenyan-lite", label: "wenyan-lite", description: "Semi-classical Chinese" },
33
+ { value: "wenyan", label: "wenyan", description: "Full 文言文" },
34
+ { value: "wenyan-ultra", label: "wenyan-ultra", description: "Extreme 文言文" },
35
+ { value: "micro", label: "micro", description: "Experimental prompt-minimized mode" },
36
+ { value: "off", label: "off", description: "Disable caveman mode" },
37
+ { value: "stop", label: "stop", description: "Disable caveman mode" },
38
+ { value: "quit", label: "quit", description: "Disable caveman mode" },
39
+ { value: "config", label: "config", description: "Open settings dialog" },
40
+ ] as const;
41
+
26
42
  // ---------------------------------------------------------------------------
27
43
  // Persistent config (survives across sessions)
28
44
  // ---------------------------------------------------------------------------
@@ -35,7 +51,8 @@ interface CavemanConfig {
35
51
  }
36
52
 
37
53
  const CONFIG_PATH = join(homedir(), ".pi", "agent", "caveman.json");
38
- const DEFAULT_CONFIG: CavemanConfig = { defaultLevel: "off", showStatus: true };
54
+ const DEFAULT_CONFIG: CavemanConfig = { defaultLevel: "full", showStatus: true };
55
+ let saveConfigQueue: Promise<void> = Promise.resolve();
39
56
 
40
57
  async function loadConfig(): Promise<CavemanConfig> {
41
58
  try {
@@ -51,8 +68,12 @@ async function loadConfig(): Promise<CavemanConfig> {
51
68
  }
52
69
 
53
70
  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");
71
+ const snapshot = JSON.stringify(config, null, 2) + "\n";
72
+ saveConfigQueue = saveConfigQueue.then(async () => {
73
+ await mkdir(join(homedir(), ".pi", "agent"), { recursive: true });
74
+ await writeFile(CONFIG_PATH, snapshot, "utf8");
75
+ });
76
+ return saveConfigQueue;
56
77
  }
57
78
 
58
79
  // ---------------------------------------------------------------------------
@@ -85,12 +106,13 @@ const FIRE_FRAMES = [
85
106
  ];
86
107
 
87
108
  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 },
109
+ lite: { frames: FIRE_FRAMES, label: "LITE", interval: 300 },
110
+ full: { frames: FIRE_FRAMES, label: "FULL", interval: 200 },
111
+ ultra: { frames: FIRE_FRAMES, label: "ULTRA", interval: 100 },
112
+ "wenyan-lite": { frames: FIRE_FRAMES, label: "文言", interval: 300 },
113
+ wenyan: { frames: FIRE_FRAMES, label: "文言文", interval: 200 },
114
+ "wenyan-ultra": { frames: FIRE_FRAMES, label: "文言文極", interval: 100 },
115
+ micro: { frames: FIRE_FRAMES, label: "MICRO", interval: 120 },
94
116
  };
95
117
 
96
118
  // ---------------------------------------------------------------------------
@@ -111,7 +133,15 @@ pleasantries, hedging
111
133
  Bad: "Sure! I'd be happy to help you with that. The issue you're experiencing is likely caused by..."
112
134
  Good: "Bug in auth middleware. Token expiry check use \`<\` not \`<=\`. Fix:"`;
113
135
 
114
- const INTENSITY: Record<Exclude<Level, "off">, string> = {
136
+ const MICRO_PROMPT = `# Token efficiency
137
+ Respond like smart caveman. Cut all filler, keep technical substance.
138
+ - Drop articles (a, an, the), filler (just, really, basically, actually).
139
+ - Drop pleasantries (sure, certainly, happy to).
140
+ - No hedging. Fragments fine. Short synonyms.
141
+ - Technical terms stay exact. Code blocks unchanged.
142
+ - Pattern: [thing] [action] [reason]. [next step].`;
143
+
144
+ const INTENSITY: Record<Exclude<Level, "off" | "micro">, string> = {
115
145
  lite: `\
116
146
  No filler/hedging. Keep articles + full sentences. Professional but tight.
117
147
  Example: "Your component re-renders because you create a new object reference each render. Wrap it in \`useMemo\`."`,
@@ -151,6 +181,20 @@ export default function caveman(pi: ExtensionAPI) {
151
181
  let config: CavemanConfig = { ...DEFAULT_CONFIG };
152
182
  let timer: ReturnType<typeof setInterval> | null = null;
153
183
  let frameIndex = 0;
184
+ let isActive = false;
185
+ let configLoadPromise: Promise<void> | null = null;
186
+
187
+ const ensureConfigLoaded = async () => {
188
+ if (!configLoadPromise) {
189
+ configLoadPromise = (async () => {
190
+ config = await loadConfig();
191
+ if (level === "off" && config.defaultLevel !== "off") {
192
+ level = config.defaultLevel;
193
+ }
194
+ })();
195
+ }
196
+ await configLoadPromise;
197
+ };
154
198
 
155
199
  // -- Animation helpers --
156
200
 
@@ -172,10 +216,17 @@ export default function caveman(pi: ExtensionAPI) {
172
216
  }
173
217
 
174
218
  const anim = ANIMATIONS[level];
219
+ const setFrame = (frame: string) => {
220
+ ctx.ui.setStatus("caveman", frame + " " + theme.fg("muted", "caveman level: ") + theme.fg("text", anim.label));
221
+ };
222
+
223
+ if (!isActive) {
224
+ setFrame(anim.frames[0]!);
225
+ return;
226
+ }
175
227
 
176
228
  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));
229
+ setFrame(anim.frames[frameIndex % anim.frames.length]!);
179
230
  frameIndex++;
180
231
  };
181
232
 
@@ -186,7 +237,7 @@ export default function caveman(pi: ExtensionAPI) {
186
237
  // -- Restore state on session load --
187
238
 
188
239
  pi.on("session_start", async (_event, ctx) => {
189
- config = await loadConfig();
240
+ await ensureConfigLoaded();
190
241
 
191
242
  // Check for session-level override first (resuming a session)
192
243
  let sessionLevel: Level | null = null;
@@ -208,14 +259,30 @@ export default function caveman(pi: ExtensionAPI) {
208
259
  syncStatus(ctx);
209
260
  });
210
261
 
262
+ pi.on("agent_start", async (_event, ctx) => {
263
+ isActive = true;
264
+ syncStatus(ctx);
265
+ });
266
+
267
+ pi.on("agent_end", async (_event, ctx) => {
268
+ isActive = false;
269
+ syncStatus(ctx);
270
+ });
271
+
211
272
  pi.on("session_shutdown", async () => {
212
273
  stopAnimation();
274
+ isActive = false;
213
275
  });
214
276
 
215
277
  // -- /caveman command --
216
278
 
217
279
  pi.registerCommand("caveman", {
218
- description: "Toggle caveman mode, set level, or 'config' to open settings",
280
+ description: "Toggle caveman mode, set level, use stop/off/quit to disable, or 'config' to open settings",
281
+ getArgumentCompletions: (prefix: string) => {
282
+ const normalized = prefix.trim().toLowerCase();
283
+ const items = CAVEMAN_COMMAND_OPTIONS.filter((item) => item.value.startsWith(normalized));
284
+ return items.length > 0 ? items : null;
285
+ },
219
286
  handler: async (args, ctx) => {
220
287
  const arg = args?.trim().toLowerCase();
221
288
 
@@ -227,10 +294,12 @@ export default function caveman(pi: ExtensionAPI) {
227
294
 
228
295
  if (!arg) {
229
296
  level = level === "off" ? "full" : "off";
297
+ } else if (STOP_ALIASES.has(arg)) {
298
+ level = "off";
230
299
  } else if (LEVELS.includes(arg as Level)) {
231
300
  level = arg as Level;
232
301
  } else {
233
- ctx.ui.notify(`Unknown: "${arg}". Use: ${LEVELS.join(", ")} or config`, "error");
302
+ ctx.ui.notify(`Unknown: "${arg}". Use: ${LEVELS.join(", ")}, stop, quit, or config`, "error");
234
303
  return;
235
304
  }
236
305
 
@@ -247,7 +316,7 @@ export default function caveman(pi: ExtensionAPI) {
247
316
  // -- /caveman config: interactive SettingsList --
248
317
 
249
318
  async function openConfig(ctx: ExtensionContext) {
250
- config = await loadConfig();
319
+ await ensureConfigLoaded();
251
320
 
252
321
  await ctx.ui.custom((_tui, theme, _kb, done) => {
253
322
  const items: SettingItem[] = [
@@ -268,32 +337,65 @@ export default function caveman(pi: ExtensionAPI) {
268
337
  const container = new Container();
269
338
  container.addChild(new Text(theme.fg("accent", theme.bold(" Caveman Config")), 0, 0));
270
339
  container.addChild(new Text(theme.fg("dim", " Saved to ~/.pi/agent/caveman.json"), 0, 0));
340
+ container.addChild(new Text(theme.fg("dim", " Default level applies to future sessions."), 0, 0));
271
341
  container.addChild(new Text("", 0, 0));
272
342
 
343
+ const applySettingChange = (id: string, newValue: string) => {
344
+ if (id === "defaultLevel" && LEVELS.includes(newValue as Level)) {
345
+ config.defaultLevel = newValue as Level;
346
+ } else if (id === "showStatus") {
347
+ config.showStatus = newValue === "on";
348
+ }
349
+ saveConfig(config);
350
+ syncStatus(ctx);
351
+ };
352
+
273
353
  const settingsList = new SettingsList(
274
354
  items,
275
355
  Math.min(items.length + 2, 10),
276
356
  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
- },
357
+ applySettingChange,
286
358
  () => done(undefined),
287
359
  );
288
360
 
289
361
  container.addChild(settingsList);
290
- container.addChild(new Text(theme.fg("dim", " ←→ change • esc close"), 0, 0));
362
+ container.addChild(new Text(theme.fg("dim", " ←→/hl/tab change • ↑↓/jk move • esc close"), 0, 0));
363
+
364
+ const cycleSelectedValue = (direction: -1 | 1) => {
365
+ const selectedIndex = (settingsList as unknown as { selectedIndex: number }).selectedIndex;
366
+ const item = items[selectedIndex];
367
+ if (!item?.values?.length) return;
368
+
369
+ const currentIndex = item.values.indexOf(item.currentValue);
370
+ const nextIndex = (currentIndex + direction + item.values.length) % item.values.length;
371
+ const newValue = item.values[nextIndex]!;
372
+ item.currentValue = newValue;
373
+ settingsList.updateValue(item.id, newValue);
374
+ applySettingChange(item.id, newValue);
375
+ };
291
376
 
292
377
  return {
293
378
  render: (w: number) => container.render(w),
294
379
  invalidate: () => container.invalidate(),
295
380
  handleInput: (data: string) => {
381
+ if (data === "j") data = "\u001b[B";
382
+ else if (data === "k") data = "\u001b[A";
383
+ else if (data === "h") {
384
+ cycleSelectedValue(-1);
385
+ _tui.requestRender();
386
+ return;
387
+ } else if (data === "l" || data === "\u001b[C" || data === "\t") {
388
+ cycleSelectedValue(1);
389
+ _tui.requestRender();
390
+ return;
391
+ } else if (data === "\u001b[D") {
392
+ cycleSelectedValue(-1);
393
+ _tui.requestRender();
394
+ return;
395
+ }
396
+
296
397
  settingsList.handleInput?.(data);
398
+ _tui.requestRender();
297
399
  },
298
400
  };
299
401
  });
@@ -302,7 +404,13 @@ export default function caveman(pi: ExtensionAPI) {
302
404
  // -- Inject caveman rules into system prompt --
303
405
 
304
406
  pi.on("before_agent_start", async (event) => {
407
+ await ensureConfigLoaded();
305
408
  if (level === "off") return;
409
+ if (level === "micro") {
410
+ return {
411
+ systemPrompt: `${event.systemPrompt}\n\n${MICRO_PROMPT}`,
412
+ };
413
+ }
306
414
  return {
307
415
  systemPrompt: `${event.systemPrompt}\n\n${BASE}\n\n${INTENSITY[level]}\n\n${SAFETY}`,
308
416
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-caveman",
3
- "version": "1.0.2",
3
+ "version": "1.0.4",
4
4
  "description": "Why use many token when few do trick. Caveman mode for pi — cuts ~75% output tokens while keeping full technical accuracy.",
5
5
  "keywords": [
6
6
  "pi-package",