@pi-unipi/compactor 0.1.6 → 0.2.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.
- package/README.md +50 -24
- package/index.ts +7 -0
- package/package.json +2 -1
- package/skills/compactor/SKILL.md +21 -65
- package/skills/compactor-detail/SKILL.md +133 -0
- package/src/commands/index.ts +186 -109
- package/src/compaction/filter-noise.ts +4 -3
- package/src/compaction/hooks.ts +22 -1
- package/src/compaction/search-entries.ts +51 -4
- package/src/config/manager.ts +55 -6
- package/src/config/presets.ts +69 -5
- package/src/config/schema.ts +9 -0
- package/src/index.ts +183 -10
- package/src/info-screen.ts +10 -4
- package/src/security/policy.ts +23 -0
- package/src/session/auto-inject.ts +60 -0
- package/src/session/db.ts +70 -9
- package/src/session/resume-inject.ts +13 -1
- package/src/store/db-base.ts +11 -0
- package/src/store/index.ts +150 -4
- package/src/store/unified.ts +109 -0
- package/src/tools/context-budget.ts +50 -0
- package/src/tools/ctx-batch-execute.ts +2 -5
- package/src/tools/ctx-fetch-and-index.ts +3 -8
- package/src/tools/ctx-index.ts +3 -9
- package/src/tools/ctx-search.ts +3 -7
- package/src/tools/ctx-stats.ts +6 -4
- package/src/tools/register.ts +251 -216
- package/src/tui/settings-overlay.ts +359 -149
- package/src/types.ts +25 -1
- package/skills/compactor-ops/SKILL.md +0 -65
- package/skills/compactor-tools/SKILL.md +0 -120
package/src/tools/ctx-index.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* ctx_index tool — chunk content → index into FTS5
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import { ContentStore } from "../store/index.js";
|
|
5
|
+
import type { ContentStore } from "../store/index.js";
|
|
6
6
|
import type { IndexResult } from "../types.js";
|
|
7
7
|
import { readFileSync } from "node:fs";
|
|
8
8
|
|
|
@@ -14,10 +14,7 @@ export interface CtxIndexInput {
|
|
|
14
14
|
chunkSize?: number;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
export async function ctxIndex(input: CtxIndexInput): Promise<IndexResult> {
|
|
18
|
-
const store = new ContentStore();
|
|
19
|
-
await store.init();
|
|
20
|
-
|
|
17
|
+
export async function ctxIndex(store: ContentStore, input: CtxIndexInput): Promise<IndexResult> {
|
|
21
18
|
let text: string;
|
|
22
19
|
let source: string;
|
|
23
20
|
|
|
@@ -31,12 +28,9 @@ export async function ctxIndex(input: CtxIndexInput): Promise<IndexResult> {
|
|
|
31
28
|
throw new Error("Either content or filePath must be provided");
|
|
32
29
|
}
|
|
33
30
|
|
|
34
|
-
|
|
31
|
+
return store.index(input.label, text, {
|
|
35
32
|
contentType: input.contentType ?? "plain",
|
|
36
33
|
source,
|
|
37
34
|
chunkSize: input.chunkSize,
|
|
38
35
|
});
|
|
39
|
-
|
|
40
|
-
store.close();
|
|
41
|
-
return result;
|
|
42
36
|
}
|
package/src/tools/ctx-search.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* ctx_search tool — query indexed content
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import { ContentStore } from "../store/index.js";
|
|
5
|
+
import type { ContentStore } from "../store/index.js";
|
|
6
6
|
import type { SearchResult } from "../types.js";
|
|
7
7
|
|
|
8
8
|
export interface CtxSearchInput {
|
|
@@ -11,13 +11,9 @@ export interface CtxSearchInput {
|
|
|
11
11
|
offset?: number;
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
export async function ctxSearch(input: CtxSearchInput): Promise<SearchResult[]> {
|
|
15
|
-
|
|
16
|
-
await store.init();
|
|
17
|
-
const results = await store.search(input.query, {
|
|
14
|
+
export async function ctxSearch(store: ContentStore, input: CtxSearchInput): Promise<SearchResult[]> {
|
|
15
|
+
return store.search(input.query, {
|
|
18
16
|
limit: input.limit ?? 10,
|
|
19
17
|
offset: input.offset ?? 0,
|
|
20
18
|
});
|
|
21
|
-
store.close();
|
|
22
|
-
return results;
|
|
23
19
|
}
|
package/src/tools/ctx-stats.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import type { SessionDB } from "../session/db.js";
|
|
6
6
|
import type { ContentStore } from "../store/index.js";
|
|
7
|
+
import type { RuntimeCounters } from "../types.js";
|
|
7
8
|
|
|
8
9
|
export interface CtxStatsResult {
|
|
9
10
|
sessionEvents: number;
|
|
@@ -20,18 +21,19 @@ export async function ctxStats(
|
|
|
20
21
|
sessionDB: SessionDB,
|
|
21
22
|
contentStore: ContentStore,
|
|
22
23
|
sessionId: string,
|
|
24
|
+
counters?: RuntimeCounters,
|
|
23
25
|
): Promise<CtxStatsResult> {
|
|
24
26
|
const sessionStats = sessionDB.getSessionStats(sessionId);
|
|
25
27
|
const storeStats = await contentStore.getStats();
|
|
26
28
|
|
|
27
29
|
return {
|
|
28
30
|
sessionEvents: sessionStats?.event_count ?? 0,
|
|
29
|
-
compactions: sessionStats?.compact_count ?? 0,
|
|
30
|
-
tokensSaved: 0,
|
|
31
|
+
compactions: counters?.compactions ?? sessionStats?.compact_count ?? 0,
|
|
32
|
+
tokensSaved: counters?.totalTokensCompacted ?? 0,
|
|
31
33
|
compressionRatio: "N/A",
|
|
32
34
|
indexedDocs: storeStats.sources,
|
|
33
35
|
indexedChunks: storeStats.chunks,
|
|
34
|
-
sandboxRuns: 0,
|
|
35
|
-
searchQueries: 0,
|
|
36
|
+
sandboxRuns: counters?.sandboxRuns ?? 0,
|
|
37
|
+
searchQueries: counters?.searchQueries ?? 0,
|
|
36
38
|
};
|
|
37
39
|
}
|
package/src/tools/register.ts
CHANGED
|
@@ -3,6 +3,14 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Each tool is registered via pi.registerTool() with proper TypeBox schemas
|
|
5
5
|
* so the LLM can discover and invoke them.
|
|
6
|
+
*
|
|
7
|
+
* New naming convention (v0.2.0):
|
|
8
|
+
* compact, session_recall, sandbox, sandbox_file, sandbox_batch,
|
|
9
|
+
* content_index, content_search, content_fetch, compactor_stats, compactor_doctor
|
|
10
|
+
*
|
|
11
|
+
* Old names kept as deprecated aliases for backward compatibility:
|
|
12
|
+
* vcc_recall, ctx_execute, ctx_execute_file, ctx_batch_execute,
|
|
13
|
+
* ctx_index, ctx_search, ctx_fetch_and_index, ctx_stats, ctx_doctor
|
|
6
14
|
*/
|
|
7
15
|
|
|
8
16
|
import { Type, type Static } from "@sinclair/typebox";
|
|
@@ -17,9 +25,10 @@ import { ctxSearch, type CtxSearchInput } from "./ctx-search.js";
|
|
|
17
25
|
import { ctxFetchAndIndex, type CtxFetchAndIndexInput } from "./ctx-fetch-and-index.js";
|
|
18
26
|
import { ctxStats, type CtxStatsResult } from "./ctx-stats.js";
|
|
19
27
|
import { ctxDoctor, type DoctorResult } from "./ctx-doctor.js";
|
|
28
|
+
import { contextBudgetTool } from "./context-budget.js";
|
|
20
29
|
import type { SessionDB } from "../session/db.js";
|
|
21
30
|
import type { ContentStore } from "../store/index.js";
|
|
22
|
-
import type { NormalizedBlock } from "../types.js";
|
|
31
|
+
import type { NormalizedBlock, RuntimeCounters } from "../types.js";
|
|
23
32
|
|
|
24
33
|
// --- TypeBox Schemas for each tool ---
|
|
25
34
|
|
|
@@ -37,9 +46,11 @@ const LanguageSchema = Type.Union([
|
|
|
37
46
|
Type.Literal("elixir"),
|
|
38
47
|
]);
|
|
39
48
|
|
|
40
|
-
const CompactParams = Type.Object({
|
|
49
|
+
const CompactParams = Type.Object({
|
|
50
|
+
dryRun: Type.Optional(Type.Boolean({ description: "If true, report what would be compacted without actually compacting" })),
|
|
51
|
+
});
|
|
41
52
|
|
|
42
|
-
const
|
|
53
|
+
const RecallParams = Type.Object({
|
|
43
54
|
query: Type.String({ description: "Search query for session history" }),
|
|
44
55
|
mode: Type.Optional(Type.Union([Type.Literal("bm25"), Type.Literal("regex")], {
|
|
45
56
|
description: "Search mode: bm25 (default) or regex fallback",
|
|
@@ -49,19 +60,19 @@ const VccRecallParams = Type.Object({
|
|
|
49
60
|
expand: Type.Optional(Type.Boolean({ description: "Return full message content for hits" })),
|
|
50
61
|
});
|
|
51
62
|
|
|
52
|
-
const
|
|
63
|
+
const SandboxParams = Type.Object({
|
|
53
64
|
language: LanguageSchema,
|
|
54
65
|
code: Type.String({ description: "Code to execute in the sandbox" }),
|
|
55
66
|
timeout: Type.Optional(Type.Number({ description: "Timeout in ms (default 30000)", minimum: 1000 })),
|
|
56
67
|
});
|
|
57
68
|
|
|
58
|
-
const
|
|
69
|
+
const SandboxFileParams = Type.Object({
|
|
59
70
|
language: LanguageSchema,
|
|
60
71
|
path: Type.String({ description: "Path to file to execute" }),
|
|
61
72
|
timeout: Type.Optional(Type.Number({ description: "Timeout in ms (default 30000)", minimum: 1000 })),
|
|
62
73
|
});
|
|
63
74
|
|
|
64
|
-
const
|
|
75
|
+
const SandboxBatchParams = Type.Object({
|
|
65
76
|
items: Type.Array(
|
|
66
77
|
Type.Union([
|
|
67
78
|
Type.Object({
|
|
@@ -80,7 +91,7 @@ const CtxBatchExecuteParams = Type.Object({
|
|
|
80
91
|
),
|
|
81
92
|
});
|
|
82
93
|
|
|
83
|
-
const
|
|
94
|
+
const ContentIndexParams = Type.Object({
|
|
84
95
|
label: Type.String({ description: "Label for the indexed content" }),
|
|
85
96
|
content: Type.Optional(Type.String({ description: "Content to index (or use filePath)" })),
|
|
86
97
|
filePath: Type.Optional(Type.String({ description: "Path to file to index" })),
|
|
@@ -92,21 +103,21 @@ const CtxIndexParams = Type.Object({
|
|
|
92
103
|
chunkSize: Type.Optional(Type.Number({ description: "Chunk size in characters", minimum: 100 })),
|
|
93
104
|
});
|
|
94
105
|
|
|
95
|
-
const
|
|
106
|
+
const ContentSearchParams = Type.Object({
|
|
96
107
|
query: Type.String({ description: "Search query against indexed content" }),
|
|
97
108
|
limit: Type.Optional(Type.Number({ description: "Max results (default 10)", minimum: 1 })),
|
|
98
109
|
offset: Type.Optional(Type.Number({ description: "Pagination offset", minimum: 0 })),
|
|
99
110
|
});
|
|
100
111
|
|
|
101
|
-
const
|
|
112
|
+
const ContentFetchParams = Type.Object({
|
|
102
113
|
url: Type.String({ description: "URL to fetch, convert to markdown, and index" }),
|
|
103
114
|
label: Type.Optional(Type.String({ description: "Label for the indexed content" })),
|
|
104
115
|
chunkSize: Type.Optional(Type.Number({ description: "Chunk size in characters", minimum: 100 })),
|
|
105
116
|
});
|
|
106
117
|
|
|
107
|
-
const
|
|
118
|
+
const StatsParams = Type.Object({});
|
|
108
119
|
|
|
109
|
-
const
|
|
120
|
+
const DoctorParams = Type.Object({});
|
|
110
121
|
|
|
111
122
|
// --- Helpers ---
|
|
112
123
|
|
|
@@ -125,6 +136,23 @@ function jsonResult(data: unknown, label?: string): any {
|
|
|
125
136
|
};
|
|
126
137
|
}
|
|
127
138
|
|
|
139
|
+
/** Log a deprecation warning when old tool names are used. */
|
|
140
|
+
function deprecationLog(oldName: string, newName: string): void {
|
|
141
|
+
console.error(`[compactor] DEPRECATED: Tool "${oldName}" used — use "${newName}" instead.`);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// --- Old schema names for backward compat aliases ---
|
|
145
|
+
|
|
146
|
+
const VccRecallParams = RecallParams;
|
|
147
|
+
const CtxExecuteParams = SandboxParams;
|
|
148
|
+
const CtxExecuteFileParams = SandboxFileParams;
|
|
149
|
+
const CtxBatchExecuteParams = SandboxBatchParams;
|
|
150
|
+
const CtxIndexParams = ContentIndexParams;
|
|
151
|
+
const CtxSearchParams = ContentSearchParams;
|
|
152
|
+
const CtxFetchAndIndexParams = ContentFetchParams;
|
|
153
|
+
const CtxStatsParams = StatsParams;
|
|
154
|
+
const CtxDoctorParams = DoctorParams;
|
|
155
|
+
|
|
128
156
|
// --- Registration ---
|
|
129
157
|
|
|
130
158
|
export interface CompactorToolDeps {
|
|
@@ -132,6 +160,7 @@ export interface CompactorToolDeps {
|
|
|
132
160
|
contentStore: ContentStore | null;
|
|
133
161
|
getSessionId: () => string;
|
|
134
162
|
getBlocks: () => NormalizedBlock[];
|
|
163
|
+
getCounters?: () => RuntimeCounters;
|
|
135
164
|
}
|
|
136
165
|
|
|
137
166
|
/**
|
|
@@ -139,222 +168,228 @@ export interface CompactorToolDeps {
|
|
|
139
168
|
* Call this during session_start after services are initialized.
|
|
140
169
|
*/
|
|
141
170
|
export function registerCompactorTools(pi: ExtensionAPI, deps: CompactorToolDeps): void {
|
|
142
|
-
// 1. compact — trigger manual compaction
|
|
143
|
-
pi.registerTool({
|
|
171
|
+
// 1. compact — trigger manual compaction (with optional dryRun)
|
|
172
|
+
pi.registerTool(({
|
|
144
173
|
name: "compact",
|
|
145
174
|
label: "Compact",
|
|
146
|
-
description: "Trigger manual context compaction. Reduces session history while preserving continuity.",
|
|
175
|
+
description: "Trigger manual context compaction. Reduces session history while preserving continuity. Use dryRun:true to preview without compacting.",
|
|
147
176
|
parameters: CompactParams,
|
|
148
|
-
async execute(): Promise<any> {
|
|
177
|
+
async execute(_toolCallId: string, params: any): Promise<any> {
|
|
178
|
+
if (params.dryRun) {
|
|
179
|
+
const blocks = deps.getBlocks();
|
|
180
|
+
const totalMessages = blocks.length;
|
|
181
|
+
const estimated = Math.round(totalMessages * 0.15);
|
|
182
|
+
return jsonResult({
|
|
183
|
+
dryRun: true,
|
|
184
|
+
wouldCompact: totalMessages,
|
|
185
|
+
estimatedKept: estimated,
|
|
186
|
+
message: `Would compact ${totalMessages} messages → ~${estimated} kept.`,
|
|
187
|
+
}, "Dry run — no compaction performed");
|
|
188
|
+
}
|
|
189
|
+
const c = deps.getCounters?.();
|
|
190
|
+
if (c) { c.compactions++; }
|
|
149
191
|
const result = compactTool();
|
|
150
192
|
return jsonResult(result, "Compaction triggered");
|
|
151
193
|
},
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
// 2. vcc_recall — search session history
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
194
|
+
} as any));
|
|
195
|
+
|
|
196
|
+
// 2. session_recall (new) / vcc_recall (deprecated) — search session history
|
|
197
|
+
const recallExec = async (_toolCallId: string, params: any): Promise<any> => {
|
|
198
|
+
const c = deps.getCounters?.();
|
|
199
|
+
if (c) { c.recallQueries++; }
|
|
200
|
+
const blocks = deps.getBlocks();
|
|
201
|
+
const input: RecallInput = {
|
|
202
|
+
query: params.query,
|
|
203
|
+
mode: params.mode,
|
|
204
|
+
limit: params.limit,
|
|
205
|
+
offset: params.offset,
|
|
206
|
+
expand: params.expand,
|
|
207
|
+
};
|
|
208
|
+
const result = vccRecall(blocks, input);
|
|
209
|
+
if (result.hits.length === 0) {
|
|
210
|
+
return textResult(`No results found for "${result.query}".`);
|
|
211
|
+
}
|
|
212
|
+
const lines = result.hits.map(
|
|
213
|
+
(h, i) =>
|
|
214
|
+
`[${i + 1}/${result.total}] score=${h.score.toFixed(2)} kind=${h.kind}\n${h.text}`,
|
|
215
|
+
);
|
|
216
|
+
return textResult(
|
|
217
|
+
`Found ${result.total} results for "${result.query}":\n\n${lines.join("\n\n")}`,
|
|
218
|
+
result as unknown as Record<string, unknown>,
|
|
219
|
+
);
|
|
220
|
+
};
|
|
221
|
+
pi.registerTool({ name: "session_recall", label: "Session Recall", description: "Search session history using BM25 or regex. Find previous goals, files, commits, and context.", parameters: RecallParams, execute: recallExec } as any);
|
|
222
|
+
pi.registerTool({ name: "vcc_recall", label: "Session Recall", description: "Search session history using BM25 or regex. (DEPRECATED: use session_recall instead)", parameters: VccRecallParams, async execute(tcId: string, p: any) { deprecationLog("vcc_recall", "session_recall"); return recallExec(tcId, p); } } as any);
|
|
223
|
+
|
|
224
|
+
// 3. sandbox (new) / ctx_execute (deprecated) — run code in sandbox
|
|
225
|
+
const sandboxExec = async (_toolCallId: string, params: any): Promise<any> => {
|
|
226
|
+
try {
|
|
227
|
+
const c = deps.getCounters?.();
|
|
228
|
+
if (c) { c.sandboxRuns++; }
|
|
229
|
+
const result = await ctxExecute(params as CtxExecuteInput);
|
|
230
|
+
const parts: string[] = [];
|
|
231
|
+
if (result.stdout) parts.push(result.stdout);
|
|
232
|
+
if (result.stderr) parts.push(`[stderr] ${result.stderr}`);
|
|
233
|
+
if (result.timedOut) parts.push("[timed out]");
|
|
234
|
+
if (result.exitCode !== 0) parts.push(`[exit code: ${result.exitCode}]`);
|
|
235
|
+
return textResult(parts.join("\n") || "(no output)", result as unknown as Record<string, unknown>);
|
|
236
|
+
} catch (err) {
|
|
237
|
+
return textResult(`Execution error: ${err}`, { error: true });
|
|
238
|
+
}
|
|
239
|
+
};
|
|
240
|
+
pi.registerTool({ name: "sandbox", label: "Sandbox", description: "Run code in a sandboxed environment. Supports 11 languages. Only stdout enters context.", parameters: SandboxParams, execute: sandboxExec } as any);
|
|
241
|
+
pi.registerTool({ name: "ctx_execute", label: "Sandbox", description: "Run code in sandbox. (DEPRECATED: use sandbox instead)", parameters: CtxExecuteParams, async execute(tcId: string, p: any) { deprecationLog("ctx_execute", "sandbox"); return sandboxExec(tcId, p); } } as any);
|
|
242
|
+
|
|
243
|
+
// 4. sandbox_file (new) / ctx_execute_file (deprecated) — execute file
|
|
244
|
+
const sandboxFileExec = async (_toolCallId: string, params: any): Promise<any> => {
|
|
245
|
+
try {
|
|
246
|
+
const c = deps.getCounters?.();
|
|
247
|
+
if (c) { c.sandboxRuns++; }
|
|
248
|
+
const result = await ctxExecuteFile(params as CtxExecuteFileInput);
|
|
249
|
+
const parts: string[] = [];
|
|
250
|
+
if (result.stdout) parts.push(result.stdout);
|
|
251
|
+
if (result.stderr) parts.push(`[stderr] ${result.stderr}`);
|
|
252
|
+
if (result.timedOut) parts.push("[timed out]");
|
|
253
|
+
return textResult(parts.join("\n") || "(no output)", result as unknown as Record<string, unknown>);
|
|
254
|
+
} catch (err) {
|
|
255
|
+
return textResult(`Execution error: ${err}`, { error: true });
|
|
256
|
+
}
|
|
257
|
+
};
|
|
258
|
+
pi.registerTool({ name: "sandbox_file", label: "Sandbox File", description: "Execute a file in the sandbox. File content is injected as FILE_CONTENT variable.", parameters: SandboxFileParams, execute: sandboxFileExec } as any);
|
|
259
|
+
pi.registerTool({ name: "ctx_execute_file", label: "Sandbox File", description: "Execute file in sandbox. (DEPRECATED: use sandbox_file instead)", parameters: CtxExecuteFileParams, async execute(tcId: string, p: any) { deprecationLog("ctx_execute_file", "sandbox_file"); return sandboxFileExec(tcId, p); } } as any);
|
|
260
|
+
|
|
261
|
+
// 5. sandbox_batch (new) / ctx_batch_execute (deprecated) — atomic batch
|
|
262
|
+
const sandboxBatchExec = async (_toolCallId: string, params: any): Promise<any> => {
|
|
263
|
+
try {
|
|
264
|
+
const c = deps.getCounters?.();
|
|
265
|
+
if (c) { c.sandboxRuns++; c.searchQueries++; }
|
|
266
|
+
const result = await ctxBatchExecute(deps.contentStore!, params.items as BatchItem[]);
|
|
267
|
+
const summaries = result.results.map((r, i) => {
|
|
268
|
+
if (r.type === "execute") {
|
|
269
|
+
const s = r.result.stdout?.slice(0, 200) || "(no output)";
|
|
270
|
+
return `[${i}] execute → ${r.result.exitCode === 0 ? "ok" : "fail"}: ${s}`;
|
|
271
|
+
}
|
|
272
|
+
return `[${i}] search → ${r.results.length} results`;
|
|
273
|
+
});
|
|
274
|
+
return textResult(`Batch results (${result.results.length} items):\n${summaries.join("\n")}`, result as unknown as Record<string, unknown>);
|
|
275
|
+
} catch (err) {
|
|
276
|
+
return textResult(`Batch error: ${err}`, { error: true });
|
|
277
|
+
}
|
|
278
|
+
};
|
|
279
|
+
pi.registerTool({ name: "sandbox_batch", label: "Sandbox Batch", description: "Run multiple code executions and searches atomically as a batch.", parameters: SandboxBatchParams, execute: sandboxBatchExec } as any);
|
|
280
|
+
pi.registerTool({ name: "ctx_batch_execute", label: "Sandbox Batch", description: "Run batch operations. (DEPRECATED: use sandbox_batch instead)", parameters: CtxBatchExecuteParams, async execute(tcId: string, p: any) { deprecationLog("ctx_batch_execute", "sandbox_batch"); return sandboxBatchExec(tcId, p); } } as any);
|
|
281
|
+
|
|
282
|
+
// 6. content_index (new) / ctx_index (deprecated) — index content into FTS5
|
|
283
|
+
const contentIndexExec = async (_toolCallId: string, params: any): Promise<any> => {
|
|
284
|
+
try {
|
|
285
|
+
const result = await ctxIndex(deps.contentStore!, params as CtxIndexInput);
|
|
286
|
+
return textResult(
|
|
287
|
+
`Indexed "${result.label}": ${result.totalChunks} chunks (${result.codeChunks} code)`,
|
|
288
|
+
result as unknown as Record<string, unknown>,
|
|
289
|
+
);
|
|
290
|
+
} catch (err) {
|
|
291
|
+
return textResult(`Index error: ${err}`, { error: true });
|
|
292
|
+
}
|
|
293
|
+
};
|
|
294
|
+
pi.registerTool({ name: "content_index", label: "Content Index", description: "Chunk content or a file and index into FTS5 for fast search.", parameters: ContentIndexParams, execute: contentIndexExec } as any);
|
|
295
|
+
pi.registerTool({ name: "ctx_index", label: "Content Index", description: "Index content into FTS5. (DEPRECATED: use content_index instead)", parameters: CtxIndexParams, async execute(tcId: string, p: any) { deprecationLog("ctx_index", "content_index"); return contentIndexExec(tcId, p); } } as any);
|
|
296
|
+
|
|
297
|
+
// 7. content_search (new) / ctx_search (deprecated) — query FTS5 content store
|
|
298
|
+
const contentSearchExec = async (_toolCallId: string, params: any): Promise<any> => {
|
|
299
|
+
try {
|
|
300
|
+
const c = deps.getCounters?.();
|
|
301
|
+
if (c) { c.searchQueries++; }
|
|
302
|
+
const results = await ctxSearch(deps.contentStore!, params as CtxSearchInput);
|
|
303
|
+
if (results.length === 0) {
|
|
304
|
+
return textResult(`No results for "${params.query}".`);
|
|
173
305
|
}
|
|
174
|
-
const lines =
|
|
175
|
-
(
|
|
176
|
-
`[${i + 1}
|
|
306
|
+
const lines = results.map(
|
|
307
|
+
(r, i) =>
|
|
308
|
+
`[${i + 1}] ${r.title} (rank: ${r.rank.toFixed(3)})\n${r.content.slice(0, 300)}`,
|
|
309
|
+
);
|
|
310
|
+
return textResult(
|
|
311
|
+
`Found ${results.length} results:\n\n${lines.join("\n\n")}`,
|
|
312
|
+
{ results } as unknown as Record<string, unknown>,
|
|
177
313
|
);
|
|
314
|
+
} catch (err) {
|
|
315
|
+
return textResult(`Search error: ${err}`, { error: true });
|
|
316
|
+
}
|
|
317
|
+
};
|
|
318
|
+
pi.registerTool({ name: "content_search", label: "Content Search", description: "Search indexed content using FTS5 full-text search.", parameters: ContentSearchParams, execute: contentSearchExec } as any);
|
|
319
|
+
pi.registerTool({ name: "ctx_search", label: "Content Search", description: "Search indexed content. (DEPRECATED: use content_search instead)", parameters: CtxSearchParams, async execute(tcId: string, p: any) { deprecationLog("ctx_search", "content_search"); return contentSearchExec(tcId, p); } } as any);
|
|
320
|
+
|
|
321
|
+
// 8. content_fetch (new) / ctx_fetch_and_index (deprecated) — fetch URL
|
|
322
|
+
const contentFetchExec = async (_toolCallId: string, params: any): Promise<any> => {
|
|
323
|
+
try {
|
|
324
|
+
const result = await ctxFetchAndIndex(deps.contentStore!, params as CtxFetchAndIndexInput);
|
|
178
325
|
return textResult(
|
|
179
|
-
`
|
|
326
|
+
`Fetched and indexed "${result.label}": ${result.totalChunks} chunks`,
|
|
180
327
|
result as unknown as Record<string, unknown>,
|
|
181
328
|
);
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
pi.registerTool({
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
pi.registerTool({
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
});
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
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,
|
|
329
|
+
} catch (err) {
|
|
330
|
+
return textResult(`Fetch error: ${err}`, { error: true });
|
|
331
|
+
}
|
|
332
|
+
};
|
|
333
|
+
pi.registerTool({ name: "content_fetch", label: "Content Fetch", description: "Fetch a URL, convert to markdown, and index into FTS5 content store.", parameters: ContentFetchParams, execute: contentFetchExec } as any);
|
|
334
|
+
pi.registerTool({ name: "ctx_fetch_and_index", label: "Content Fetch", description: "Fetch URL and index. (DEPRECATED: use content_fetch instead)", parameters: CtxFetchAndIndexParams, async execute(tcId: string, p: any) { deprecationLog("ctx_fetch_and_index", "content_fetch"); return contentFetchExec(tcId, p); } } as any);
|
|
335
|
+
|
|
336
|
+
// 9. compactor_stats (new) / ctx_stats (deprecated) — context savings dashboard
|
|
337
|
+
const statsExec = async (): Promise<any> => {
|
|
338
|
+
try {
|
|
339
|
+
const result = await ctxStats(deps.sessionDB, deps.contentStore!, deps.getSessionId(), deps.getCounters?.());
|
|
340
|
+
const lines = [
|
|
341
|
+
`📊 Compactor Stats`,
|
|
342
|
+
`Session events: ${result.sessionEvents}`,
|
|
343
|
+
`Compactions: ${result.compactions}`,
|
|
344
|
+
`Tokens saved: ${result.tokensSaved}`,
|
|
345
|
+
`Indexed docs: ${result.indexedDocs} (${result.indexedChunks} chunks)`,
|
|
346
|
+
`Sandbox runs: ${result.sandboxRuns}`,
|
|
347
|
+
`Search queries: ${result.searchQueries}`,
|
|
348
|
+
];
|
|
349
|
+
return textResult(lines.join("\n"), result as unknown as Record<string, unknown>);
|
|
350
|
+
} catch (err) {
|
|
351
|
+
return textResult(`Stats error: ${err}`, { error: true });
|
|
352
|
+
}
|
|
353
|
+
};
|
|
354
|
+
pi.registerTool({ name: "compactor_stats", label: "Compactor Stats", description: "Show context savings dashboard — session events, compactions, indexed content.", parameters: StatsParams, execute: statsExec } as any);
|
|
355
|
+
pi.registerTool({ name: "ctx_stats", label: "Compactor Stats", description: "Show stats dashboard. (DEPRECATED: use compactor_stats instead)", parameters: CtxStatsParams, async execute() { deprecationLog("ctx_stats", "compactor_stats"); return statsExec(); } } as any);
|
|
356
|
+
|
|
357
|
+
// 10. compactor_doctor (new) / ctx_doctor (deprecated) — diagnostics checklist
|
|
358
|
+
const doctorExec = async (): Promise<any> => {
|
|
359
|
+
try {
|
|
360
|
+
const result = await ctxDoctor(deps.sessionDB, deps.contentStore!);
|
|
361
|
+
const icon = (s: string) => (s === "pass" ? "✅" : s === "warn" ? "⚠️" : "❌");
|
|
362
|
+
const lines = [
|
|
363
|
+
result.healthy ? "🩺 All checks passed" : "🩺 Issues found",
|
|
364
|
+
"",
|
|
365
|
+
...result.checks.map((c) => `${icon(c.status)} ${c.name}: ${c.message}`),
|
|
366
|
+
];
|
|
367
|
+
return jsonResult(result, lines.join("\n"));
|
|
368
|
+
} catch (err) {
|
|
369
|
+
return textResult(`Doctor error: ${err}`, { error: true });
|
|
370
|
+
}
|
|
371
|
+
};
|
|
372
|
+
pi.registerTool({ name: "compactor_doctor", label: "Compactor Doctor", description: "Run diagnostics checklist — validate config, DB, FTS5, runtimes.", parameters: DoctorParams, execute: doctorExec } as any);
|
|
373
|
+
pi.registerTool({ name: "ctx_doctor", label: "Compactor Doctor", description: "Run diagnostics. (DEPRECATED: use compactor_doctor instead)", parameters: CtxDoctorParams, async execute() { deprecationLog("ctx_doctor", "compactor_doctor"); return doctorExec(); } } as any);
|
|
374
|
+
|
|
375
|
+
// 11. context_budget — estimate remaining context window
|
|
376
|
+
pi.registerTool(({
|
|
377
|
+
name: "context_budget",
|
|
378
|
+
label: "Context Budget",
|
|
379
|
+
description: "Estimate remaining context window (% full, tokens left) and get advice on whether to compact.",
|
|
380
|
+
parameters: Type.Object({}),
|
|
345
381
|
async execute(): Promise<any> {
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
const
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
}
|
|
382
|
+
const blocks = deps.getBlocks();
|
|
383
|
+
const estimatedTokens = blocks.reduce((sum, b) => {
|
|
384
|
+
const text = b.kind === "tool_call"
|
|
385
|
+
? `${b.name} ${JSON.stringify((b as any).args ?? {})}`
|
|
386
|
+
: b.kind === "tool_result"
|
|
387
|
+
? `${b.name} ${(b as any).text ?? ""}`
|
|
388
|
+
: (b as any).text ?? "";
|
|
389
|
+
return sum + Math.ceil(text.length / 4);
|
|
390
|
+
}, 0);
|
|
391
|
+
const message = contextBudgetTool(estimatedTokens);
|
|
392
|
+
return textResult(message);
|
|
358
393
|
},
|
|
359
|
-
});
|
|
394
|
+
} as any));
|
|
360
395
|
}
|