@kulapard/pi-caveman 0.4.2 → 0.5.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/AGENTS.md CHANGED
@@ -5,20 +5,25 @@ Project memory. Non-obvious only.
5
5
  ## Architecture (do not rebuild)
6
6
 
7
7
  - `extensions/caveman.ts` = Pi extension. `extensions/caveman-core.ts` = pure SDK-free logic (`normalizeMode`, `modeInstructions`, `VALID_MODES`, regexes). Unit-testable without fake SDK.
8
- - Mode state **session-scoped**: `pi.appendEntry("caveman-mode", …)`, restored from `ctx.sessionManager.getBranch()` on `session_start`. New session always `off`. No cross-session config/env.
8
+ - Mode state **session + project-scoped**: `pi.appendEntry("caveman-mode", …)`
9
+ restores from `ctx.sessionManager.getBranch()` on `session_start`; since
10
+ v0.4.3 `extensions/caveman-state.ts` also reads/writes `.pi/caveman-mode.json`
11
+ in the project directory. A session entry overrides the project default; a
12
+ project without a state file falls back to `off`. No global/env default.
9
13
  - Activation = `before_agent_start` appends `modeInstructions(mode)`. Statusline = `ctx.ui.setStatus("caveman", …)` guarded by `hasUI`.
10
- - Extension `modeInstructions` = **canonical** activator. `skills/caveman/SKILL.md` = fallback for hosts loading skills but not extension. If both active, model sees both rule sets intentional redundancy, no de-dupe.
14
+ - Extension `modeInstructions` = **canonical** activator. `skills/caveman/SKILL.md` = fallback for hosts loading skills but not extension. Both active model sees both rule sets; intentional redundancy, no de-dupe.
11
15
  - Pi 0.80.2 has **no `agents/` subagent mechanism**. `agents/cavecrew-*.md` = reference personas only; cavecrew optional/out-of-scope.
12
16
 
13
17
  ## Invariants
14
18
 
15
19
  - **SDK import `import type` only** in `extensions/*.ts`. JS tests run via `--experimental-strip-types`, erases type-only imports. Value import from `@earendil-works/pi-coding-agent` breaks tests — `tests/extension.test.mjs` asserts this.
16
- - **Verbatim preservation**: caveman-compress never changes code blocks, inline code, URLs, paths, commands, exact error strings. Skill instructs self-validate against original. Mismatch unfixable → restore from `.original` backup created in same invocation (stale backups rejected before compression).
20
+ - **Verbatim preservation**: caveman-compress never changes code blocks, inline code, URLs, paths, commands, exact error strings. Self-validate against original. Mismatch unfixable → restore from `.original` backup created in same invocation (stale backups rejected before compression).
17
21
  - `caveman-compress` is **prompt-only**: Pi agent compresses with own model + file tools, driven by `SKILL.md`. No Python, no external model CLI. Coverage = `tests/compress-docs.test.mjs`.
22
+ - `/caveman-compress` supports `--force`: overwrites existing `.original.<ext>` backup instead of aborting.
18
23
 
19
24
  ## Changelog
20
25
 
21
- Every notable change must update `CHANGELOG.md` under `[Unreleased]`. Before finishing task, run `npm run changelog:check` to confirm changelog updated for files changed. CI runs same check on every PR.
26
+ Every notable change update `CHANGELOG.md` under `[Unreleased]`. Before finishing, run `npm run changelog:check`. CI runs same check on every PR.
22
27
 
23
28
  - `npm test` runs `pretest` (`npm run typecheck` → `tsc --noEmit`), then `node --test tests/**/*.test.mjs`. Typecheck failures fail test run.
24
29
  - Node `--test` glob expanded by runner, not shell. Directory-recursion `--test tests/` does **not** work on current Node — keep glob.
package/CHANGELOG.md CHANGED
@@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.5.0] - 2026-06-29
11
+
12
+ ### Added
13
+
14
+ - `/caveman-compress` now accepts an optional `--force` flag. With `--force`, an existing `.original.<ext>` backup is overwritten instead of aborting.
15
+
16
+ ### Changed
17
+
18
+ - Updated README, AGENTS.md, caveman-help, and caveman-compress docs to reflect the project-scoped persistence added in v0.4.3 and the new `--force` flag.
19
+
10
20
  ## [0.4.2] - 2026-06-29
11
21
 
12
22
  ### Changed
@@ -112,7 +122,8 @@ port of [caveman](https://github.com/JuliusBrussee/caveman).
112
122
  token, automatic provenance, a tag-equals-version guard, and a concurrency
113
123
  group).
114
124
 
115
- [Unreleased]: https://github.com/kulapard/pi-caveman/compare/v0.4.2...HEAD
125
+ [Unreleased]: https://github.com/kulapard/pi-caveman/compare/v0.5.0...HEAD
126
+ [0.5.0]: https://github.com/kulapard/pi-caveman/compare/v0.4.2...v0.5.0
116
127
  [0.4.2]: https://github.com/kulapard/pi-caveman/compare/v0.4.1...v0.4.2
117
128
  [0.4.1]: https://github.com/kulapard/pi-caveman/compare/v0.4.0...v0.4.1
118
129
  [0.4.0]: https://github.com/kulapard/pi-caveman/compare/v0.3.0...v0.4.0
package/README.md CHANGED
@@ -92,7 +92,7 @@ session ends.
92
92
  | `/caveman-help` | Show the quick-reference card. |
93
93
  | `/caveman-commit [notes]` | Generate a terse Conventional Commit message. Does **not** commit. |
94
94
  | `/caveman-review [scope]` | One-line-per-finding code review comments. |
95
- | `/caveman-compress <file>` | Compress a prose file via the caveman-compress skill. |
95
+ | `/caveman-compress <file> [--force]` | Compress a prose file via the caveman-compress skill. `--force` overwrites an existing `.original` backup. |
96
96
  | `/caveman-stats` | Load the stats skill (an on-demand, model-driven estimate). |
97
97
 
98
98
  ## Natural-language activation
@@ -105,18 +105,19 @@ switches mode on phrases like:
105
105
  - **Deactivate:** "stop caveman", "normal mode", "disable caveman" → turns it
106
106
  off.
107
107
 
108
- ## Session-scoped behavior
108
+ ## Session and project-scoped behavior
109
109
 
110
- Mode state is **session-scoped**. It is stored as a session entry and restored
111
- across a `/reload` within the same session, but a **new session always starts
112
- with caveman off** — by design. There is no cross-session config file or global
113
- default that auto-enables it.
110
+ Mode state is stored as a session entry, so it survives a `/reload` within the
111
+ same session. Since v0.4.3 it is also persisted per project in
112
+ `.pi/caveman-mode.json`, so a **new session in the same project directory**
113
+ restores the last used mode. A session entry always overrides the project
114
+ default, and a project without a state file falls back to `off`.
114
115
 
115
116
  ## Statusline indicator
116
117
 
117
118
  When a UI is attached, the statusline shows the active mode as
118
- `caveman:<mode>` (for example `caveman:ultra`). When caveman is off the indicator
119
- is cleared.
119
+ `caveman:<mode>` (for example `caveman:ultra`), and appends live context usage
120
+ as `ctx:XX%` when Pi provides it. When caveman is off the indicator is cleared.
120
121
 
121
122
  ## Compression vs. upstream MCP shrink
122
123
 
@@ -0,0 +1,49 @@
1
+ // Project-scoped persistence for caveman mode.
2
+ // This is a workaround until Pi exposes SettingsManager to extensions
3
+ // (tracked upstream as pi-coding-agent issue #4981).
4
+
5
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
6
+ import { join } from "node:path";
7
+ import { normalizeMode, type StoredMode } from "./caveman-core.ts";
8
+
9
+ const STATE_DIR = ".pi";
10
+ const STATE_FILE = "caveman-mode.json";
11
+
12
+ function statePath(cwd: string): string {
13
+ return join(cwd, STATE_DIR, STATE_FILE);
14
+ }
15
+
16
+ /**
17
+ * Load the project-scoped caveman mode default. Returns `undefined` when no
18
+ * state file exists or it cannot be read, so the caller can fall back to the
19
+ * session-scoped default (`off`).
20
+ */
21
+ export function loadProjectMode(cwd: string): StoredMode | undefined {
22
+ const path = statePath(cwd);
23
+ if (!existsSync(path)) return undefined;
24
+ try {
25
+ const raw = JSON.parse(readFileSync(path, "utf8"));
26
+ return normalizeMode(raw.mode);
27
+ } catch {
28
+ return undefined;
29
+ }
30
+ }
31
+
32
+ /**
33
+ * Persist the caveman mode for this project directory. Returns `true` on
34
+ * success; failures are silent so that session operation is never blocked by
35
+ * disk issues.
36
+ */
37
+ export function saveProjectMode(cwd: string, mode: StoredMode): boolean {
38
+ try {
39
+ const dir = join(cwd, STATE_DIR);
40
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
41
+ writeFileSync(
42
+ statePath(cwd),
43
+ JSON.stringify({ mode, updatedAt: Date.now() }, null, 2),
44
+ );
45
+ return true;
46
+ } catch {
47
+ return false;
48
+ }
49
+ }
@@ -10,6 +10,7 @@ import {
10
10
  normalizeMode,
11
11
  type StoredMode,
12
12
  } from "./caveman-core.ts";
13
+ import { loadProjectMode, saveProjectMode } from "./caveman-state.ts";
13
14
 
14
15
  const HELP_TEXT = `# Caveman for Pi
15
16
 
@@ -19,18 +20,25 @@ Commands:
19
20
  - /caveman-help — show this card.
20
21
  - /caveman-commit [notes] — generate Conventional Commit message. Does not commit.
21
22
  - /caveman-review [scope] — terse review comments.
22
- - /caveman-compress <file> — compress prose file via caveman-compress skill.
23
+ - /caveman-compress <file> [--force] — compress prose file via caveman-compress skill. --force overwrites an existing .original backup.
23
24
  - /caveman-stats — load stats skill/help.
24
25
 
25
- Mode persists in current Pi session and survives /reload via session state.
26
- Code, commands, API names, file paths, and exact errors stay verbatim.`;
26
+ Mode persists across sessions in the same project via \`.pi/caveman-mode.json\`, and
27
+ survives /reload via session state. Code, commands, API names, file paths, and
28
+ exact errors stay verbatim.`;
27
29
 
28
30
  export default function cavemanExtension(pi: ExtensionAPI) {
29
31
  let mode: StoredMode = "off";
30
32
 
31
33
  function setStatus(ctx?: ExtensionContext) {
32
34
  if (!ctx?.hasUI) return;
33
- ctx.ui.setStatus("caveman", mode === "off" ? undefined : `caveman:${mode}`);
35
+ const usage = ctx.getContextUsage?.();
36
+ const suffix =
37
+ usage?.percent != null ? ` ctx:${Math.round(usage.percent)}%` : "";
38
+ ctx.ui.setStatus(
39
+ "caveman",
40
+ mode === "off" ? undefined : `caveman:${mode}${suffix}`,
41
+ );
34
42
  }
35
43
 
36
44
  function persistMode(nextMode: StoredMode, ctx?: ExtensionContext) {
@@ -40,7 +48,7 @@ export default function cavemanExtension(pi: ExtensionAPI) {
40
48
  }
41
49
 
42
50
  pi.on("session_start", (_event, ctx) => {
43
- mode = "off";
51
+ mode = loadProjectMode(ctx.cwd) ?? "off";
44
52
  for (const entry of ctx.sessionManager.getBranch() as Array<{
45
53
  type?: string;
46
54
  customType?: string;
@@ -73,6 +81,7 @@ export default function cavemanExtension(pi: ExtensionAPI) {
73
81
  return;
74
82
  }
75
83
  persistMode(nextMode, ctx);
84
+ saveProjectMode(ctx.cwd, nextMode);
76
85
  ctx.ui.notify(
77
86
  nextMode === "off" ? "Caveman disabled" : `Caveman ${nextMode} enabled`,
78
87
  "info",
@@ -112,14 +121,20 @@ export default function cavemanExtension(pi: ExtensionAPI) {
112
121
  });
113
122
 
114
123
  pi.registerCommand("caveman-compress", {
115
- description: "Compress prose/memory file into caveman style",
124
+ description:
125
+ "Compress prose/memory file into caveman style (optional --force)",
116
126
  handler: async (args, ctx) => {
117
- const target = args?.trim();
127
+ const raw = args?.trim() ?? "";
128
+ const tokens = raw.split(/\s+/).filter(Boolean);
129
+ const force = tokens.includes("--force");
130
+ const target = tokens.filter((t) => t !== "--force").join(" ");
118
131
  if (!target) {
119
- ctx.ui.notify("Usage: /caveman-compress <file>", "error");
132
+ ctx.ui.notify("Usage: /caveman-compress <file> [--force]", "error");
120
133
  return;
121
134
  }
122
- pi.sendUserMessage(`/skill:caveman-compress ${target}`);
135
+ pi.sendUserMessage(
136
+ `/skill:caveman-compress ${target}${force ? " --force" : ""}`,
137
+ );
123
138
  },
124
139
  });
125
140
 
@@ -137,14 +152,22 @@ export default function cavemanExtension(pi: ExtensionAPI) {
137
152
  if (event.source !== "extension") {
138
153
  const text = (event.text ?? "").trim();
139
154
  if (DEACTIVATION_RE.test(text)) {
140
- if (mode !== "off") persistMode("off", ctx);
155
+ if (mode !== "off") {
156
+ persistMode("off", ctx);
157
+ saveProjectMode(ctx.cwd, "off");
158
+ }
141
159
  } else if (mode === "off" && ACTIVATION_RE.test(text)) {
142
160
  persistMode("full", ctx);
161
+ saveProjectMode(ctx.cwd, "full");
143
162
  }
144
163
  }
145
164
  return { action: "continue" as const };
146
165
  });
147
166
 
167
+ pi.on("turn_end", (_event, ctx) => {
168
+ setStatus(ctx);
169
+ });
170
+
148
171
  pi.on("before_agent_start", (event) => {
149
172
  if (mode === "off") return undefined;
150
173
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kulapard/pi-caveman",
3
- "version": "0.4.2",
3
+ "version": "0.5.0",
4
4
  "description": "Caveman for Pi: ultra-compressed agent output that preserves technical substance. Six intensity modes, slash commands, natural-language activation, and a session statusline.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -25,7 +25,7 @@ AGENTS.md ← compressed (the agent reads this — fewer tokens every s
25
25
  AGENTS.original.md ← human-readable backup (snapshot taken at first compression)
26
26
  ```
27
27
 
28
- Original never lost — first compression writes a verbatim backup. To edit and re-compress, **edit `AGENTS.md` itself**, then delete or rename the existing `AGENTS.original.md` so the next `/caveman-compress` can create a fresh backup.
28
+ Original never lost — first compression writes a verbatim backup. To edit and re-compress, **edit `AGENTS.md` itself**, then delete or rename the existing `AGENTS.original.md` so the next `/caveman-compress` can create a fresh backup. Or use `/caveman-compress AGENTS.md --force` to overwrite the existing backup in one step.
29
29
 
30
30
  ## Benchmarks
31
31
 
@@ -77,7 +77,7 @@ its own model and file tools — there is no separate tool or language to instal
77
77
  ## Usage
78
78
 
79
79
  ```
80
- /caveman-compress <filepath>
80
+ /caveman-compress <filepath> [--force]
81
81
  ```
82
82
 
83
83
  Examples:
@@ -87,6 +87,7 @@ Examples:
87
87
  /caveman-compress CLAUDE.md
88
88
  /caveman-compress docs/preferences.md
89
89
  /caveman-compress todos.md
90
+ /caveman-compress AGENTS.md --force
90
91
  ```
91
92
 
92
93
  ### What files work
@@ -4,7 +4,7 @@ description: >
4
4
  Compress natural language memory files (AGENTS.md, CLAUDE.md, todos, preferences) into caveman
5
5
  format to save input tokens. Preserves all technical substance, code, URLs, and structure.
6
6
  Compressed version overwrites the original file. Human-readable backup saved as FILE.original.<ext> (same extension as the source).
7
- Trigger: /caveman-compress FILEPATH or "compress memory file"
7
+ Trigger: /caveman-compress FILEPATH [--force]
8
8
  ---
9
9
 
10
10
  # Caveman Compress
@@ -15,16 +15,19 @@ Compress natural language files (`AGENTS.md`, `CLAUDE.md`, todos, preferences) i
15
15
 
16
16
  ## Trigger
17
17
 
18
- `/caveman-compress <filepath>` or when user asks to compress a memory file.
18
+ `/caveman-compress <filepath>` or when user asks to compress a memory file. Append `--force` to overwrite an existing `.original.<ext>` backup instead of aborting.
19
19
 
20
20
  ## Process
21
21
 
22
- You (the Pi agent) perform the compression directly — there is no separate tool to run. Given `/caveman-compress <filepath>`:
22
+ You (the Pi agent) perform the compression directly — there is no separate tool to run. Given `/caveman-compress <filepath>` (optionally with `--force`):
23
23
 
24
24
  1. **Skip backups.** If the path ends in `.original.<ext>` or, for extensionless files, in `.original` (e.g. `AGENTS.original.md`, `NOTES.original`), stop — never compress a backup file.
25
25
  2. **Check it is compressible** per **Boundaries** below: prose files (`.md`, `.txt`, `.rst`, `.typ`, `.typst`, `.tex`, or extensionless natural language). If it is code/config (`.py`, `.js`, `.ts`, `.json`, `.yaml`, …) or larger than ~500 KB, report it is out of scope and stop.
26
26
  3. **Read** the file's full contents.
27
- 4. **Back up the original.** If a `.original`/`.original.<ext>` backup already exists, **abort** and tell the user to remove or rename the stale backup before re-compressing. Otherwise write a verbatim copy to `<filename>.original.<ext>` (or `<filename>.original` for extensionless files) before any rewrite.
27
+ 4. **Back up the original.** If a `.original`/`.original.<ext>` backup already exists:
28
+ - Without `--force`, **abort** and tell the user to remove or rename the stale backup before re-compressing.
29
+ - With `--force`, overwrite the existing backup with a verbatim copy of the current source before any rewrite.
30
+ If no backup exists, write a verbatim copy to `<filename>.original.<ext>` (or `<filename>.original` for extensionless files) before any rewrite.
28
31
  5. **Rewrite** the file in place, applying the **Compression Rules** below. Treat code blocks, inline code, URLs, paths, commands, headings, and table structure as read-only regions.
29
32
  6. **Self-validate** against the contents you read in step 3: every protected token — fenced and inline code, URLs, file paths, heading text, table structure, dates/version numbers — must be byte-for-byte identical. If any changed, fix just that region; if you cannot make it identical, restore the file from the backup you wrote in step 4 and report the failure rather than leave a corrupted file.
30
33
  7. **Report** the result: bytes before/after and the approximate reduction.
@@ -4,7 +4,13 @@ Quick-reference card. One shot, no mode change.
4
4
 
5
5
  ## What it does
6
6
 
7
- Prints a cheat sheet of all caveman modes, sibling skills, deactivation triggers, and how mode lasts for the session (set with `/caveman`, resets to `off` on a new session — no config file or env var). One-shot display — does not flip the active mode, write flag files, or persist anything. Use when you forget the slash commands.
7
+ Prints a cheat sheet of all caveman modes, sibling skills, deactivation
8
+ triggers, and how mode persists: it is stored as a session entry (survives
9
+ `/reload`), and since v0.4.3 it is also saved per project in
10
+ `.pi/caveman-mode.json` (a new session in the same project directory restores
11
+ the last mode). No config file? Falls back to `off`. One-shot display — does not
12
+ flip the active mode, write flag files, or persist anything. Use when you forget
13
+ the slash commands.
8
14
 
9
15
  ## How to invoke
10
16
 
@@ -29,7 +29,7 @@ Mode stick until changed or session end.
29
29
  |-------|---------|-----------|
30
30
  | **caveman-commit** | `/caveman-commit` | Terse commit messages. Conventional Commits. ≤50 char subject. |
31
31
  | **caveman-review** | `/caveman-review` | One-line PR comments: `L42: bug: user null. Add guard.` |
32
- | **caveman-compress** | `/caveman-compress <file>` | Compress .md files to caveman prose. Saves ~46% input tokens. |
32
+ | **caveman-compress** | `/caveman-compress <file> [--force]` | Compress .md files to caveman prose. Saves ~46% input tokens. `--force` overwrites stale backup. |
33
33
  | **caveman-stats** | `/caveman-stats` | On-demand, model-driven estimate of tokens saved this session. |
34
34
  | **caveman-help** | `/caveman-help` | This card. |
35
35
 
@@ -41,12 +41,16 @@ Say "stop caveman" or "normal mode". Resume anytime with `/caveman`.
41
41
 
42
42
  Keep user's language by default. User write Portuguese → reply Portuguese caveman. Compress the style, not the language. Technical terms, code, commands, commit types, and exact error strings stay verbatim unless user ask for translation.
43
43
 
44
- ## Mode lasts the session
44
+ ## Mode persistence
45
45
 
46
- `/caveman` (no argument) = `full`. Pick another with `/caveman ultra`, `/caveman lite`, etc.
46
+ Mode set per session entry, so it survives `/reload`. Since v0.4.3 it is also
47
+ saved per project in `.pi/caveman-mode.json`; a new session in the same project
48
+ directory restores the last mode. Session entry overrides project default. No
49
+ config file? Falls back to `off`.
47
50
 
48
- Mode set per session. New session start → mode `off`; activate again with `/caveman`. No config file, no env var — the `/caveman` command is the only switch.
51
+ `/caveman` (no argument) = `full`. Pick another with `/caveman ultra`,
52
+ `/caveman lite`, etc. Say "stop caveman" or "normal mode" to turn off.
49
53
 
50
54
  ## More
51
55
 
52
- Full docs: https://github.com/kulapard/pi-caveman
56
+ Full docs: <https://github.com/kulapard/pi-caveman>