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/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
- return {
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
- return {
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
- return {
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
- return {
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 previously indexed content using BM25 full-text search. " +
384
- "Returns the top matching chunks with heading context and full content. " +
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
- query: z.string().describe("Natural language search query"),
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("Maximum results to return (default: 3)"),
485
+ .describe("Results per query (default: 3)"),
405
486
  source: z
406
487
  .string()
407
488
  .optional()
408
- .describe("Filter results to a specific indexed source (partial match). " +
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 ({ query, limit, source }) => {
491
+ }, async (params) => {
412
492
  try {
413
493
  const store = getStore();
414
- const results = store.search(query, limit, source);
415
- if (results.length === 0) {
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
- const formatted = results
430
- .map((r, i) => {
431
- const header = `--- Result ${i + 1} [${r.source}] (${r.contentType}) ---`;
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