context-mode 0.5.26 → 0.6.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/.claude-plugin/plugin.json +1 -1
- package/build/server.js +419 -88
- package/build/store.d.ts +5 -0
- package/build/store.js +20 -0
- package/package.json +1 -1
- package/skills/context-mode/SKILL.md +8 -42
- package/server.bundle.mjs +0 -265
package/build/server.js
CHANGED
|
@@ -20,12 +20,82 @@ function getStore() {
|
|
|
20
20
|
_store = new ContentStore();
|
|
21
21
|
return _store;
|
|
22
22
|
}
|
|
23
|
+
// ─────────────────────────────────────────────────────────
|
|
24
|
+
// Session stats — track context consumption per tool
|
|
25
|
+
// ─────────────────────────────────────────────────────────
|
|
26
|
+
const sessionStats = {
|
|
27
|
+
calls: {},
|
|
28
|
+
bytesReturned: {},
|
|
29
|
+
bytesIndexed: 0,
|
|
30
|
+
sessionStart: Date.now(),
|
|
31
|
+
};
|
|
32
|
+
function trackResponse(toolName, response) {
|
|
33
|
+
const bytes = response.content.reduce((sum, c) => sum + Buffer.byteLength(c.text), 0);
|
|
34
|
+
sessionStats.calls[toolName] = (sessionStats.calls[toolName] || 0) + 1;
|
|
35
|
+
sessionStats.bytesReturned[toolName] =
|
|
36
|
+
(sessionStats.bytesReturned[toolName] || 0) + bytes;
|
|
37
|
+
return response;
|
|
38
|
+
}
|
|
39
|
+
function trackIndexed(bytes) {
|
|
40
|
+
sessionStats.bytesIndexed += bytes;
|
|
41
|
+
}
|
|
23
42
|
// Build description dynamically based on detected runtimes
|
|
24
43
|
const langList = available.join(", ");
|
|
25
44
|
const bunNote = hasBunRuntime()
|
|
26
45
|
? " (Bun detected — JS/TS runs 3-5x faster)"
|
|
27
46
|
: "";
|
|
28
47
|
// ─────────────────────────────────────────────────────────
|
|
48
|
+
// Helper: smart snippet extraction — returns windows around
|
|
49
|
+
// matching query terms instead of dumb truncation
|
|
50
|
+
// ─────────────────────────────────────────────────────────
|
|
51
|
+
function extractSnippet(content, query, maxLen = 1500) {
|
|
52
|
+
if (content.length <= maxLen)
|
|
53
|
+
return content;
|
|
54
|
+
const terms = query
|
|
55
|
+
.toLowerCase()
|
|
56
|
+
.split(/\s+/)
|
|
57
|
+
.filter((t) => t.length > 2);
|
|
58
|
+
const lower = content.toLowerCase();
|
|
59
|
+
// Find all positions where query terms appear
|
|
60
|
+
const positions = [];
|
|
61
|
+
for (const term of terms) {
|
|
62
|
+
let idx = lower.indexOf(term);
|
|
63
|
+
while (idx !== -1) {
|
|
64
|
+
positions.push(idx);
|
|
65
|
+
idx = lower.indexOf(term, idx + 1);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
// No term matches — return start (BM25 matched on stems/variants)
|
|
69
|
+
if (positions.length === 0) {
|
|
70
|
+
return content.slice(0, maxLen) + "\n…";
|
|
71
|
+
}
|
|
72
|
+
// Sort positions, merge overlapping windows
|
|
73
|
+
positions.sort((a, b) => a - b);
|
|
74
|
+
const WINDOW = 300;
|
|
75
|
+
const windows = [];
|
|
76
|
+
for (const pos of positions) {
|
|
77
|
+
const start = Math.max(0, pos - WINDOW);
|
|
78
|
+
const end = Math.min(content.length, pos + WINDOW);
|
|
79
|
+
if (windows.length > 0 && start <= windows[windows.length - 1][1]) {
|
|
80
|
+
windows[windows.length - 1][1] = end;
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
windows.push([start, end]);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
// Collect windows until maxLen
|
|
87
|
+
const parts = [];
|
|
88
|
+
let total = 0;
|
|
89
|
+
for (const [start, end] of windows) {
|
|
90
|
+
if (total >= maxLen)
|
|
91
|
+
break;
|
|
92
|
+
const part = content.slice(start, Math.min(end, start + (maxLen - total)));
|
|
93
|
+
parts.push((start > 0 ? "…" : "") + part + (end < content.length ? "…" : ""));
|
|
94
|
+
total += part.length;
|
|
95
|
+
}
|
|
96
|
+
return parts.join("\n\n");
|
|
97
|
+
}
|
|
98
|
+
// ─────────────────────────────────────────────────────────
|
|
29
99
|
// Tool: execute
|
|
30
100
|
// ─────────────────────────────────────────────────────────
|
|
31
101
|
server.registerTool("execute", {
|
|
@@ -59,14 +129,14 @@ server.registerTool("execute", {
|
|
|
59
129
|
.optional()
|
|
60
130
|
.describe("What you're looking for in the output. When provided and output is large (>5KB), " +
|
|
61
131
|
"indexes output into knowledge base and returns section titles + previews — not full content. " +
|
|
62
|
-
"Use search() to retrieve specific sections. Example: 'failing tests', 'HTTP 500 errors'." +
|
|
132
|
+
"Use search(queries: [...]) to retrieve specific sections. Example: 'failing tests', 'HTTP 500 errors'." +
|
|
63
133
|
"\n\nTIP: Use specific technical terms, not just concepts. Check 'Searchable terms' in the response for available vocabulary."),
|
|
64
134
|
}),
|
|
65
135
|
}, async ({ language, code, timeout, intent }) => {
|
|
66
136
|
try {
|
|
67
137
|
const result = await executor.execute({ language, code, timeout });
|
|
68
138
|
if (result.timedOut) {
|
|
69
|
-
return {
|
|
139
|
+
return trackResponse("execute", {
|
|
70
140
|
content: [
|
|
71
141
|
{
|
|
72
142
|
type: "text",
|
|
@@ -74,48 +144,50 @@ server.registerTool("execute", {
|
|
|
74
144
|
},
|
|
75
145
|
],
|
|
76
146
|
isError: true,
|
|
77
|
-
};
|
|
147
|
+
});
|
|
78
148
|
}
|
|
79
149
|
if (result.exitCode !== 0) {
|
|
80
150
|
const output = `Exit code: ${result.exitCode}\n\nstdout:\n${result.stdout}\n\nstderr:\n${result.stderr}`;
|
|
81
151
|
if (intent && intent.trim().length > 0 && Buffer.byteLength(output) > INTENT_SEARCH_THRESHOLD) {
|
|
82
|
-
|
|
152
|
+
trackIndexed(Buffer.byteLength(output));
|
|
153
|
+
return trackResponse("execute", {
|
|
83
154
|
content: [
|
|
84
155
|
{ type: "text", text: intentSearch(output, intent, `execute:${language}:error`) },
|
|
85
156
|
],
|
|
86
157
|
isError: true,
|
|
87
|
-
};
|
|
158
|
+
});
|
|
88
159
|
}
|
|
89
|
-
return {
|
|
160
|
+
return trackResponse("execute", {
|
|
90
161
|
content: [
|
|
91
162
|
{ type: "text", text: output },
|
|
92
163
|
],
|
|
93
164
|
isError: true,
|
|
94
|
-
};
|
|
165
|
+
});
|
|
95
166
|
}
|
|
96
167
|
const stdout = result.stdout || "(no output)";
|
|
97
168
|
// Intent-driven search: if intent provided and output is large enough
|
|
98
169
|
if (intent && intent.trim().length > 0 && Buffer.byteLength(stdout) > INTENT_SEARCH_THRESHOLD) {
|
|
99
|
-
|
|
170
|
+
trackIndexed(Buffer.byteLength(stdout));
|
|
171
|
+
return trackResponse("execute", {
|
|
100
172
|
content: [
|
|
101
173
|
{ type: "text", text: intentSearch(stdout, intent, `execute:${language}`) },
|
|
102
174
|
],
|
|
103
|
-
};
|
|
175
|
+
});
|
|
104
176
|
}
|
|
105
|
-
return {
|
|
177
|
+
return trackResponse("execute", {
|
|
106
178
|
content: [
|
|
107
179
|
{ type: "text", text: stdout },
|
|
108
180
|
],
|
|
109
|
-
};
|
|
181
|
+
});
|
|
110
182
|
}
|
|
111
183
|
catch (err) {
|
|
112
184
|
const message = err instanceof Error ? err.message : String(err);
|
|
113
|
-
return {
|
|
185
|
+
return trackResponse("execute", {
|
|
114
186
|
content: [
|
|
115
187
|
{ type: "text", text: `Runtime error: ${message}` },
|
|
116
188
|
],
|
|
117
189
|
isError: true,
|
|
118
|
-
};
|
|
190
|
+
});
|
|
119
191
|
}
|
|
120
192
|
});
|
|
121
193
|
// ─────────────────────────────────────────────────────────
|
|
@@ -123,12 +195,13 @@ server.registerTool("execute", {
|
|
|
123
195
|
// ─────────────────────────────────────────────────────────
|
|
124
196
|
function indexStdout(stdout, source) {
|
|
125
197
|
const store = getStore();
|
|
198
|
+
trackIndexed(Buffer.byteLength(stdout));
|
|
126
199
|
const indexed = store.index({ content: stdout, source });
|
|
127
200
|
return {
|
|
128
201
|
content: [
|
|
129
202
|
{
|
|
130
203
|
type: "text",
|
|
131
|
-
text: `Indexed ${indexed.totalChunks} sections (${indexed.codeChunks} with code) from: ${indexed.label}\nUse search() to query this content. Use source: "${indexed.label}" to scope results.`,
|
|
204
|
+
text: `Indexed ${indexed.totalChunks} sections (${indexed.codeChunks} with code) from: ${indexed.label}\nUse search(queries: ["..."]) to query this content. Use source: "${indexed.label}" to scope results.`,
|
|
132
205
|
},
|
|
133
206
|
],
|
|
134
207
|
};
|
|
@@ -205,7 +278,7 @@ function intentSearch(stdout, intent, source, maxResults = 5) {
|
|
|
205
278
|
lines.push(`Searchable terms: ${distinctiveTerms.join(", ")}`);
|
|
206
279
|
}
|
|
207
280
|
lines.push("");
|
|
208
|
-
lines.push("Use search() to retrieve full content of any section.");
|
|
281
|
+
lines.push("Use search(queries: [...]) to retrieve full content of any section.");
|
|
209
282
|
return lines.join("\n");
|
|
210
283
|
}
|
|
211
284
|
finally {
|
|
@@ -259,7 +332,7 @@ server.registerTool("execute_file", {
|
|
|
259
332
|
timeout,
|
|
260
333
|
});
|
|
261
334
|
if (result.timedOut) {
|
|
262
|
-
return {
|
|
335
|
+
return trackResponse("execute_file", {
|
|
263
336
|
content: [
|
|
264
337
|
{
|
|
265
338
|
type: "text",
|
|
@@ -267,47 +340,49 @@ server.registerTool("execute_file", {
|
|
|
267
340
|
},
|
|
268
341
|
],
|
|
269
342
|
isError: true,
|
|
270
|
-
};
|
|
343
|
+
});
|
|
271
344
|
}
|
|
272
345
|
if (result.exitCode !== 0) {
|
|
273
346
|
const output = `Error processing ${path} (exit ${result.exitCode}):\n${result.stderr || result.stdout}`;
|
|
274
347
|
if (intent && intent.trim().length > 0 && Buffer.byteLength(output) > INTENT_SEARCH_THRESHOLD) {
|
|
275
|
-
|
|
348
|
+
trackIndexed(Buffer.byteLength(output));
|
|
349
|
+
return trackResponse("execute_file", {
|
|
276
350
|
content: [
|
|
277
351
|
{ type: "text", text: intentSearch(output, intent, `file:${path}:error`) },
|
|
278
352
|
],
|
|
279
353
|
isError: true,
|
|
280
|
-
};
|
|
354
|
+
});
|
|
281
355
|
}
|
|
282
|
-
return {
|
|
356
|
+
return trackResponse("execute_file", {
|
|
283
357
|
content: [
|
|
284
358
|
{ type: "text", text: output },
|
|
285
359
|
],
|
|
286
360
|
isError: true,
|
|
287
|
-
};
|
|
361
|
+
});
|
|
288
362
|
}
|
|
289
363
|
const stdout = result.stdout || "(no output)";
|
|
290
364
|
if (intent && intent.trim().length > 0 && Buffer.byteLength(stdout) > INTENT_SEARCH_THRESHOLD) {
|
|
291
|
-
|
|
365
|
+
trackIndexed(Buffer.byteLength(stdout));
|
|
366
|
+
return trackResponse("execute_file", {
|
|
292
367
|
content: [
|
|
293
368
|
{ type: "text", text: intentSearch(stdout, intent, `file:${path}`) },
|
|
294
369
|
],
|
|
295
|
-
};
|
|
370
|
+
});
|
|
296
371
|
}
|
|
297
|
-
return {
|
|
372
|
+
return trackResponse("execute_file", {
|
|
298
373
|
content: [
|
|
299
374
|
{ type: "text", text: stdout },
|
|
300
375
|
],
|
|
301
|
-
};
|
|
376
|
+
});
|
|
302
377
|
}
|
|
303
378
|
catch (err) {
|
|
304
379
|
const message = err instanceof Error ? err.message : String(err);
|
|
305
|
-
return {
|
|
380
|
+
return trackResponse("execute_file", {
|
|
306
381
|
content: [
|
|
307
382
|
{ type: "text", text: `Runtime error: ${message}` },
|
|
308
383
|
],
|
|
309
384
|
isError: true,
|
|
310
|
-
};
|
|
385
|
+
});
|
|
311
386
|
}
|
|
312
387
|
});
|
|
313
388
|
// ─────────────────────────────────────────────────────────
|
|
@@ -343,7 +418,7 @@ server.registerTool("index", {
|
|
|
343
418
|
}),
|
|
344
419
|
}, async ({ content, path, source }) => {
|
|
345
420
|
if (!content && !path) {
|
|
346
|
-
return {
|
|
421
|
+
return trackResponse("index", {
|
|
347
422
|
content: [
|
|
348
423
|
{
|
|
349
424
|
type: "text",
|
|
@@ -351,100 +426,160 @@ server.registerTool("index", {
|
|
|
351
426
|
},
|
|
352
427
|
],
|
|
353
428
|
isError: true,
|
|
354
|
-
};
|
|
429
|
+
});
|
|
355
430
|
}
|
|
356
431
|
try {
|
|
432
|
+
// Track the raw bytes being indexed (content or file)
|
|
433
|
+
if (content)
|
|
434
|
+
trackIndexed(Buffer.byteLength(content));
|
|
435
|
+
else if (path) {
|
|
436
|
+
try {
|
|
437
|
+
const fs = await import("fs");
|
|
438
|
+
trackIndexed(fs.readFileSync(path).byteLength);
|
|
439
|
+
}
|
|
440
|
+
catch { /* ignore — file read errors handled by store */ }
|
|
441
|
+
}
|
|
357
442
|
const store = getStore();
|
|
358
443
|
const result = store.index({ content, path, source });
|
|
359
|
-
return {
|
|
444
|
+
return trackResponse("index", {
|
|
360
445
|
content: [
|
|
361
446
|
{
|
|
362
447
|
type: "text",
|
|
363
|
-
text: `Indexed ${result.totalChunks} sections (${result.codeChunks} with code) from: ${result.label}\nUse search() to query this content. Use source: "${result.label}" to scope results.`,
|
|
448
|
+
text: `Indexed ${result.totalChunks} sections (${result.codeChunks} with code) from: ${result.label}\nUse search(queries: ["..."]) to query this content. Use source: "${result.label}" to scope results.`,
|
|
364
449
|
},
|
|
365
450
|
],
|
|
366
|
-
};
|
|
451
|
+
});
|
|
367
452
|
}
|
|
368
453
|
catch (err) {
|
|
369
454
|
const message = err instanceof Error ? err.message : String(err);
|
|
370
|
-
return {
|
|
455
|
+
return trackResponse("index", {
|
|
371
456
|
content: [
|
|
372
457
|
{ type: "text", text: `Index error: ${message}` },
|
|
373
458
|
],
|
|
374
459
|
isError: true,
|
|
375
|
-
};
|
|
460
|
+
});
|
|
376
461
|
}
|
|
377
462
|
});
|
|
378
463
|
// ─────────────────────────────────────────────────────────
|
|
379
|
-
// Tool: search
|
|
464
|
+
// Tool: search — progressive throttling
|
|
380
465
|
// ─────────────────────────────────────────────────────────
|
|
466
|
+
// Track search calls per 60-second window for progressive throttling
|
|
467
|
+
let searchCallCount = 0;
|
|
468
|
+
let searchWindowStart = Date.now();
|
|
469
|
+
const SEARCH_WINDOW_MS = 60_000;
|
|
470
|
+
const SEARCH_MAX_RESULTS_AFTER = 3; // after 3 calls: 1 result per query
|
|
471
|
+
const SEARCH_BLOCK_AFTER = 8; // after 8 calls: refuse, demand batching
|
|
381
472
|
server.registerTool("search", {
|
|
382
473
|
title: "Search Indexed Content",
|
|
383
|
-
description: "Search
|
|
384
|
-
"
|
|
385
|
-
"Use after 'index' to retrieve specific documentation sections, code examples, or API details on demand.\n\n" +
|
|
386
|
-
"WHEN TO USE:\n" +
|
|
387
|
-
"- Find specific code examples ('useEffect cleanup pattern')\n" +
|
|
388
|
-
"- Look up API signatures ('Supabase RLS policy syntax')\n" +
|
|
389
|
-
"- Get configuration details ('Tailwind responsive breakpoints')\n" +
|
|
390
|
-
"- Find migration steps ('App Router data fetching')\n\n" +
|
|
391
|
-
"SEARCH TIPS:\n" +
|
|
392
|
-
"- Queries use OR semantics — results matching more terms rank higher via BM25\n" +
|
|
393
|
-
"- Use 2-4 specific technical terms per query for best results\n" +
|
|
394
|
-
"- Use 'source' parameter to scope search to a specific indexed source (partial match)\n" +
|
|
395
|
-
"- Check 'Searchable terms' from execute/execute_file results for available vocabulary\n" +
|
|
396
|
-
"- For broad topics, send multiple focused searches in parallel\n\n" +
|
|
397
|
-
"Returns exact content — not summaries. Each result includes heading hierarchy and full section text.",
|
|
474
|
+
description: "Search indexed content. Pass ALL search questions as queries array in ONE call.\n\n" +
|
|
475
|
+
"TIPS: 2-4 specific terms per query. Use 'source' to scope results.",
|
|
398
476
|
inputSchema: z.object({
|
|
399
|
-
|
|
477
|
+
queries: z
|
|
478
|
+
.array(z.string())
|
|
479
|
+
.optional()
|
|
480
|
+
.describe("Array of search queries. Batch ALL questions in one call."),
|
|
400
481
|
limit: z
|
|
401
482
|
.number()
|
|
402
483
|
.optional()
|
|
403
484
|
.default(3)
|
|
404
|
-
.describe("
|
|
485
|
+
.describe("Results per query (default: 3)"),
|
|
405
486
|
source: z
|
|
406
487
|
.string()
|
|
407
488
|
.optional()
|
|
408
|
-
.describe("Filter
|
|
409
|
-
"Use the source label from index/fetch_and_index response."),
|
|
489
|
+
.describe("Filter to a specific indexed source (partial match)."),
|
|
410
490
|
}),
|
|
411
|
-
}, async (
|
|
491
|
+
}, async (params) => {
|
|
412
492
|
try {
|
|
413
493
|
const store = getStore();
|
|
414
|
-
const
|
|
415
|
-
|
|
494
|
+
const raw = params;
|
|
495
|
+
// Normalize: accept both query (string) and queries (array)
|
|
496
|
+
const queryList = [];
|
|
497
|
+
if (Array.isArray(raw.queries) && raw.queries.length > 0) {
|
|
498
|
+
queryList.push(...raw.queries);
|
|
499
|
+
}
|
|
500
|
+
else if (typeof raw.query === "string" && raw.query.length > 0) {
|
|
501
|
+
queryList.push(raw.query);
|
|
502
|
+
}
|
|
503
|
+
if (queryList.length === 0) {
|
|
504
|
+
return trackResponse("search", {
|
|
505
|
+
content: [{ type: "text", text: "Error: provide query or queries." }],
|
|
506
|
+
isError: true,
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
const { limit = 3, source } = params;
|
|
510
|
+
// Progressive throttling: track calls in time window
|
|
511
|
+
const now = Date.now();
|
|
512
|
+
if (now - searchWindowStart > SEARCH_WINDOW_MS) {
|
|
513
|
+
searchCallCount = 0;
|
|
514
|
+
searchWindowStart = now;
|
|
515
|
+
}
|
|
516
|
+
searchCallCount++;
|
|
517
|
+
// After SEARCH_BLOCK_AFTER calls: refuse
|
|
518
|
+
if (searchCallCount > SEARCH_BLOCK_AFTER) {
|
|
519
|
+
return trackResponse("search", {
|
|
520
|
+
content: [{
|
|
521
|
+
type: "text",
|
|
522
|
+
text: `BLOCKED: ${searchCallCount} search calls in ${Math.round((now - searchWindowStart) / 1000)}s. ` +
|
|
523
|
+
"You're flooding context. STOP making individual search calls. " +
|
|
524
|
+
"Use batch_execute(commands, queries) for your next research step.",
|
|
525
|
+
}],
|
|
526
|
+
isError: true,
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
// Determine per-query result limit based on throttle level
|
|
530
|
+
const effectiveLimit = searchCallCount > SEARCH_MAX_RESULTS_AFTER
|
|
531
|
+
? 1 // after 3 calls: only 1 result per query
|
|
532
|
+
: Math.min(limit, 2); // normal: max 2
|
|
533
|
+
const MAX_TOTAL = 40 * 1024; // 40KB total cap
|
|
534
|
+
let totalSize = 0;
|
|
535
|
+
const sections = [];
|
|
536
|
+
for (const q of queryList) {
|
|
537
|
+
if (totalSize > MAX_TOTAL) {
|
|
538
|
+
sections.push(`## ${q}\n(output cap reached)\n`);
|
|
539
|
+
continue;
|
|
540
|
+
}
|
|
541
|
+
const results = store.search(q, effectiveLimit, source);
|
|
542
|
+
if (results.length === 0) {
|
|
543
|
+
sections.push(`## ${q}\nNo results found.`);
|
|
544
|
+
continue;
|
|
545
|
+
}
|
|
546
|
+
const formatted = results
|
|
547
|
+
.map((r, i) => {
|
|
548
|
+
const header = `--- [${r.source}] ---`;
|
|
549
|
+
const heading = `### ${r.title}`;
|
|
550
|
+
const snippet = extractSnippet(r.content, q);
|
|
551
|
+
return `${header}\n${heading}\n\n${snippet}`;
|
|
552
|
+
})
|
|
553
|
+
.join("\n\n");
|
|
554
|
+
sections.push(`## ${q}\n\n${formatted}`);
|
|
555
|
+
totalSize += formatted.length;
|
|
556
|
+
}
|
|
557
|
+
let output = sections.join("\n\n---\n\n");
|
|
558
|
+
// Add throttle warning after threshold
|
|
559
|
+
if (searchCallCount >= SEARCH_MAX_RESULTS_AFTER) {
|
|
560
|
+
output += `\n\n⚠ search call #${searchCallCount}/${SEARCH_BLOCK_AFTER} in this window. ` +
|
|
561
|
+
`Results limited to ${effectiveLimit}/query. ` +
|
|
562
|
+
`Batch queries: search(queries: ["q1","q2","q3"]) or use batch_execute.`;
|
|
563
|
+
}
|
|
564
|
+
if (output.trim().length === 0) {
|
|
416
565
|
const sources = store.listSources();
|
|
417
566
|
const sourceList = sources.length > 0
|
|
418
567
|
? `\nIndexed sources: ${sources.map((s) => `"${s.label}" (${s.chunkCount} sections)`).join(", ")}`
|
|
419
568
|
: "";
|
|
420
|
-
return {
|
|
421
|
-
content: [
|
|
422
|
-
|
|
423
|
-
type: "text",
|
|
424
|
-
text: `No results found for: "${query}"${source ? ` in source "${source}"` : ""}.${sourceList}`,
|
|
425
|
-
},
|
|
426
|
-
],
|
|
427
|
-
};
|
|
569
|
+
return trackResponse("search", {
|
|
570
|
+
content: [{ type: "text", text: `No results found.${sourceList}` }],
|
|
571
|
+
});
|
|
428
572
|
}
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
const heading = `## ${r.title}`;
|
|
433
|
-
return `${header}\n${heading}\n\n${r.content}`;
|
|
434
|
-
})
|
|
435
|
-
.join("\n\n");
|
|
436
|
-
return {
|
|
437
|
-
content: [{ type: "text", text: formatted }],
|
|
438
|
-
};
|
|
573
|
+
return trackResponse("search", {
|
|
574
|
+
content: [{ type: "text", text: output }],
|
|
575
|
+
});
|
|
439
576
|
}
|
|
440
577
|
catch (err) {
|
|
441
578
|
const message = err instanceof Error ? err.message : String(err);
|
|
442
|
-
return {
|
|
443
|
-
content: [
|
|
444
|
-
{ type: "text", text: `Search error: ${message}` },
|
|
445
|
-
],
|
|
579
|
+
return trackResponse("search", {
|
|
580
|
+
content: [{ type: "text", text: `Search error: ${message}` }],
|
|
446
581
|
isError: true,
|
|
447
|
-
};
|
|
582
|
+
});
|
|
448
583
|
}
|
|
449
584
|
});
|
|
450
585
|
// ─────────────────────────────────────────────────────────
|
|
@@ -541,7 +676,7 @@ server.registerTool("fetch_and_index", {
|
|
|
541
676
|
timeout: 30_000,
|
|
542
677
|
});
|
|
543
678
|
if (result.exitCode !== 0) {
|
|
544
|
-
return {
|
|
679
|
+
return trackResponse("fetch_and_index", {
|
|
545
680
|
content: [
|
|
546
681
|
{
|
|
547
682
|
type: "text",
|
|
@@ -549,10 +684,10 @@ server.registerTool("fetch_and_index", {
|
|
|
549
684
|
},
|
|
550
685
|
],
|
|
551
686
|
isError: true,
|
|
552
|
-
};
|
|
687
|
+
});
|
|
553
688
|
}
|
|
554
689
|
if (!result.stdout || result.stdout.trim().length === 0) {
|
|
555
|
-
return {
|
|
690
|
+
return trackResponse("fetch_and_index", {
|
|
556
691
|
content: [
|
|
557
692
|
{
|
|
558
693
|
type: "text",
|
|
@@ -560,20 +695,216 @@ server.registerTool("fetch_and_index", {
|
|
|
560
695
|
},
|
|
561
696
|
],
|
|
562
697
|
isError: true,
|
|
563
|
-
};
|
|
698
|
+
});
|
|
564
699
|
}
|
|
565
|
-
// Index the markdown into FTS5
|
|
566
|
-
return indexStdout(result.stdout, source ?? url);
|
|
700
|
+
// Index the markdown into FTS5 (indexStdout already calls trackIndexed)
|
|
701
|
+
return trackResponse("fetch_and_index", indexStdout(result.stdout, source ?? url));
|
|
567
702
|
}
|
|
568
703
|
catch (err) {
|
|
569
704
|
const message = err instanceof Error ? err.message : String(err);
|
|
570
|
-
return {
|
|
705
|
+
return trackResponse("fetch_and_index", {
|
|
571
706
|
content: [
|
|
572
707
|
{ type: "text", text: `Fetch error: ${message}` },
|
|
573
708
|
],
|
|
574
709
|
isError: true,
|
|
575
|
-
};
|
|
710
|
+
});
|
|
711
|
+
}
|
|
712
|
+
});
|
|
713
|
+
// ─────────────────────────────────────────────────────────
|
|
714
|
+
// Tool: batch_execute
|
|
715
|
+
// ─────────────────────────────────────────────────────────
|
|
716
|
+
server.registerTool("batch_execute", {
|
|
717
|
+
title: "Batch Execute & Search",
|
|
718
|
+
description: "Execute multiple commands in ONE call, auto-index all output, and search with multiple queries. " +
|
|
719
|
+
"Returns search results directly — no follow-up calls needed.\n\n" +
|
|
720
|
+
"THIS IS THE PRIMARY TOOL. Use this instead of multiple execute() calls.\n\n" +
|
|
721
|
+
"One batch_execute call replaces 30+ execute calls + 10+ search calls.\n" +
|
|
722
|
+
"Provide all commands to run and all queries to search — everything happens in one round trip.",
|
|
723
|
+
inputSchema: z.object({
|
|
724
|
+
commands: z
|
|
725
|
+
.array(z.object({
|
|
726
|
+
label: z
|
|
727
|
+
.string()
|
|
728
|
+
.describe("Section header for this command's output (e.g., 'README', 'Package.json', 'Source Tree')"),
|
|
729
|
+
command: z
|
|
730
|
+
.string()
|
|
731
|
+
.describe("Shell command to execute"),
|
|
732
|
+
}))
|
|
733
|
+
.min(1)
|
|
734
|
+
.describe("Commands to execute as a batch. Each runs sequentially, output is labeled with the section header."),
|
|
735
|
+
queries: z
|
|
736
|
+
.array(z.string())
|
|
737
|
+
.min(1)
|
|
738
|
+
.describe("Search queries to extract information from indexed output. Use 5-8 comprehensive queries. " +
|
|
739
|
+
"Each returns top 5 matching sections with full content. " +
|
|
740
|
+
"This is your ONLY chance — put ALL your questions here. No follow-up calls needed."),
|
|
741
|
+
timeout: z
|
|
742
|
+
.number()
|
|
743
|
+
.optional()
|
|
744
|
+
.default(60000)
|
|
745
|
+
.describe("Max execution time in ms (default: 60s)"),
|
|
746
|
+
}),
|
|
747
|
+
}, async ({ commands, queries, timeout }) => {
|
|
748
|
+
try {
|
|
749
|
+
// Build batch script with markdown section headers for proper chunking
|
|
750
|
+
const script = commands
|
|
751
|
+
.map((c) => {
|
|
752
|
+
const safeLabel = c.label.replace(/'/g, "'\\''");
|
|
753
|
+
return `echo '# ${safeLabel}'\necho ''\n${c.command} 2>&1\necho ''`;
|
|
754
|
+
})
|
|
755
|
+
.join("\n");
|
|
756
|
+
const result = await executor.execute({
|
|
757
|
+
language: "shell",
|
|
758
|
+
code: script,
|
|
759
|
+
timeout,
|
|
760
|
+
});
|
|
761
|
+
if (result.timedOut) {
|
|
762
|
+
return trackResponse("batch_execute", {
|
|
763
|
+
content: [
|
|
764
|
+
{
|
|
765
|
+
type: "text",
|
|
766
|
+
text: `Batch timed out after ${timeout}ms. Partial output:\n${result.stdout?.slice(0, 2000) || "(none)"}`,
|
|
767
|
+
},
|
|
768
|
+
],
|
|
769
|
+
isError: true,
|
|
770
|
+
});
|
|
771
|
+
}
|
|
772
|
+
const stdout = result.stdout || "(no output)";
|
|
773
|
+
const totalBytes = Buffer.byteLength(stdout);
|
|
774
|
+
const totalLines = stdout.split("\n").length;
|
|
775
|
+
// Track indexed bytes (raw data that stays in sandbox)
|
|
776
|
+
trackIndexed(totalBytes);
|
|
777
|
+
// Index into knowledge base — markdown heading chunking splits by # labels
|
|
778
|
+
const store = getStore();
|
|
779
|
+
const source = `batch:${commands
|
|
780
|
+
.map((c) => c.label)
|
|
781
|
+
.join(",")
|
|
782
|
+
.slice(0, 80)}`;
|
|
783
|
+
const indexed = store.index({ content: stdout, source });
|
|
784
|
+
// Build section inventory — direct query by source_id (no FTS5 MATCH needed)
|
|
785
|
+
const allSections = store.getChunksBySource(indexed.sourceId);
|
|
786
|
+
const inventory = ["## Indexed Sections", ""];
|
|
787
|
+
const sectionTitles = [];
|
|
788
|
+
for (const s of allSections) {
|
|
789
|
+
const bytes = Buffer.byteLength(s.content);
|
|
790
|
+
inventory.push(`- ${s.title} (${(bytes / 1024).toFixed(1)}KB)`);
|
|
791
|
+
sectionTitles.push(s.title);
|
|
792
|
+
}
|
|
793
|
+
// Run all search queries — 3 results each, smart snippets
|
|
794
|
+
// Three-tier fallback: scoped → boosted → global
|
|
795
|
+
const MAX_OUTPUT = 80 * 1024; // 80KB total output cap
|
|
796
|
+
const queryResults = [];
|
|
797
|
+
let outputSize = 0;
|
|
798
|
+
for (const query of queries) {
|
|
799
|
+
if (outputSize > MAX_OUTPUT) {
|
|
800
|
+
queryResults.push(`## ${query}\n(output cap reached — use search(queries: ["${query}"]) for details)\n`);
|
|
801
|
+
continue;
|
|
802
|
+
}
|
|
803
|
+
// Tier 1: scoped search (within this batch's source)
|
|
804
|
+
let results = store.search(query, 3, source);
|
|
805
|
+
// Tier 2: boosted with section titles
|
|
806
|
+
if (results.length === 0 && sectionTitles.length > 0) {
|
|
807
|
+
const boosted = `${query} ${sectionTitles.join(" ")}`;
|
|
808
|
+
results = store.search(boosted, 3, source);
|
|
809
|
+
}
|
|
810
|
+
// Tier 3: global fallback (no source filter)
|
|
811
|
+
if (results.length === 0) {
|
|
812
|
+
results = store.search(query, 3);
|
|
813
|
+
}
|
|
814
|
+
queryResults.push(`## ${query}`);
|
|
815
|
+
queryResults.push("");
|
|
816
|
+
if (results.length > 0) {
|
|
817
|
+
for (const r of results) {
|
|
818
|
+
const snippet = extractSnippet(r.content, query);
|
|
819
|
+
queryResults.push(`### ${r.title}`);
|
|
820
|
+
queryResults.push(snippet);
|
|
821
|
+
queryResults.push("");
|
|
822
|
+
outputSize += snippet.length + r.title.length;
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
else {
|
|
826
|
+
queryResults.push("No matching sections found.");
|
|
827
|
+
queryResults.push("");
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
// Get searchable terms for edge cases where follow-up is needed
|
|
831
|
+
const distinctiveTerms = store.getDistinctiveTerms
|
|
832
|
+
? store.getDistinctiveTerms(indexed.sourceId)
|
|
833
|
+
: [];
|
|
834
|
+
const output = [
|
|
835
|
+
`Executed ${commands.length} commands (${totalLines} lines, ${(totalBytes / 1024).toFixed(1)}KB). ` +
|
|
836
|
+
`Indexed ${indexed.totalChunks} sections. Searched ${queries.length} queries.`,
|
|
837
|
+
"",
|
|
838
|
+
...inventory,
|
|
839
|
+
"",
|
|
840
|
+
...queryResults,
|
|
841
|
+
distinctiveTerms.length > 0
|
|
842
|
+
? `\nSearchable terms for follow-up: ${distinctiveTerms.join(", ")}`
|
|
843
|
+
: "",
|
|
844
|
+
].join("\n");
|
|
845
|
+
return trackResponse("batch_execute", {
|
|
846
|
+
content: [{ type: "text", text: output }],
|
|
847
|
+
});
|
|
848
|
+
}
|
|
849
|
+
catch (err) {
|
|
850
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
851
|
+
return trackResponse("batch_execute", {
|
|
852
|
+
content: [
|
|
853
|
+
{
|
|
854
|
+
type: "text",
|
|
855
|
+
text: `Batch execution error: ${message}`,
|
|
856
|
+
},
|
|
857
|
+
],
|
|
858
|
+
isError: true,
|
|
859
|
+
});
|
|
860
|
+
}
|
|
861
|
+
});
|
|
862
|
+
// ─────────────────────────────────────────────────────────
|
|
863
|
+
// Tool: stats
|
|
864
|
+
// ─────────────────────────────────────────────────────────
|
|
865
|
+
server.registerTool("stats", {
|
|
866
|
+
title: "Session Statistics",
|
|
867
|
+
description: "Returns context consumption statistics for the current session. " +
|
|
868
|
+
"Shows total bytes returned to context, breakdown by tool, call counts, " +
|
|
869
|
+
"estimated token usage, and context savings ratio.",
|
|
870
|
+
inputSchema: z.object({}),
|
|
871
|
+
}, async () => {
|
|
872
|
+
const totalBytesReturned = Object.values(sessionStats.bytesReturned).reduce((sum, b) => sum + b, 0);
|
|
873
|
+
const estimatedTokens = Math.round(totalBytesReturned / 4);
|
|
874
|
+
const totalCalls = Object.values(sessionStats.calls).reduce((sum, c) => sum + c, 0);
|
|
875
|
+
const uptimeMs = Date.now() - sessionStats.sessionStart;
|
|
876
|
+
const uptimeMin = (uptimeMs / 60_000).toFixed(1);
|
|
877
|
+
const lines = [
|
|
878
|
+
`## Context Mode Session Stats`,
|
|
879
|
+
"",
|
|
880
|
+
`Session uptime: ${uptimeMin} min`,
|
|
881
|
+
`Total tool calls: ${totalCalls}`,
|
|
882
|
+
`Total bytes returned to context: ${totalBytesReturned.toLocaleString()} (${(totalBytesReturned / 1024).toFixed(1)}KB)`,
|
|
883
|
+
`Estimated tokens consumed: ~${estimatedTokens.toLocaleString()} (bytes/4)`,
|
|
884
|
+
`Total bytes indexed (stayed in sandbox): ${sessionStats.bytesIndexed.toLocaleString()} (${(sessionStats.bytesIndexed / 1024).toFixed(1)}KB)`,
|
|
885
|
+
];
|
|
886
|
+
if (sessionStats.bytesIndexed > 0) {
|
|
887
|
+
const savingsRatio = sessionStats.bytesIndexed / Math.max(totalBytesReturned, 1);
|
|
888
|
+
lines.push(`Context savings ratio: ${savingsRatio.toFixed(1)}x (${((1 - 1 / Math.max(savingsRatio, 1)) * 100).toFixed(0)}% reduction)`);
|
|
889
|
+
}
|
|
890
|
+
lines.push("", "### Per-Tool Breakdown", "");
|
|
891
|
+
lines.push("| Tool | Calls | Bytes Returned | Est. Tokens |");
|
|
892
|
+
lines.push("|------|------:|---------------:|------------:|");
|
|
893
|
+
const toolNames = new Set([
|
|
894
|
+
...Object.keys(sessionStats.calls),
|
|
895
|
+
...Object.keys(sessionStats.bytesReturned),
|
|
896
|
+
]);
|
|
897
|
+
for (const tool of Array.from(toolNames).sort()) {
|
|
898
|
+
const calls = sessionStats.calls[tool] || 0;
|
|
899
|
+
const bytes = sessionStats.bytesReturned[tool] || 0;
|
|
900
|
+
const tokens = Math.round(bytes / 4);
|
|
901
|
+
lines.push(`| ${tool} | ${calls} | ${bytes.toLocaleString()} (${(bytes / 1024).toFixed(1)}KB) | ~${tokens.toLocaleString()} |`);
|
|
576
902
|
}
|
|
903
|
+
const text = lines.join("\n");
|
|
904
|
+
// Track the stats tool itself
|
|
905
|
+
return trackResponse("stats", {
|
|
906
|
+
content: [{ type: "text", text }],
|
|
907
|
+
});
|
|
577
908
|
});
|
|
578
909
|
// ─────────────────────────────────────────────────────────
|
|
579
910
|
// Server startup
|