context-mode 0.5.25 → 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/hooks/hooks.json +16 -0
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +2 -24
- package/build/server.js +424 -92
- 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 -261
package/build/server.js
CHANGED
|
@@ -5,7 +5,7 @@ import { z } from "zod";
|
|
|
5
5
|
import { PolyglotExecutor } from "./executor.js";
|
|
6
6
|
import { ContentStore } from "./store.js";
|
|
7
7
|
import { detectRuntimes, getRuntimeSummary, getAvailableLanguages, hasBunRuntime, } from "./runtime.js";
|
|
8
|
-
const VERSION = "0.5.
|
|
8
|
+
const VERSION = "0.5.26";
|
|
9
9
|
const runtimes = detectRuntimes();
|
|
10
10
|
const available = getAvailableLanguages(runtimes);
|
|
11
11
|
const server = new McpServer({
|
|
@@ -20,17 +20,87 @@ 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", {
|
|
32
102
|
title: "Execute Code",
|
|
33
|
-
description: `Execute code in a sandboxed subprocess. Only stdout enters context — raw data stays in the subprocess. Use instead of bash/cat when output would exceed 20 lines.${bunNote} Available: ${langList}.`,
|
|
103
|
+
description: `Execute code in a sandboxed subprocess. Only stdout enters context — raw data stays in the subprocess. Use instead of bash/cat when output would exceed 20 lines.${bunNote} Available: ${langList}.\n\nPREFER THIS OVER BASH for: API calls (gh, curl, aws), test runners (npm test, pytest), git queries (git log, git diff), data processing, and ANY CLI command that may produce large output. Bash should only be used for file mutations, git writes, and navigation.`,
|
|
34
104
|
inputSchema: z.object({
|
|
35
105
|
language: z
|
|
36
106
|
.enum([
|
|
@@ -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 {
|
|
@@ -217,7 +290,7 @@ function intentSearch(stdout, intent, source, maxResults = 5) {
|
|
|
217
290
|
// ─────────────────────────────────────────────────────────
|
|
218
291
|
server.registerTool("execute_file", {
|
|
219
292
|
title: "Execute File Processing",
|
|
220
|
-
description: "Read a file and process it without loading contents into context. The file is read into a FILE_CONTENT variable inside the sandbox. Only your printed summary enters context.",
|
|
293
|
+
description: "Read a file and process it without loading contents into context. The file is read into a FILE_CONTENT variable inside the sandbox. Only your printed summary enters context.\n\nPREFER THIS OVER Read/cat for: log files, data files (CSV, JSON, XML), large source files for analysis, and any file where you need to extract specific information rather than read the entire content.",
|
|
221
294
|
inputSchema: z.object({
|
|
222
295
|
path: z
|
|
223
296
|
.string()
|
|
@@ -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
|
// ─────────────────────────────────────────────────────────
|
|
@@ -521,7 +656,8 @@ server.registerTool("fetch_and_index", {
|
|
|
521
656
|
title: "Fetch & Index URL",
|
|
522
657
|
description: "Fetches URL content, converts HTML to markdown, and indexes into the searchable knowledge base. " +
|
|
523
658
|
"Raw content never enters context — only a brief confirmation is returned.\n\n" +
|
|
524
|
-
"
|
|
659
|
+
"PREFER THIS OVER WebFetch when you need to reference web documentation later via search. " +
|
|
660
|
+
"WebFetch loads entire page content into context; this tool indexes it and lets you search() on-demand.\n\n" +
|
|
525
661
|
"After fetching, use 'search' to retrieve specific sections on-demand.",
|
|
526
662
|
inputSchema: z.object({
|
|
527
663
|
url: z.string().describe("The URL to fetch and index"),
|
|
@@ -540,7 +676,7 @@ server.registerTool("fetch_and_index", {
|
|
|
540
676
|
timeout: 30_000,
|
|
541
677
|
});
|
|
542
678
|
if (result.exitCode !== 0) {
|
|
543
|
-
return {
|
|
679
|
+
return trackResponse("fetch_and_index", {
|
|
544
680
|
content: [
|
|
545
681
|
{
|
|
546
682
|
type: "text",
|
|
@@ -548,10 +684,10 @@ server.registerTool("fetch_and_index", {
|
|
|
548
684
|
},
|
|
549
685
|
],
|
|
550
686
|
isError: true,
|
|
551
|
-
};
|
|
687
|
+
});
|
|
552
688
|
}
|
|
553
689
|
if (!result.stdout || result.stdout.trim().length === 0) {
|
|
554
|
-
return {
|
|
690
|
+
return trackResponse("fetch_and_index", {
|
|
555
691
|
content: [
|
|
556
692
|
{
|
|
557
693
|
type: "text",
|
|
@@ -559,20 +695,216 @@ server.registerTool("fetch_and_index", {
|
|
|
559
695
|
},
|
|
560
696
|
],
|
|
561
697
|
isError: true,
|
|
562
|
-
};
|
|
698
|
+
});
|
|
563
699
|
}
|
|
564
|
-
// Index the markdown into FTS5
|
|
565
|
-
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));
|
|
566
702
|
}
|
|
567
703
|
catch (err) {
|
|
568
704
|
const message = err instanceof Error ? err.message : String(err);
|
|
569
|
-
return {
|
|
705
|
+
return trackResponse("fetch_and_index", {
|
|
570
706
|
content: [
|
|
571
707
|
{ type: "text", text: `Fetch error: ${message}` },
|
|
572
708
|
],
|
|
573
709
|
isError: true,
|
|
574
|
-
};
|
|
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()} |`);
|
|
575
902
|
}
|
|
903
|
+
const text = lines.join("\n");
|
|
904
|
+
// Track the stats tool itself
|
|
905
|
+
return trackResponse("stats", {
|
|
906
|
+
content: [{ type: "text", text }],
|
|
907
|
+
});
|
|
576
908
|
});
|
|
577
909
|
// ─────────────────────────────────────────────────────────
|
|
578
910
|
// Server startup
|