pi-context-whisperer 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.
Files changed (3) hide show
  1. package/README.md +61 -0
  2. package/index.ts +304 -0
  3. package/package.json +16 -0
package/README.md ADDED
@@ -0,0 +1,61 @@
1
+ # pi-context-whisperer 🦜
2
+
3
+ Smart gradual auto-compaction for Pi — warns you and auto-compacts before context limits hit, preserving your flow.
4
+
5
+ ## How it works
6
+
7
+ Context Whisperer monitors your context window usage after every turn and takes action before you hit limits:
8
+
9
+ ```
10
+ Context: 45% → 🦜 ███████░░░ 58k/128k ✓ Healthy
11
+ Context: 72% → 🦜 █████████░░ 92k/128k ⚡ Warning (notifies you)
12
+ Context: 82% → 🦜 ██████████░ 105k/128k ⚠ COMPACT NOW (auto-compacts)
13
+ Context: 35% → 🦜 █████░░░░░ 45k/128k ✓ Healthy (freed up!)
14
+ ```
15
+
16
+ ## Features
17
+
18
+ - **Context health bar** — live in footer: ████░░░░ with percentage
19
+ - **Warning at customizable threshold** — nudges you when context gets tight
20
+ - **Auto-compaction** — automatically compacts at critical threshold so you don't crash
21
+ - **Prevents wasted calls** — no more "context limit exceeded" mid-task
22
+ - **Compaction counter** — see how many times it's saved you this session
23
+ - **Fully configurable** — set your own warn/auto thresholds
24
+
25
+ ## Install
26
+
27
+ ```bash
28
+ # npm
29
+ pi install npm:pi-context-whisperer
30
+
31
+ # GitHub
32
+ pi install git:github.com/Jaraxxxx/pi-context-whisperer
33
+ ```
34
+
35
+ ## Commands
36
+
37
+ | Command | Description |
38
+ |---------|-------------|
39
+ | `/whisper-enable` | Turn on auto-compaction |
40
+ | `/whisper-disable` | Turn off (manual `/compact` only) |
41
+ | `/whisper-stats` | Show current stats and thresholds |
42
+ | `/whisper-config 60 80` | Set warn at 60%, auto at 80% |
43
+ | `Ctrl+Shift+C` | Force compact now |
44
+
45
+ ## LLM Tools
46
+
47
+ The `context_health` tool lets the agent check context status before making large requests.
48
+
49
+ ## Default thresholds
50
+
51
+ | Threshold | Default | Behavior |
52
+ |-----------|---------|----------|
53
+ | Warn | 70% | Notification only |
54
+ | Auto-compact | 80% | Triggers compaction automatically |
55
+
56
+ Change with: `/whisper-config <warnPct> <autoPct>`
57
+
58
+ ## Requirements
59
+
60
+ - Pi coding agent
61
+ - Auto-compaction enabled in settings (default: on)
package/index.ts ADDED
@@ -0,0 +1,304 @@
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+
3
+ // ---- State ----
4
+
5
+ interface WhispererState {
6
+ enabled: boolean;
7
+ warnThreshold: number; // % of context at which to warn (default 70)
8
+ autoThreshold: number; // % of context at which to compact (default 80)
9
+ compactedCount: number; // how many times compacted this session
10
+ lastWarningAt: number; // last warning % to avoid spam
11
+ }
12
+
13
+ const STORAGE_KEY = "pi-context-whisperer-state";
14
+
15
+ let state: WhispererState = {
16
+ enabled: true,
17
+ warnThreshold: 70,
18
+ autoThreshold: 80,
19
+ compactedCount: 0,
20
+ lastWarningAt: 0,
21
+ };
22
+
23
+ let tuiRef: any = null;
24
+ let inContextLock = false;
25
+
26
+ // ---- Helpers ----
27
+
28
+ function fmtTokens(n: number | null | undefined): string {
29
+ if (n == null) return "?k";
30
+ if (n > 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`;
31
+ if (n > 1_000) return `${(n / 1_000).toFixed(1)}k`;
32
+ return `${n}`;
33
+ }
34
+
35
+ function pct(n: number): string {
36
+ return `${n.toFixed(0)}%`;
37
+ }
38
+
39
+ // ---- Extension ----
40
+
41
+ export default function (pi: ExtensionAPI) {
42
+ // ---- Restore state ----
43
+ pi.on("session_start", async (_event, ctx) => {
44
+ for (const entry of ctx.sessionManager.getEntries()) {
45
+ if (entry.type === "custom" && entry.customType === STORAGE_KEY && entry.data) {
46
+ state = { ...state, ...(entry.data as Partial<WhispererState>) };
47
+ break;
48
+ }
49
+ }
50
+
51
+ // Replace/supplement footer with context health indicator
52
+ ctx.ui.setFooter((tui, theme, footerData) => {
53
+ tuiRef = tui;
54
+ const unsub = footerData.onBranchChange(() => tui.requestRender());
55
+
56
+ return {
57
+ dispose: unsub,
58
+ invalidate() {},
59
+ render(width: number): string[] {
60
+ const A = (s: string) => theme.fg("accent", s);
61
+ const D = (s: string) => theme.fg("dim", s);
62
+ const W = (s: string) => theme.fg("warning", s);
63
+ const E = (s: string) => theme.fg("error", s);
64
+ const S = (s: string) => theme.fg("success", s);
65
+
66
+ const lines: string[] = [];
67
+
68
+ if (!state.enabled) {
69
+ lines.push(D("🦜 Context Whisperer: off (/whisper-enable to turn on)"));
70
+ return lines;
71
+ }
72
+
73
+ // Get current context
74
+ const cu = (ctx as any).getContextUsage?.();
75
+ let pctVal: number | null = null;
76
+ let tokens: number | null = null;
77
+ let window: number | null = (ctx as any).model?.contextWindow ?? null;
78
+
79
+ if (cu && cu.percent != null) {
80
+ pctVal = cu.percent;
81
+ tokens = cu.tokens ?? null;
82
+ }
83
+
84
+ if (pctVal === null) {
85
+ lines.push(D("🦜 Context Whisperer: waiting for context data..."));
86
+ return lines;
87
+ }
88
+
89
+ const w = Math.min(20, Math.max(8, Math.floor(width / 5)));
90
+ const filled = Math.min(w, Math.max(0, Math.round((pctVal / 100) * w)));
91
+ const empty = w - filled;
92
+ const barColor =
93
+ pctVal >= state.autoThreshold ? "error" :
94
+ pctVal >= state.warnThreshold ? "warning" : "success";
95
+ const bar = theme.fg(barColor, "â–ˆ".repeat(filled)) + D("â–‘".repeat(empty));
96
+
97
+ const tokenStr = tokens != null ? fmtTokens(tokens) : "?k";
98
+ const windowStr = window != null ? fmtTokens(window) : "?k";
99
+
100
+ const health = pctVal >= state.autoThreshold
101
+ ? E("âš  COMPACT NOW")
102
+ : pctVal >= state.warnThreshold
103
+ ? W("âš¡ Warning")
104
+ : S("✓ Healthy");
105
+
106
+ const compacted = state.compactedCount > 0
107
+ ? D(` | ${state.compactedCount} compacted`)
108
+ : "";
109
+
110
+ lines.push(
111
+ `🦜 ${bar} ${tokenStr}/${windowStr} ${health}${compacted} ${D("warn:")}${state.warnThreshold}% ${D("auto:")}${state.autoThreshold}%`,
112
+ );
113
+
114
+ return lines;
115
+ },
116
+ };
117
+ });
118
+ });
119
+
120
+ // ---- Detect context pressure ----
121
+ pi.on("turn_end", async (_event, ctx) => {
122
+ if (!state.enabled || inContextLock) return;
123
+
124
+ const cu = (ctx as any).getContextUsage?.();
125
+ if (!cu || cu.percent == null) return;
126
+
127
+ const pct = cu.percent;
128
+ const tokens = cu.tokens;
129
+
130
+ // Warning at warnThreshold — only warn once per crossing
131
+ if (pct >= state.warnThreshold && pct < state.autoThreshold && state.lastWarningAt < state.warnThreshold) {
132
+ ctx.ui.notify(
133
+ `🦜 Context at ${pct(pct)} — ${fmtTokens(tokens)}/${fmtTokens(cu.contextWindow)}. Consider /compact soon.`,
134
+ "warning",
135
+ );
136
+ state.lastWarningAt = pct;
137
+ tuiRef?.requestRender();
138
+ }
139
+
140
+ // Auto-compact at autoThreshold
141
+ if (pct >= state.autoThreshold && !inContextLock) {
142
+ inContextLock = true;
143
+ ctx.ui.notify(
144
+ `🦜 Context at ${pct(pct)} — auto-compacting to preserve history...`,
145
+ "info",
146
+ );
147
+
148
+ try {
149
+ let done = false;
150
+ let errorMsg = "";
151
+
152
+ ctx.compact({
153
+ customInstructions:
154
+ "Summarize the conversation so far, preserving all key decisions, file changes, and the user's current goal. Be concise but complete.",
155
+ onComplete: () => {
156
+ done = true;
157
+ state.compactedCount++;
158
+ state.lastWarningAt = 0;
159
+ persistState(ctx);
160
+ tuiRef?.requestRender();
161
+ },
162
+ onError: (err: Error) => {
163
+ errorMsg = err.message;
164
+ done = true;
165
+ },
166
+ });
167
+
168
+ // Wait for compaction to complete
169
+ let waited = 0;
170
+ while (!done && waited < 30000) {
171
+ await new Promise((r) => setTimeout(r, 200));
172
+ waited += 200;
173
+ }
174
+
175
+ if (errorMsg) {
176
+ ctx.ui.notify(`🦜 Compaction failed: ${errorMsg.slice(0, 100)}`, "error");
177
+ } else if (done) {
178
+ ctx.ui.notify(
179
+ `🦜 Compaction complete (${state.compactedCount} total). Context freed.`,
180
+ "success",
181
+ );
182
+ }
183
+ } finally {
184
+ inContextLock = false;
185
+ }
186
+ }
187
+ });
188
+
189
+ // ---- Commands ----
190
+
191
+ pi.registerCommand({
192
+ name: "whisper-enable",
193
+ description: "Enable the Context Whisperer auto-compaction",
194
+ async handler(_args: string[], ctx: any) {
195
+ state.enabled = true;
196
+ persistState(ctx);
197
+ tuiRef?.requestRender();
198
+ ctx.ui.notify("🦜 Context Whisperer enabled", "success");
199
+ },
200
+ });
201
+
202
+ pi.registerCommand({
203
+ name: "whisper-disable",
204
+ description: "Disable the Context Whisperer (manual compaction only)",
205
+ async handler(_args: string[], ctx: any) {
206
+ state.enabled = false;
207
+ persistState(ctx);
208
+ tuiRef?.requestRender();
209
+ ctx.ui.notify("🦜 Context Whisperer disabled", "info");
210
+ },
211
+ });
212
+
213
+ pi.registerCommand({
214
+ name: "whisper-stats",
215
+ description: "Show Context Whisperer statistics",
216
+ async handler(_args: string[], ctx: any) {
217
+ const cu = (ctx as any).getContextUsage?.();
218
+ const pct = cu?.percent ?? null;
219
+ ctx.ui.notify(
220
+ `🦜 Whisperer: ${state.enabled ? "on" : "off"} | Compactions: ${state.compactedCount} | Context: ${pct != null ? pct(pct) : "?"} | Warn: ${state.warnThreshold}% | Auto: ${state.autoThreshold}%`,
221
+ "info",
222
+ );
223
+ },
224
+ });
225
+
226
+ pi.registerCommand({
227
+ name: "whisper-config",
228
+ description: "Set Context Whisperer thresholds. Usage: /whisper-config <warnPct> <autoPct>",
229
+ async handler(_args: string[], ctx: any) {
230
+ const warnPct = parseInt(_args[0]);
231
+ const autoPct = parseInt(_args[1]);
232
+
233
+ if (isNaN(warnPct) || warnPct < 30 || warnPct > 95) {
234
+ ctx.ui.notify("Warn threshold must be between 30 and 95", "error");
235
+ return;
236
+ }
237
+ if (isNaN(autoPct) || autoPct < 40 || autoPct > 98) {
238
+ ctx.ui.notify("Auto threshold must be between 40 and 98", "error");
239
+ return;
240
+ }
241
+ if (autoPct <= warnPct) {
242
+ ctx.ui.notify("Auto threshold must be higher than warn threshold", "error");
243
+ return;
244
+ }
245
+
246
+ state.warnThreshold = warnPct;
247
+ state.autoThreshold = autoPct;
248
+ persistState(ctx);
249
+ tuiRef?.requestRender();
250
+ ctx.ui.notify(`🦜 Whisperer: warn at ${warnPct}%, auto-compact at ${autoPct}%`, "success");
251
+ },
252
+ });
253
+
254
+ // ---- LLM Tools ----
255
+
256
+ pi.registerTool({
257
+ name: "context_health",
258
+ description: "Check current context window usage percentage and token counts. Use when concerned about approaching limits.",
259
+ parameters: { type: "object", properties: {} },
260
+ async execute(_toolCallId: any, _args: any, _signal: any, _onUpdate: any, ctx: any) {
261
+ const cu = (ctx as any).getContextUsage?.();
262
+ if (!cu || cu.percent == null) return "Context usage data not available yet.";
263
+ const status =
264
+ cu.percent >= state.autoThreshold ? "CRITICAL — compaction recommended" :
265
+ cu.percent >= state.warnThreshold ? "WARNING — approaching limit" :
266
+ "HEALTHY";
267
+ return `Context: ${fmtTokens(cu.tokens)}/${fmtTokens(cu.contextWindow)} (${pct(cu.percent)}) — ${status}. Whisperer: ${state.enabled ? "on" : "off"} (${state.compactedCount} compactions this session).`;
268
+ },
269
+ });
270
+
271
+ // ---- Keyboard shortcut ----
272
+ // Ctrl+Shift+C: force compact now
273
+ pi.registerShortcut("c", { ctrl: true, shift: true }, (_event: any, ctx: any) => {
274
+ if (!ctx.hasUI || !state.enabled || inContextLock) return;
275
+ inContextLock = true;
276
+ ctx.compact({
277
+ customInstructions: "Summarize the conversation so far concisely.",
278
+ onComplete: () => {
279
+ state.compactedCount++;
280
+ state.lastWarningAt = 0;
281
+ persistState(ctx);
282
+ inContextLock = false;
283
+ tuiRef?.requestRender();
284
+ },
285
+ onError: () => {
286
+ inContextLock = false;
287
+ },
288
+ });
289
+ });
290
+ }
291
+
292
+ // ---- Persistence ----
293
+
294
+ function persistState(ctx: any) {
295
+ try {
296
+ ctx.sessionManager.appendEntry?.({
297
+ type: "custom",
298
+ customType: STORAGE_KEY,
299
+ data: { ...state },
300
+ });
301
+ } catch {
302
+ // Ignore persistence errors
303
+ }
304
+ }
package/package.json ADDED
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "pi-context-whisperer",
3
+ "version": "1.0.0",
4
+ "description": "Smart gradual auto-compaction for Pi — summarizes conversation mid-session before context limits hit, preserving key decisions",
5
+ "keywords": ["pi-package", "pi-extension", "context", "compaction", "auto-summarize", "token-management"],
6
+ "author": { "name": "Jay Rathod", "url": "https://github.com/Jaraxxxx" },
7
+ "repository": { "type": "git", "url": "https://github.com/Jaraxxxx/pi-context-whisperer" },
8
+ "license": "MIT",
9
+ "type": "module",
10
+ "pi": { "extensions": ["./index.ts"] },
11
+ "peerDependencies": {
12
+ "@earendil-works/pi-ai": "*",
13
+ "@earendil-works/pi-coding-agent": "*",
14
+ "@earendil-works/pi-tui": "*"
15
+ }
16
+ }