@pi-unipi/compactor 0.1.1

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 (65) hide show
  1. package/README.md +86 -0
  2. package/package.json +54 -0
  3. package/skills/compactor/SKILL.md +74 -0
  4. package/skills/compactor-doctor/SKILL.md +74 -0
  5. package/skills/compactor-ops/SKILL.md +65 -0
  6. package/skills/compactor-stats/SKILL.md +49 -0
  7. package/skills/compactor-tools/SKILL.md +120 -0
  8. package/src/commands/index.ts +248 -0
  9. package/src/compaction/brief.ts +334 -0
  10. package/src/compaction/build-sections.ts +77 -0
  11. package/src/compaction/content.ts +47 -0
  12. package/src/compaction/cut.ts +80 -0
  13. package/src/compaction/extract/commits.ts +52 -0
  14. package/src/compaction/extract/files.ts +58 -0
  15. package/src/compaction/extract/goals.ts +36 -0
  16. package/src/compaction/extract/preferences.ts +40 -0
  17. package/src/compaction/filter-noise.ts +46 -0
  18. package/src/compaction/format.ts +48 -0
  19. package/src/compaction/hooks.ts +145 -0
  20. package/src/compaction/merge.ts +113 -0
  21. package/src/compaction/normalize.ts +68 -0
  22. package/src/compaction/recall-scope.ts +32 -0
  23. package/src/compaction/sanitize.ts +12 -0
  24. package/src/compaction/search-entries.ts +101 -0
  25. package/src/compaction/sections.ts +15 -0
  26. package/src/compaction/summarize.ts +29 -0
  27. package/src/config/manager.ts +89 -0
  28. package/src/config/presets.ts +83 -0
  29. package/src/config/schema.ts +55 -0
  30. package/src/display/bash-display.ts +28 -0
  31. package/src/display/diff-presentation.ts +20 -0
  32. package/src/display/diff-renderer.ts +255 -0
  33. package/src/display/line-width-safety.ts +16 -0
  34. package/src/display/pending-diff-preview.ts +51 -0
  35. package/src/display/render-utils.ts +52 -0
  36. package/src/display/thinking-label.ts +18 -0
  37. package/src/display/tool-overrides.ts +136 -0
  38. package/src/display/user-message-box.ts +16 -0
  39. package/src/executor/executor.ts +242 -0
  40. package/src/executor/runtime.ts +125 -0
  41. package/src/index.ts +211 -0
  42. package/src/info-screen.ts +60 -0
  43. package/src/security/evaluator.ts +142 -0
  44. package/src/security/policy.ts +74 -0
  45. package/src/security/scanner.ts +65 -0
  46. package/src/session/db.ts +237 -0
  47. package/src/session/extract.ts +107 -0
  48. package/src/session/resume-inject.ts +25 -0
  49. package/src/session/snapshot.ts +326 -0
  50. package/src/store/chunking.ts +126 -0
  51. package/src/store/db-base.ts +79 -0
  52. package/src/store/index.ts +364 -0
  53. package/src/tools/compact.ts +20 -0
  54. package/src/tools/ctx-batch-execute.ts +53 -0
  55. package/src/tools/ctx-doctor.ts +78 -0
  56. package/src/tools/ctx-execute-file.ts +26 -0
  57. package/src/tools/ctx-execute.ts +21 -0
  58. package/src/tools/ctx-fetch-and-index.ts +37 -0
  59. package/src/tools/ctx-index.ts +42 -0
  60. package/src/tools/ctx-search.ts +23 -0
  61. package/src/tools/ctx-stats.ts +37 -0
  62. package/src/tools/register.ts +360 -0
  63. package/src/tools/vcc-recall.ts +64 -0
  64. package/src/tui/settings-overlay.ts +290 -0
  65. package/src/types.ts +269 -0
@@ -0,0 +1,37 @@
1
+ /**
2
+ * ctx_stats tool — context savings dashboard
3
+ */
4
+
5
+ import type { SessionDB } from "../session/db.js";
6
+ import type { ContentStore } from "../store/index.js";
7
+
8
+ export interface CtxStatsResult {
9
+ sessionEvents: number;
10
+ compactions: number;
11
+ tokensSaved: number;
12
+ compressionRatio: string;
13
+ indexedDocs: number;
14
+ indexedChunks: number;
15
+ sandboxRuns: number;
16
+ searchQueries: number;
17
+ }
18
+
19
+ export async function ctxStats(
20
+ sessionDB: SessionDB,
21
+ contentStore: ContentStore,
22
+ sessionId: string,
23
+ ): Promise<CtxStatsResult> {
24
+ const sessionStats = sessionDB.getSessionStats(sessionId);
25
+ const storeStats = await contentStore.getStats();
26
+
27
+ return {
28
+ sessionEvents: sessionStats?.event_count ?? 0,
29
+ compactions: sessionStats?.compact_count ?? 0,
30
+ tokensSaved: 0, // populated by caller from compaction stats
31
+ compressionRatio: "N/A",
32
+ indexedDocs: storeStats.sources,
33
+ indexedChunks: storeStats.chunks,
34
+ sandboxRuns: 0,
35
+ searchQueries: 0,
36
+ };
37
+ }
@@ -0,0 +1,360 @@
1
+ /**
2
+ * Tool registration — register all compactor tools with Pi's ExtensionAPI
3
+ *
4
+ * Each tool is registered via pi.registerTool() with proper TypeBox schemas
5
+ * so the LLM can discover and invoke them.
6
+ */
7
+
8
+ import { Type, type Static } from "@sinclair/typebox";
9
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
10
+ import { compactTool } from "./compact.js";
11
+ import { vccRecall, type RecallInput } from "./vcc-recall.js";
12
+ import { ctxExecute, type CtxExecuteInput } from "./ctx-execute.js";
13
+ import { ctxExecuteFile, type CtxExecuteFileInput } from "./ctx-execute-file.js";
14
+ import { ctxBatchExecute, type BatchItem } from "./ctx-batch-execute.js";
15
+ import { ctxIndex, type CtxIndexInput } from "./ctx-index.js";
16
+ import { ctxSearch, type CtxSearchInput } from "./ctx-search.js";
17
+ import { ctxFetchAndIndex, type CtxFetchAndIndexInput } from "./ctx-fetch-and-index.js";
18
+ import { ctxStats, type CtxStatsResult } from "./ctx-stats.js";
19
+ import { ctxDoctor, type DoctorResult } from "./ctx-doctor.js";
20
+ import type { SessionDB } from "../session/db.js";
21
+ import type { ContentStore } from "../store/index.js";
22
+ import type { NormalizedBlock } from "../types.js";
23
+
24
+ // --- TypeBox Schemas for each tool ---
25
+
26
+ const LanguageSchema = Type.Union([
27
+ Type.Literal("javascript"),
28
+ Type.Literal("typescript"),
29
+ Type.Literal("python"),
30
+ Type.Literal("shell"),
31
+ Type.Literal("ruby"),
32
+ Type.Literal("go"),
33
+ Type.Literal("rust"),
34
+ Type.Literal("php"),
35
+ Type.Literal("perl"),
36
+ Type.Literal("r"),
37
+ Type.Literal("elixir"),
38
+ ]);
39
+
40
+ const CompactParams = Type.Object({});
41
+
42
+ const VccRecallParams = Type.Object({
43
+ query: Type.String({ description: "Search query for session history" }),
44
+ mode: Type.Optional(Type.Union([Type.Literal("bm25"), Type.Literal("regex")], {
45
+ description: "Search mode: bm25 (default) or regex fallback",
46
+ })),
47
+ limit: Type.Optional(Type.Number({ description: "Max results to return (default 10)", minimum: 1 })),
48
+ offset: Type.Optional(Type.Number({ description: "Pagination offset", minimum: 0 })),
49
+ expand: Type.Optional(Type.Boolean({ description: "Return full message content for hits" })),
50
+ });
51
+
52
+ const CtxExecuteParams = Type.Object({
53
+ language: LanguageSchema,
54
+ code: Type.String({ description: "Code to execute in the sandbox" }),
55
+ timeout: Type.Optional(Type.Number({ description: "Timeout in ms (default 30000)", minimum: 1000 })),
56
+ });
57
+
58
+ const CtxExecuteFileParams = Type.Object({
59
+ language: LanguageSchema,
60
+ path: Type.String({ description: "Path to file to execute" }),
61
+ timeout: Type.Optional(Type.Number({ description: "Timeout in ms (default 30000)", minimum: 1000 })),
62
+ });
63
+
64
+ const CtxBatchExecuteParams = Type.Object({
65
+ items: Type.Array(
66
+ Type.Union([
67
+ Type.Object({
68
+ type: Type.Literal("execute"),
69
+ language: LanguageSchema,
70
+ code: Type.String(),
71
+ timeout: Type.Optional(Type.Number()),
72
+ }),
73
+ Type.Object({
74
+ type: Type.Literal("search"),
75
+ query: Type.String(),
76
+ limit: Type.Optional(Type.Number()),
77
+ }),
78
+ ]),
79
+ { description: "Array of execute or search commands to run atomically" },
80
+ ),
81
+ });
82
+
83
+ const CtxIndexParams = Type.Object({
84
+ label: Type.String({ description: "Label for the indexed content" }),
85
+ content: Type.Optional(Type.String({ description: "Content to index (or use filePath)" })),
86
+ filePath: Type.Optional(Type.String({ description: "Path to file to index" })),
87
+ contentType: Type.Optional(Type.Union([
88
+ Type.Literal("markdown"),
89
+ Type.Literal("json"),
90
+ Type.Literal("plain"),
91
+ ], { description: "Content type for chunking strategy" })),
92
+ chunkSize: Type.Optional(Type.Number({ description: "Chunk size in characters", minimum: 100 })),
93
+ });
94
+
95
+ const CtxSearchParams = Type.Object({
96
+ query: Type.String({ description: "Search query against indexed content" }),
97
+ limit: Type.Optional(Type.Number({ description: "Max results (default 10)", minimum: 1 })),
98
+ offset: Type.Optional(Type.Number({ description: "Pagination offset", minimum: 0 })),
99
+ });
100
+
101
+ const CtxFetchAndIndexParams = Type.Object({
102
+ url: Type.String({ description: "URL to fetch, convert to markdown, and index" }),
103
+ label: Type.Optional(Type.String({ description: "Label for the indexed content" })),
104
+ chunkSize: Type.Optional(Type.Number({ description: "Chunk size in characters", minimum: 100 })),
105
+ });
106
+
107
+ const CtxStatsParams = Type.Object({});
108
+
109
+ const CtxDoctorParams = Type.Object({});
110
+
111
+ // --- Helpers ---
112
+
113
+ function textResult(text: string, details?: Record<string, unknown>): any {
114
+ return {
115
+ content: [{ type: "text", text }],
116
+ details,
117
+ };
118
+ }
119
+
120
+ function jsonResult(data: unknown, label?: string): any {
121
+ const text = label ? `${label}:\n${JSON.stringify(data, null, 2)}` : JSON.stringify(data, null, 2);
122
+ return {
123
+ content: [{ type: "text", text }],
124
+ details: data as Record<string, unknown>,
125
+ };
126
+ }
127
+
128
+ // --- Registration ---
129
+
130
+ export interface CompactorToolDeps {
131
+ sessionDB: SessionDB;
132
+ contentStore: ContentStore | null;
133
+ getSessionId: () => string;
134
+ getBlocks: () => NormalizedBlock[];
135
+ }
136
+
137
+ /**
138
+ * Register all compactor tools with Pi's ExtensionAPI.
139
+ * Call this during session_start after services are initialized.
140
+ */
141
+ export function registerCompactorTools(pi: ExtensionAPI, deps: CompactorToolDeps): void {
142
+ // 1. compact — trigger manual compaction
143
+ pi.registerTool({
144
+ name: "compact",
145
+ label: "Compact",
146
+ description: "Trigger manual context compaction. Reduces session history while preserving continuity.",
147
+ parameters: CompactParams,
148
+ async execute(): Promise<any> {
149
+ const result = compactTool();
150
+ return jsonResult(result, "Compaction triggered");
151
+ },
152
+ });
153
+
154
+ // 2. vcc_recall — search session history
155
+ pi.registerTool({
156
+ name: "vcc_recall",
157
+ label: "Recall",
158
+ description:
159
+ "Search session history using BM25 or regex. Find previous goals, files, commits, and context.",
160
+ parameters: VccRecallParams,
161
+ async execute(_toolCallId, params: Static<typeof VccRecallParams>): Promise<any> {
162
+ const blocks = deps.getBlocks();
163
+ const input: RecallInput = {
164
+ query: params.query,
165
+ mode: params.mode,
166
+ limit: params.limit,
167
+ offset: params.offset,
168
+ expand: params.expand,
169
+ };
170
+ const result = vccRecall(blocks, input);
171
+ if (result.hits.length === 0) {
172
+ return textResult(`No results found for "${result.query}".`);
173
+ }
174
+ const lines = result.hits.map(
175
+ (h, i) =>
176
+ `[${i + 1}/${result.total}] score=${h.score.toFixed(2)} kind=${h.kind}\n${h.text}`,
177
+ );
178
+ return textResult(
179
+ `Found ${result.total} results for "${result.query}":\n\n${lines.join("\n\n")}`,
180
+ result as unknown as Record<string, unknown>,
181
+ );
182
+ },
183
+ });
184
+
185
+ // 3. ctx_execute — run code in sandbox
186
+ pi.registerTool({
187
+ name: "ctx_execute",
188
+ label: "Execute",
189
+ description:
190
+ "Run code in a sandboxed environment. Supports 11 languages. Only stdout enters context.",
191
+ parameters: CtxExecuteParams,
192
+ async execute(_toolCallId, params: Static<typeof CtxExecuteParams>): Promise<any> {
193
+ try {
194
+ const result = await ctxExecute(params as CtxExecuteInput);
195
+ const parts: string[] = [];
196
+ if (result.stdout) parts.push(result.stdout);
197
+ if (result.stderr) parts.push(`[stderr] ${result.stderr}`);
198
+ if (result.timedOut) parts.push("[timed out]");
199
+ if (result.exitCode !== 0) parts.push(`[exit code: ${result.exitCode}]`);
200
+ return textResult(parts.join("\n") || "(no output)", result as unknown as Record<string, unknown>);
201
+ } catch (err) {
202
+ return textResult(`Execution error: ${err}`, { error: true });
203
+ }
204
+ },
205
+ });
206
+
207
+ // 4. ctx_execute_file — execute file with FILE_CONTENT
208
+ pi.registerTool({
209
+ name: "ctx_execute_file",
210
+ label: "Execute File",
211
+ description: "Execute a file in the sandbox. File content is injected as FILE_CONTENT variable.",
212
+ parameters: CtxExecuteFileParams,
213
+ async execute(_toolCallId, params: Static<typeof CtxExecuteFileParams>): Promise<any> {
214
+ try {
215
+ const result = await ctxExecuteFile(params as CtxExecuteFileInput);
216
+ const parts: string[] = [];
217
+ if (result.stdout) parts.push(result.stdout);
218
+ if (result.stderr) parts.push(`[stderr] ${result.stderr}`);
219
+ if (result.timedOut) parts.push("[timed out]");
220
+ return textResult(parts.join("\n") || "(no output)", result as unknown as Record<string, unknown>);
221
+ } catch (err) {
222
+ return textResult(`Execution error: ${err}`, { error: true });
223
+ }
224
+ },
225
+ });
226
+
227
+ // 5. ctx_batch_execute — atomic batch
228
+ pi.registerTool({
229
+ name: "ctx_batch_execute",
230
+ label: "Batch Execute",
231
+ description: "Run multiple code executions and searches atomically as a batch.",
232
+ parameters: CtxBatchExecuteParams,
233
+ async execute(_toolCallId, params: Static<typeof CtxBatchExecuteParams>): Promise<any> {
234
+ try {
235
+ const result = await ctxBatchExecute(params.items as BatchItem[]);
236
+ const summaries = result.results.map((r, i) => {
237
+ if (r.type === "execute") {
238
+ const s = r.result.stdout?.slice(0, 200) || "(no output)";
239
+ return `[${i}] execute → ${r.result.exitCode === 0 ? "ok" : "fail"}: ${s}`;
240
+ }
241
+ return `[${i}] search → ${r.results.length} results`;
242
+ });
243
+ return textResult(`Batch results (${result.results.length} items):\n${summaries.join("\n")}`, result as unknown as Record<string, unknown>);
244
+ } catch (err) {
245
+ return textResult(`Batch error: ${err}`, { error: true });
246
+ }
247
+ },
248
+ });
249
+
250
+ // 6. ctx_index — index content into FTS5
251
+ pi.registerTool({
252
+ name: "ctx_index",
253
+ label: "Index",
254
+ description: "Chunk content or a file and index into FTS5 for fast search.",
255
+ parameters: CtxIndexParams,
256
+ async execute(_toolCallId, params: Static<typeof CtxIndexParams>): Promise<any> {
257
+ try {
258
+ const result = await ctxIndex(params as CtxIndexInput);
259
+ return textResult(
260
+ `Indexed "${result.label}": ${result.totalChunks} chunks (${result.codeChunks} code)`,
261
+ result as unknown as Record<string, unknown>,
262
+ );
263
+ } catch (err) {
264
+ return textResult(`Index error: ${err}`, { error: true });
265
+ }
266
+ },
267
+ });
268
+
269
+ // 7. ctx_search — query FTS5 content store
270
+ pi.registerTool({
271
+ name: "ctx_search",
272
+ label: "Search",
273
+ description: "Search indexed content using FTS5 full-text search.",
274
+ parameters: CtxSearchParams,
275
+ async execute(_toolCallId, params: Static<typeof CtxSearchParams>): Promise<any> {
276
+ try {
277
+ const results = await ctxSearch(params as CtxSearchInput);
278
+ if (results.length === 0) {
279
+ return textResult(`No results for "${params.query}".`);
280
+ }
281
+ const lines = results.map(
282
+ (r, i) =>
283
+ `[${i + 1}] ${r.title} (rank: ${r.rank.toFixed(3)})\n${r.content.slice(0, 300)}`,
284
+ );
285
+ return textResult(
286
+ `Found ${results.length} results:\n\n${lines.join("\n\n")}`,
287
+ { results } as unknown as Record<string, unknown>,
288
+ );
289
+ } catch (err) {
290
+ return textResult(`Search error: ${err}`, { error: true });
291
+ }
292
+ },
293
+ });
294
+
295
+ // 8. ctx_fetch_and_index — fetch URL → markdown → index
296
+ pi.registerTool({
297
+ name: "ctx_fetch_and_index",
298
+ label: "Fetch & Index",
299
+ description: "Fetch a URL, convert to markdown, and index into FTS5 content store.",
300
+ parameters: CtxFetchAndIndexParams,
301
+ async execute(_toolCallId, params: Static<typeof CtxFetchAndIndexParams>): Promise<any> {
302
+ try {
303
+ const result = await ctxFetchAndIndex(params as CtxFetchAndIndexInput);
304
+ return textResult(
305
+ `Fetched and indexed "${result.label}": ${result.totalChunks} chunks`,
306
+ result as unknown as Record<string, unknown>,
307
+ );
308
+ } catch (err) {
309
+ return textResult(`Fetch error: ${err}`, { error: true });
310
+ }
311
+ },
312
+ });
313
+
314
+ // 9. ctx_stats — context savings dashboard
315
+ pi.registerTool({
316
+ name: "ctx_stats",
317
+ label: "Stats",
318
+ description: "Show context savings dashboard — session events, compactions, indexed content.",
319
+ parameters: CtxStatsParams,
320
+ async execute(): Promise<any> {
321
+ try {
322
+ const result = await ctxStats(deps.sessionDB, deps.contentStore!, deps.getSessionId());
323
+ const lines = [
324
+ `📊 Compactor Stats`,
325
+ `Session events: ${result.sessionEvents}`,
326
+ `Compactions: ${result.compactions}`,
327
+ `Tokens saved: ${result.tokensSaved}`,
328
+ `Indexed docs: ${result.indexedDocs} (${result.indexedChunks} chunks)`,
329
+ `Sandbox runs: ${result.sandboxRuns}`,
330
+ `Search queries: ${result.searchQueries}`,
331
+ ];
332
+ return textResult(lines.join("\n"), result as unknown as Record<string, unknown>);
333
+ } catch (err) {
334
+ return textResult(`Stats error: ${err}`, { error: true });
335
+ }
336
+ },
337
+ });
338
+
339
+ // 10. ctx_doctor — diagnostics checklist
340
+ pi.registerTool({
341
+ name: "ctx_doctor",
342
+ label: "Doctor",
343
+ description: "Run diagnostics checklist — validate config, DB, FTS5, runtimes.",
344
+ parameters: CtxDoctorParams,
345
+ async execute(): Promise<any> {
346
+ try {
347
+ const result = await ctxDoctor(deps.sessionDB, deps.contentStore!);
348
+ const icon = (s: string) => (s === "pass" ? "✅" : s === "warn" ? "⚠️" : "❌");
349
+ const lines = [
350
+ result.healthy ? "🩺 All checks passed" : "🩺 Issues found",
351
+ "",
352
+ ...result.checks.map((c) => `${icon(c.status)} ${c.name}: ${c.message}`),
353
+ ];
354
+ return jsonResult(result, lines.join("\n"));
355
+ } catch (err) {
356
+ return textResult(`Doctor error: ${err}`, { error: true });
357
+ }
358
+ },
359
+ });
360
+ }
@@ -0,0 +1,64 @@
1
+ /**
2
+ * vcc_recall tool — BM25-lite session history search
3
+ */
4
+
5
+ import type { NormalizedBlock } from "../types.js";
6
+ import { searchEntries } from "../compaction/search-entries.js";
7
+
8
+ export interface RecallInput {
9
+ query: string;
10
+ mode?: "bm25" | "regex";
11
+ limit?: number;
12
+ offset?: number;
13
+ expand?: boolean;
14
+ }
15
+
16
+ export interface RecallResult {
17
+ hits: Array<{
18
+ index: number;
19
+ score: number;
20
+ text: string;
21
+ kind: string;
22
+ }>;
23
+ total: number;
24
+ query: string;
25
+ }
26
+
27
+ export function vccRecall(
28
+ blocks: NormalizedBlock[],
29
+ input: RecallInput,
30
+ ): RecallResult {
31
+ const { query, mode = "bm25", limit = 10, offset = 0, expand = false } = input;
32
+
33
+ let hits: Array<{ index: number; score: number; text: string; kind: string }> = [];
34
+
35
+ if (mode === "bm25") {
36
+ const results = searchEntries(blocks, query, { limit: limit + offset, offset: 0 });
37
+ hits = results.map((r, i) => ({
38
+ index: r.docId,
39
+ score: r.score,
40
+ text: expand ? r.text : r.text.slice(0, 200),
41
+ kind: r.kind,
42
+ }));
43
+ } else {
44
+ // Regex fallback
45
+ const re = new RegExp(query, "i");
46
+ for (let i = 0; i < blocks.length; i++) {
47
+ const b = blocks[i];
48
+ const text = b.kind === "tool_call" ? `${b.name} ${JSON.stringify(b.args)}` : b.kind === "tool_result" ? `${b.name} ${b.text}` : b.text;
49
+ if (re.test(text)) {
50
+ hits.push({
51
+ index: i,
52
+ score: 1,
53
+ text: expand ? text : text.slice(0, 200),
54
+ kind: b.kind,
55
+ });
56
+ }
57
+ }
58
+ }
59
+
60
+ const total = hits.length;
61
+ const paginated = hits.slice(offset, offset + limit);
62
+
63
+ return { hits: paginated, total, query };
64
+ }