context-compress 2026.3.22 → 2026.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.
Files changed (120) hide show
  1. package/.claude-plugin/marketplace.json +17 -0
  2. package/.claude-plugin/plugin.json +12 -0
  3. package/.codex-plugin/plugin.json +40 -0
  4. package/.mcp.json +11 -0
  5. package/README.md +275 -44
  6. package/dist/cli/doctor.d.ts.map +1 -1
  7. package/dist/cli/doctor.js +2 -10
  8. package/dist/cli/doctor.js.map +1 -1
  9. package/dist/cli/filter.d.ts +52 -0
  10. package/dist/cli/filter.d.ts.map +1 -0
  11. package/dist/cli/filter.js +200 -0
  12. package/dist/cli/filter.js.map +1 -0
  13. package/dist/cli/index.d.ts +8 -4
  14. package/dist/cli/index.d.ts.map +1 -1
  15. package/dist/cli/index.js +19 -6
  16. package/dist/cli/index.js.map +1 -1
  17. package/dist/cli/lite.d.ts +15 -0
  18. package/dist/cli/lite.d.ts.map +1 -0
  19. package/dist/cli/lite.js +37 -0
  20. package/dist/cli/lite.js.map +1 -0
  21. package/dist/cli/setup.d.ts +23 -1
  22. package/dist/cli/setup.d.ts.map +1 -1
  23. package/dist/cli/setup.js +122 -21
  24. package/dist/cli/setup.js.map +1 -1
  25. package/dist/executor.d.ts.map +1 -1
  26. package/dist/executor.js +7 -4
  27. package/dist/executor.js.map +1 -1
  28. package/dist/filters.d.ts +39 -5
  29. package/dist/filters.d.ts.map +1 -1
  30. package/dist/filters.js +577 -25
  31. package/dist/filters.js.map +1 -1
  32. package/dist/hooks/pretooluse.js +57 -0
  33. package/dist/hooks/pretooluse.js.map +1 -1
  34. package/dist/network.d.ts.map +1 -1
  35. package/dist/network.js +11 -0
  36. package/dist/network.js.map +1 -1
  37. package/dist/server.bundle.mjs +1140 -641
  38. package/dist/server.bundle.mjs.map +4 -4
  39. package/dist/server.d.ts.map +1 -1
  40. package/dist/server.js +36 -612
  41. package/dist/server.js.map +1 -1
  42. package/dist/stats.js +1 -1
  43. package/dist/stats.js.map +1 -1
  44. package/dist/store.d.ts +1 -0
  45. package/dist/store.d.ts.map +1 -1
  46. package/dist/store.js +15 -2
  47. package/dist/store.js.map +1 -1
  48. package/dist/tools/batch-execute.d.ts +4 -0
  49. package/dist/tools/batch-execute.d.ts.map +1 -0
  50. package/dist/tools/batch-execute.js +75 -0
  51. package/dist/tools/batch-execute.js.map +1 -0
  52. package/dist/tools/context.d.ts +17 -0
  53. package/dist/tools/context.d.ts.map +1 -0
  54. package/dist/tools/context.js +2 -0
  55. package/dist/tools/context.js.map +1 -0
  56. package/dist/tools/discover.d.ts +4 -0
  57. package/dist/tools/discover.d.ts.map +1 -0
  58. package/dist/tools/discover.js +65 -0
  59. package/dist/tools/discover.js.map +1 -0
  60. package/dist/tools/execute-file.d.ts +4 -0
  61. package/dist/tools/execute-file.d.ts.map +1 -0
  62. package/dist/tools/execute-file.js +66 -0
  63. package/dist/tools/execute-file.js.map +1 -0
  64. package/dist/tools/execute.d.ts +4 -0
  65. package/dist/tools/execute.d.ts.map +1 -0
  66. package/dist/tools/execute.js +54 -0
  67. package/dist/tools/execute.js.map +1 -0
  68. package/dist/tools/fetch-and-index.d.ts +4 -0
  69. package/dist/tools/fetch-and-index.d.ts.map +1 -0
  70. package/dist/tools/fetch-and-index.js +91 -0
  71. package/dist/tools/fetch-and-index.js.map +1 -0
  72. package/dist/tools/index-content.d.ts +4 -0
  73. package/dist/tools/index-content.d.ts.map +1 -0
  74. package/dist/tools/index-content.js +85 -0
  75. package/dist/tools/index-content.js.map +1 -0
  76. package/dist/tools/search.d.ts +4 -0
  77. package/dist/tools/search.d.ts.map +1 -0
  78. package/dist/tools/search.js +57 -0
  79. package/dist/tools/search.js.map +1 -0
  80. package/dist/tools/stats.d.ts +4 -0
  81. package/dist/tools/stats.d.ts.map +1 -0
  82. package/dist/tools/stats.js +10 -0
  83. package/dist/tools/stats.js.map +1 -0
  84. package/dist/types.d.ts +0 -1
  85. package/dist/types.d.ts.map +1 -1
  86. package/dist/util/auto-mode.d.ts +40 -0
  87. package/dist/util/auto-mode.d.ts.map +1 -0
  88. package/dist/util/auto-mode.js +181 -0
  89. package/dist/util/auto-mode.js.map +1 -0
  90. package/dist/util/fetch-code.d.ts +10 -0
  91. package/dist/util/fetch-code.d.ts.map +1 -0
  92. package/dist/util/fetch-code.js +87 -0
  93. package/dist/util/fetch-code.js.map +1 -0
  94. package/dist/util/intent-filter.d.ts +17 -0
  95. package/dist/util/intent-filter.d.ts.map +1 -0
  96. package/dist/util/intent-filter.js +28 -0
  97. package/dist/util/intent-filter.js.map +1 -0
  98. package/dist/util/label.d.ts +4 -0
  99. package/dist/util/label.d.ts.map +1 -0
  100. package/dist/util/label.js +14 -0
  101. package/dist/util/label.js.map +1 -0
  102. package/dist/util/path.d.ts +8 -0
  103. package/dist/util/path.d.ts.map +1 -0
  104. package/dist/util/path.js +21 -0
  105. package/dist/util/path.js.map +1 -0
  106. package/dist/util/stream-compress.d.ts +36 -0
  107. package/dist/util/stream-compress.d.ts.map +1 -0
  108. package/dist/util/stream-compress.js +104 -0
  109. package/dist/util/stream-compress.js.map +1 -0
  110. package/dist/util/version.d.ts +2 -0
  111. package/dist/util/version.d.ts.map +1 -0
  112. package/dist/util/version.js +15 -0
  113. package/dist/util/version.js.map +1 -0
  114. package/docs/agentic-benchmark.md +110 -0
  115. package/docs/token-reduction-report.md +47 -18
  116. package/hooks/claude-codex-hooks.json +19 -0
  117. package/hooks/pretooluse.mjs +38 -0
  118. package/package.json +12 -8
  119. package/skills/context-compress-audit/SKILL.md +49 -0
  120. package/skills/context-compress-audit/agents/openai.yaml +13 -0
@@ -2980,7 +2980,7 @@ var require_compile = __commonJS({
2980
2980
  const schOrFunc = root.refs[ref];
2981
2981
  if (schOrFunc)
2982
2982
  return schOrFunc;
2983
- let _sch = resolve2.call(this, root, ref);
2983
+ let _sch = resolve5.call(this, root, ref);
2984
2984
  if (_sch === void 0) {
2985
2985
  const schema = (_a = root.localRefs) === null || _a === void 0 ? void 0 : _a[ref];
2986
2986
  const { schemaId } = this.opts;
@@ -3007,7 +3007,7 @@ var require_compile = __commonJS({
3007
3007
  function sameSchemaEnv(s1, s2) {
3008
3008
  return s1.schema === s2.schema && s1.root === s2.root && s1.baseId === s2.baseId;
3009
3009
  }
3010
- function resolve2(root, ref) {
3010
+ function resolve5(root, ref) {
3011
3011
  let sch;
3012
3012
  while (typeof (sch = this.refs[ref]) == "string")
3013
3013
  ref = sch;
@@ -3582,7 +3582,7 @@ var require_fast_uri = __commonJS({
3582
3582
  }
3583
3583
  return uri;
3584
3584
  }
3585
- function resolve2(baseURI, relativeURI, options) {
3585
+ function resolve5(baseURI, relativeURI, options) {
3586
3586
  const schemelessOptions = options ? Object.assign({ scheme: "null" }, options) : { scheme: "null" };
3587
3587
  const resolved = resolveComponent(parse3(baseURI, schemelessOptions), parse3(relativeURI, schemelessOptions), schemelessOptions, true);
3588
3588
  schemelessOptions.skipEscape = true;
@@ -3809,7 +3809,7 @@ var require_fast_uri = __commonJS({
3809
3809
  var fastUri = {
3810
3810
  SCHEMES,
3811
3811
  normalize,
3812
- resolve: resolve2,
3812
+ resolve: resolve5,
3813
3813
  resolveComponent,
3814
3814
  equal,
3815
3815
  serialize,
@@ -11061,9 +11061,7 @@ function debug(...args) {
11061
11061
  }
11062
11062
 
11063
11063
  // src/server.ts
11064
- import { readFileSync as readFileSync3, realpathSync, statSync } from "node:fs";
11065
- import { dirname, join as join4, resolve } from "node:path";
11066
- import { fileURLToPath } from "node:url";
11064
+ import { join as join4 } from "node:path";
11067
11065
 
11068
11066
  // node_modules/zod/v4/core/core.js
11069
11067
  var NEVER2 = Object.freeze({
@@ -19096,7 +19094,7 @@ var Protocol = class {
19096
19094
  return;
19097
19095
  }
19098
19096
  const pollInterval = task2.pollInterval ?? this._options?.defaultTaskPollInterval ?? 1e3;
19099
- await new Promise((resolve2) => setTimeout(resolve2, pollInterval));
19097
+ await new Promise((resolve5) => setTimeout(resolve5, pollInterval));
19100
19098
  options?.signal?.throwIfAborted();
19101
19099
  }
19102
19100
  } catch (error2) {
@@ -19113,7 +19111,7 @@ var Protocol = class {
19113
19111
  */
19114
19112
  request(request, resultSchema, options) {
19115
19113
  const { relatedRequestId, resumptionToken, onresumptiontoken, task, relatedTask } = options ?? {};
19116
- return new Promise((resolve2, reject) => {
19114
+ return new Promise((resolve5, reject) => {
19117
19115
  const earlyReject = (error2) => {
19118
19116
  reject(error2);
19119
19117
  };
@@ -19191,7 +19189,7 @@ var Protocol = class {
19191
19189
  if (!parseResult.success) {
19192
19190
  reject(parseResult.error);
19193
19191
  } else {
19194
- resolve2(parseResult.data);
19192
+ resolve5(parseResult.data);
19195
19193
  }
19196
19194
  } catch (error2) {
19197
19195
  reject(error2);
@@ -19452,12 +19450,12 @@ var Protocol = class {
19452
19450
  }
19453
19451
  } catch {
19454
19452
  }
19455
- return new Promise((resolve2, reject) => {
19453
+ return new Promise((resolve5, reject) => {
19456
19454
  if (signal.aborted) {
19457
19455
  reject(new McpError(ErrorCode.InvalidRequest, "Request cancelled"));
19458
19456
  return;
19459
19457
  }
19460
- const timeoutId = setTimeout(resolve2, interval);
19458
+ const timeoutId = setTimeout(resolve5, interval);
19461
19459
  signal.addEventListener("abort", () => {
19462
19460
  clearTimeout(timeoutId);
19463
19461
  reject(new McpError(ErrorCode.InvalidRequest, "Request cancelled"));
@@ -20557,7 +20555,7 @@ var McpServer = class {
20557
20555
  let task = createTaskResult.task;
20558
20556
  const pollInterval = task.pollInterval ?? 5e3;
20559
20557
  while (task.status !== "completed" && task.status !== "failed" && task.status !== "cancelled") {
20560
- await new Promise((resolve2) => setTimeout(resolve2, pollInterval));
20558
+ await new Promise((resolve5) => setTimeout(resolve5, pollInterval));
20561
20559
  const updatedTask = await extra.taskStore.getTask(taskId);
20562
20560
  if (!updatedTask) {
20563
20561
  throw new McpError(ErrorCode.InternalError, `Task ${taskId} not found during polling`);
@@ -21200,12 +21198,12 @@ var StdioServerTransport = class {
21200
21198
  this.onclose?.();
21201
21199
  }
21202
21200
  send(message) {
21203
- return new Promise((resolve2) => {
21201
+ return new Promise((resolve5) => {
21204
21202
  const json = serializeMessage(message);
21205
21203
  if (this._stdout.write(json)) {
21206
- resolve2();
21204
+ resolve5();
21207
21205
  } else {
21208
- this._stdout.once("drain", resolve2);
21206
+ this._stdout.once("drain", resolve5);
21209
21207
  }
21210
21208
  });
21211
21209
  }
@@ -21218,22 +21216,33 @@ import { tmpdir } from "node:os";
21218
21216
  import { join as join2 } from "node:path";
21219
21217
 
21220
21218
  // src/filters.ts
21221
- function applyCommandFilter(code, stdout) {
21219
+ var DEFAULT_MODE = "balanced";
21220
+ function applyCommandFilter(code, stdout, mode = DEFAULT_MODE) {
21221
+ if (mode === "conservative") return { output: stdout, filtered: false };
21222
21222
  const cmd = code.trim().split(/\s+/)[0];
21223
21223
  const fullCmd = code.trim();
21224
- if (cmd === "git") return filterGit(fullCmd, stdout);
21224
+ if (cmd === "git") return filterGit(fullCmd, stdout, mode);
21225
21225
  if (cmd === "npm" || cmd === "yarn" || cmd === "pnpm" || cmd === "bun")
21226
- return filterPackageManager(fullCmd, stdout);
21226
+ return filterPackageManager(fullCmd, stdout, mode);
21227
21227
  if (fullCmd.includes("test") || fullCmd.includes("jest") || fullCmd.includes("vitest") || fullCmd.includes("pytest") || fullCmd.includes("cargo test")) {
21228
21228
  return filterTestOutput(stdout);
21229
21229
  }
21230
21230
  if (cmd === "cargo" || cmd === "make" || cmd === "gradle")
21231
21231
  return filterBuildOutput(fullCmd, stdout);
21232
21232
  if (cmd === "docker" || cmd === "kubectl") return filterContainerOutput(fullCmd, stdout);
21233
- if (cmd === "ls" || cmd === "find" || cmd === "tree") return filterFileList(fullCmd, stdout);
21233
+ if (cmd === "ls" || cmd === "find" || cmd === "tree")
21234
+ return filterFileList(fullCmd, stdout, mode);
21235
+ if (cmd === "grep" || cmd === "rg" || cmd === "ripgrep") {
21236
+ if (mode === "aggressive") return filterGrep(stdout);
21237
+ }
21238
+ if (mode === "aggressive") {
21239
+ if (cmd === "df") return filterDf(stdout);
21240
+ if (cmd === "du") return filterDu(stdout);
21241
+ if (cmd === "ps") return filterPs(stdout);
21242
+ }
21234
21243
  return { output: stdout, filtered: false };
21235
21244
  }
21236
- function filterGit(cmd, stdout) {
21245
+ function filterGit(cmd, stdout, mode = DEFAULT_MODE) {
21237
21246
  if (/git\s+(push|pull|fetch|clone)/.test(cmd)) {
21238
21247
  const lines = stdout.split("\n");
21239
21248
  const filtered = lines.filter(
@@ -21242,31 +21251,235 @@ function filterGit(cmd, stdout) {
21242
21251
  return { output: filtered.join("\n"), filtered: true };
21243
21252
  }
21244
21253
  if (/git\s+status/.test(cmd)) {
21245
- const lines = stdout.split("\n");
21246
- const filtered = lines.filter((l) => !l.startsWith(" (use ") && l.trim() !== "");
21247
- return { output: filtered.join("\n"), filtered: true };
21254
+ return { output: filterGitStatus(stdout, mode), filtered: true };
21255
+ }
21256
+ if (/git\s+log/.test(cmd) && !cmd.includes("--oneline")) {
21257
+ if (mode === "aggressive") {
21258
+ return { output: aggressiveGitLog(stdout), filtered: true };
21259
+ }
21260
+ if (mode === "balanced") {
21261
+ const truncated = balancedGitLog(stdout);
21262
+ if (truncated.length < stdout.length) {
21263
+ return { output: truncated, filtered: true };
21264
+ }
21265
+ }
21266
+ }
21267
+ if (/git\s+diff/.test(cmd) && mode === "aggressive") {
21268
+ if (/--(stat|name-only|name-status|shortstat|numstat)\b/.test(cmd)) {
21269
+ return { output: stdout, filtered: false };
21270
+ }
21271
+ return { output: aggressiveGitDiff(stdout), filtered: true };
21248
21272
  }
21249
21273
  return { output: stdout, filtered: false };
21250
21274
  }
21251
- function filterPackageManager(cmd, stdout) {
21275
+ function filterGitStatus(stdout, mode) {
21276
+ const lines = stdout.split("\n");
21277
+ const balanced = lines.filter((l) => !l.startsWith(" (use ") && l.trim() !== "");
21278
+ if (mode !== "aggressive") return balanced.join("\n");
21279
+ const out = [];
21280
+ for (const l of balanced) {
21281
+ if (/^On branch/.test(l)) {
21282
+ out.push(l.replace(/^On branch /, "* "));
21283
+ continue;
21284
+ }
21285
+ if (/^Your branch is/.test(l)) continue;
21286
+ if (/^Changes (to be committed|not staged for commit):/.test(l)) continue;
21287
+ if (/^Untracked files:/.test(l)) {
21288
+ out.push("? Untracked:");
21289
+ continue;
21290
+ }
21291
+ if (/^no changes added to commit/.test(l)) continue;
21292
+ if (/^nothing to commit/.test(l)) {
21293
+ out.push("(clean)");
21294
+ continue;
21295
+ }
21296
+ const m = l.match(/^\s*(modified|new file|deleted|renamed|typechange):\s*(.+)$/);
21297
+ if (m) {
21298
+ const code = { modified: "M", "new file": "A", deleted: "D", renamed: "R", typechange: "T" }[m[1]] ?? "?";
21299
+ out.push(`${code} ${m[2]}`);
21300
+ continue;
21301
+ }
21302
+ out.push(l);
21303
+ }
21304
+ return out.join("\n");
21305
+ }
21306
+ function aggressiveGitLog(stdout) {
21307
+ const lines = stdout.split("\n");
21308
+ const out = [];
21309
+ let sha = "";
21310
+ let author = "";
21311
+ let date3 = "";
21312
+ let subject = "";
21313
+ let inCommit = false;
21314
+ let blanksAfterDate = 0;
21315
+ const flush = () => {
21316
+ if (!sha) return;
21317
+ const reltime = date3 ? ` (${humanReltime(date3)})` : "";
21318
+ const auth = author ? ` <${author.replace(/\s*<.*?>/, "").trim()}>` : "";
21319
+ out.push(`${sha.slice(0, 7)} ${subject}${reltime}${auth}`);
21320
+ };
21321
+ for (const line of lines) {
21322
+ const m = line.match(/^commit\s+([0-9a-f]{7,40})/);
21323
+ if (m) {
21324
+ flush();
21325
+ sha = m[1];
21326
+ author = "";
21327
+ date3 = "";
21328
+ subject = "";
21329
+ inCommit = true;
21330
+ blanksAfterDate = 0;
21331
+ continue;
21332
+ }
21333
+ if (!inCommit) continue;
21334
+ if (/^Author:\s/.test(line)) {
21335
+ author = line.replace(/^Author:\s+/, "").trim();
21336
+ continue;
21337
+ }
21338
+ if (/^Date:\s/.test(line)) {
21339
+ date3 = line.replace(/^Date:\s+/, "").trim();
21340
+ continue;
21341
+ }
21342
+ if (line.trim() === "") {
21343
+ blanksAfterDate++;
21344
+ continue;
21345
+ }
21346
+ if (!subject && blanksAfterDate >= 1) {
21347
+ subject = line.trim();
21348
+ }
21349
+ }
21350
+ flush();
21351
+ return out.join("\n");
21352
+ }
21353
+ var BALANCED_GIT_LOG_BODY_LINES = 3;
21354
+ function balancedGitLog(stdout) {
21355
+ const lines = stdout.split("\n");
21356
+ const out = [];
21357
+ let bodyKept = 0;
21358
+ let bodyDropped = 0;
21359
+ let subjectSeen = false;
21360
+ let inCommit = false;
21361
+ let inBody = false;
21362
+ let blanksAfterDate = 0;
21363
+ const flushOmitted = () => {
21364
+ if (bodyDropped > 0) {
21365
+ out.push(` [+${bodyDropped} lines omitted]`);
21366
+ bodyDropped = 0;
21367
+ }
21368
+ };
21369
+ for (const line of lines) {
21370
+ if (/^commit\s+[0-9a-f]{7,40}/.test(line)) {
21371
+ flushOmitted();
21372
+ out.push(line);
21373
+ inCommit = true;
21374
+ inBody = false;
21375
+ subjectSeen = false;
21376
+ bodyKept = 0;
21377
+ blanksAfterDate = 0;
21378
+ continue;
21379
+ }
21380
+ if (!inCommit) {
21381
+ out.push(line);
21382
+ continue;
21383
+ }
21384
+ if (/^(Author|Date|Merge):\s/.test(line)) {
21385
+ out.push(line);
21386
+ continue;
21387
+ }
21388
+ if (line.trim() === "") {
21389
+ out.push(line);
21390
+ blanksAfterDate++;
21391
+ continue;
21392
+ }
21393
+ if (blanksAfterDate >= 1) inBody = true;
21394
+ if (inBody) {
21395
+ if (!subjectSeen) {
21396
+ subjectSeen = true;
21397
+ out.push(line);
21398
+ continue;
21399
+ }
21400
+ if (bodyKept >= BALANCED_GIT_LOG_BODY_LINES) {
21401
+ bodyDropped++;
21402
+ continue;
21403
+ }
21404
+ bodyKept++;
21405
+ }
21406
+ out.push(line);
21407
+ }
21408
+ flushOmitted();
21409
+ return out.join("\n");
21410
+ }
21411
+ function aggressiveGitDiff(stdout) {
21412
+ const lines = stdout.split("\n");
21413
+ const out = [];
21414
+ let currentFile = "";
21415
+ for (const line of lines) {
21416
+ const fm = line.match(/^diff --git a\/(.+?) b\//);
21417
+ if (fm) {
21418
+ currentFile = fm[1];
21419
+ out.push(`@@ ${currentFile}`);
21420
+ continue;
21421
+ }
21422
+ if (/^---\s|^\+\+\+\s|^index\s|^@@\s/.test(line)) continue;
21423
+ if (line.startsWith("+") || line.startsWith("-")) out.push(line);
21424
+ }
21425
+ return out.join("\n");
21426
+ }
21427
+ function humanReltime(dateStr) {
21428
+ const d = new Date(dateStr);
21429
+ if (Number.isNaN(d.getTime())) return dateStr;
21430
+ const ms = Date.now() - d.getTime();
21431
+ const hours = Math.round(ms / 36e5);
21432
+ if (hours < 1) return "just now";
21433
+ if (hours < 24) return `${hours}h ago`;
21434
+ const days = Math.round(hours / 24);
21435
+ if (days < 30) return `${days}d ago`;
21436
+ const months = Math.round(days / 30);
21437
+ if (months < 12) return `${months}mo ago`;
21438
+ return `${Math.round(months / 12)}y ago`;
21439
+ }
21440
+ function filterPackageManager(cmd, stdout, mode = DEFAULT_MODE) {
21252
21441
  if (/\b(install|add|i)\b/.test(cmd)) {
21253
21442
  const lines = stdout.split("\n");
21254
21443
  const filtered = lines.filter(
21255
- (l) => !l.startsWith("npm warn") && !l.includes("packages are looking for funding") && !l.includes("run `npm fund`") && !l.startsWith("npm notice") && !/^[\s\u2502\u251C\u2514\u2500]+$/.test(l) && // tree-drawing characters
21444
+ (l) => !l.startsWith("npm warn") && !l.includes("packages are looking for funding") && !l.includes("run `npm fund`") && !l.startsWith("npm notice") && !/^[\s│├└─]+$/.test(l) && // tree-drawing characters
21256
21445
  !/^\s*$/.test(l)
21257
21446
  );
21447
+ if (mode === "aggressive") {
21448
+ const summaryOnly = filtered.filter(
21449
+ (l) => /^(added|removed|changed|audited)\s+\d+/.test(l) || /vulnerabilit(y|ies)/i.test(l) || /^npm\s+ERR/.test(l)
21450
+ );
21451
+ return { output: summaryOnly.join("\n"), filtered: true };
21452
+ }
21258
21453
  return { output: filtered.join("\n"), filtered: true };
21259
21454
  }
21260
21455
  if (/\btest\b/.test(cmd)) {
21261
21456
  return filterTestOutput(stdout);
21262
21457
  }
21458
+ if (mode === "aggressive" && /\b(ls|list|ll)\b/.test(cmd)) {
21459
+ return filterNpmLs(stdout);
21460
+ }
21263
21461
  return { output: stdout, filtered: false };
21264
21462
  }
21265
- var FAIL_MARKER_RE = /^\s*[\u2717\u2718\u00D7]\s/;
21463
+ function filterNpmLs(stdout) {
21464
+ const lines = stdout.split("\n");
21465
+ const seen = /* @__PURE__ */ new Set();
21466
+ const out = [];
21467
+ for (const l of lines) {
21468
+ const stripped = l.replace(/^[\s│├└─┬]+/u, "").trimEnd();
21469
+ if (!stripped) continue;
21470
+ if (/\bdeduped\b/.test(stripped)) continue;
21471
+ const cleaned = stripped.replace(/^extraneous\s+/, "").replace(/\s+\bextraneous\b/g, "");
21472
+ if (seen.has(cleaned)) continue;
21473
+ seen.add(cleaned);
21474
+ out.push(cleaned);
21475
+ }
21476
+ return { output: out.join("\n"), filtered: true };
21477
+ }
21478
+ var FAIL_MARKER_RE = /^\s*[✗✘×]\s/;
21266
21479
  var FAIL_WORD_RE = /\bFAIL\b/;
21267
21480
  var FAILED_RE = /\bfailed?\b/i;
21268
21481
  var ERROR_RE = /\bERROR\b/;
21269
- var SUMMARY_RE = /^\s*(Tests?|Suites?|Test Suites)\s*:|^\s*(pass|fail|skip|pending|todo)\s|\b\d+\s+(passing|failing|pending|skipped)\b|^(ok|not ok)\s|^\u2139\s|^(PASS|FAIL)\s/i;
21482
+ var SUMMARY_RE = /^\s*(Tests?|Suites?|Test Suites)\s*:|^\s*(pass|fail|skip|pending|todo)\s|\b\d+\s+(passing|failing|pending|skipped)\b|^(ok|not ok)\s|^ℹ\s|^(PASS|FAIL)\s/i;
21270
21483
  function isFailMarker(line) {
21271
21484
  return FAIL_MARKER_RE.test(line) || FAIL_WORD_RE.test(line) || FAILED_RE.test(line) || ERROR_RE.test(line);
21272
21485
  }
@@ -21296,7 +21509,8 @@ function filterTestOutput(stdout) {
21296
21509
  return { output: summary.join("\n"), filtered: true };
21297
21510
  }
21298
21511
  if (failures.length > 0) {
21299
- const result = [...failures, "", ...summary].join("\n");
21512
+ const rollup = summary.filter((l) => !/^PASS\s/i.test(l));
21513
+ const result = [...failures, "", ...rollup].join("\n");
21300
21514
  return { output: result, filtered: true };
21301
21515
  }
21302
21516
  return { output: stdout, filtered: false };
@@ -21304,7 +21518,7 @@ function filterTestOutput(stdout) {
21304
21518
  function filterBuildOutput(cmd, stdout) {
21305
21519
  const lines = stdout.split("\n");
21306
21520
  const filtered = lines.filter(
21307
- (l) => !l.includes("Downloading") && !l.includes("Downloaded") && !/Compiling\s+\d+\s+of\s+\d+/.test(l) && !l.includes("Blocking waiting for file lock") && !/^\s*$/.test(l)
21521
+ (l) => !l.includes("Downloading") && !l.includes("Downloaded") && !/Compiling\s+\d+\s+of\s+\d+/.test(l) && !/^\s*Compiling\s+[\w-]+\s+v\d/.test(l) && !/^\s*Checking\s+[\w-]+\s+v\d/.test(l) && !l.includes("Blocking waiting for file lock") && !/^\s*$/.test(l)
21308
21522
  );
21309
21523
  return { output: filtered.join("\n"), filtered: filtered.length < lines.length };
21310
21524
  }
@@ -21312,15 +21526,47 @@ function filterContainerOutput(cmd, stdout) {
21312
21526
  if (/docker\s+build/.test(cmd)) {
21313
21527
  const lines = stdout.split("\n");
21314
21528
  const filtered = lines.filter(
21315
- (l) => !l.startsWith(" ---> ") && !l.startsWith("Sending build context")
21529
+ (l) => !l.startsWith(" ---> ") && !l.startsWith("Sending build context") && !/^\s*$/.test(l)
21316
21530
  );
21317
21531
  return { output: filtered.join("\n"), filtered: true };
21318
21532
  }
21533
+ if (/^kubectl\s+(get|describe)\b/.test(cmd)) {
21534
+ const lines = stdout.split("\n").filter((l) => l.length > 0);
21535
+ if (lines.length <= 30) return { output: stdout, filtered: false };
21536
+ const header = lines[0];
21537
+ const rows = lines.slice(1);
21538
+ const headerCols = header.split(/\s{2,}/);
21539
+ const hasNamespace = headerCols[0]?.toUpperCase() === "NAMESPACE";
21540
+ const statusIdx = headerCols.findIndex((c) => /^STATUS$/i.test(c));
21541
+ const counts = /* @__PURE__ */ new Map();
21542
+ for (const row of rows) {
21543
+ const cols = row.split(/\s{2,}/);
21544
+ const ns = hasNamespace ? cols[0] : "(default)";
21545
+ const status = statusIdx >= 0 ? cols[statusIdx] : "\u2014";
21546
+ const key = `${ns} ${status}`;
21547
+ counts.set(key, (counts.get(key) ?? 0) + 1);
21548
+ }
21549
+ const summaryLines = [`${header}`, `(${rows.length} rows summarized by namespace/status)`];
21550
+ for (const [key, n] of [...counts.entries()].sort((a, b) => b[1] - a[1])) {
21551
+ const [ns, status] = key.split(" ");
21552
+ summaryLines.push(` ${ns} \u2014 ${status}: ${n}`);
21553
+ }
21554
+ return { output: summaryLines.join("\n"), filtered: true };
21555
+ }
21319
21556
  return { output: stdout, filtered: false };
21320
21557
  }
21321
- function filterFileList(cmd, stdout) {
21558
+ function filterFileList(cmd, stdout, mode = DEFAULT_MODE) {
21559
+ const isLs = /^ls\b/.test(cmd);
21560
+ const isLong = /-l/.test(cmd);
21561
+ if (mode === "aggressive" && isLs && isLong) {
21562
+ return { output: aggressiveLsLong(stdout), filtered: true };
21563
+ }
21564
+ if (mode === "balanced" && isLs && isLong) {
21565
+ return { output: balancedLsLong(stdout), filtered: true };
21566
+ }
21567
+ const { summarizeAt, minDirs } = mode === "aggressive" ? { summarizeAt: 10, minDirs: 3 } : { summarizeAt: 20, minDirs: 4 };
21322
21568
  const lines = stdout.split("\n").filter((l) => l.trim() !== "");
21323
- if (lines.length <= 30) return { output: stdout, filtered: false };
21569
+ if (lines.length <= summarizeAt) return { output: stdout, filtered: false };
21324
21570
  if (cmd.includes("-R") || cmd.startsWith("find")) {
21325
21571
  const dirs = /* @__PURE__ */ new Map();
21326
21572
  for (const line of lines) {
@@ -21328,7 +21574,7 @@ function filterFileList(cmd, stdout) {
21328
21574
  const dir = parts.length > 1 ? parts.slice(0, -1).join("/") : ".";
21329
21575
  dirs.set(dir, (dirs.get(dir) ?? 0) + 1);
21330
21576
  }
21331
- if (dirs.size > 5 && lines.length > 50) {
21577
+ if (dirs.size > minDirs) {
21332
21578
  const summary = Array.from(dirs.entries()).sort((a, b) => b[1] - a[1]).map(([dir, count]) => ` ${dir}/ (${count} files)`).join("\n");
21333
21579
  return {
21334
21580
  output: `${lines.length} files found:
@@ -21339,6 +21585,139 @@ ${summary}`,
21339
21585
  }
21340
21586
  return { output: stdout, filtered: false };
21341
21587
  }
21588
+ function balancedLsLong(stdout) {
21589
+ const lines = stdout.split("\n");
21590
+ const out = [];
21591
+ for (const line of lines) {
21592
+ if (line.trim() === "") continue;
21593
+ if (/^total\s+\d+/.test(line)) continue;
21594
+ const m = line.match(
21595
+ /^([dlcb-])[rwxst@+\-]{9,}\s+\d+\s+\S+\s+\S+\s+\S+\s+\S+\s+\S+\s+\S+\s+(\.\.?)$/
21596
+ );
21597
+ if (m) continue;
21598
+ out.push(line);
21599
+ }
21600
+ return out.join("\n");
21601
+ }
21602
+ function aggressiveLsLong(stdout) {
21603
+ const lines = stdout.split("\n");
21604
+ const out = [];
21605
+ let inSection = false;
21606
+ for (const line of lines) {
21607
+ if (/^[^\s]+:$/.test(line.trim())) {
21608
+ out.push(line.trim());
21609
+ inSection = true;
21610
+ continue;
21611
+ }
21612
+ if (/^total\s+\d+/.test(line)) continue;
21613
+ if (line.trim() === "") continue;
21614
+ const m = line.match(
21615
+ /^([dlcb-])[rwxst@+\-]{9,}\s+\d+\s+\S+\s+\S+\s+(\S+)\s+\S+\s+\S+\s+\S+\s+(.+)$/
21616
+ );
21617
+ if (m) {
21618
+ const type = m[1];
21619
+ const sizeStr = m[2];
21620
+ const name = m[3];
21621
+ if (name === "." || name === "..") continue;
21622
+ if (type === "d" && inSection) continue;
21623
+ out.push(name + (type === "d" ? "/" : ` ${formatSize(sizeStr)}`));
21624
+ continue;
21625
+ }
21626
+ out.push(line);
21627
+ }
21628
+ return out.join("\n");
21629
+ }
21630
+ function formatSize(s) {
21631
+ const n = Number.parseInt(s, 10);
21632
+ if (Number.isNaN(n)) return s;
21633
+ if (n >= 1024 * 1024) return `${(n / 1024 / 1024).toFixed(1)}M`;
21634
+ if (n >= 1024) return `${(n / 1024).toFixed(1)}K`;
21635
+ return `${n}B`;
21636
+ }
21637
+ function filterGrep(stdout) {
21638
+ const lines = stdout.split("\n").filter((l) => l.length > 0);
21639
+ if (lines.length === 0) return { output: stdout, filtered: false };
21640
+ const byFile = /* @__PURE__ */ new Map();
21641
+ for (const line of lines) {
21642
+ const m = line.match(/^([^:]+):(\d+):(.*)$/);
21643
+ if (!m) {
21644
+ const arr2 = byFile.get("(no path)") ?? [];
21645
+ arr2.push(line.length > 100 ? `${line.slice(0, 100)}\u2026` : line);
21646
+ byFile.set("(no path)", arr2);
21647
+ continue;
21648
+ }
21649
+ const [, file, lineNo, content] = m;
21650
+ const truncated = content.length > 100 ? `${content.slice(0, 100)}\u2026` : content;
21651
+ const arr = byFile.get(file) ?? [];
21652
+ arr.push(` L${lineNo}: ${truncated.trim()}`);
21653
+ byFile.set(file, arr);
21654
+ }
21655
+ const out = [];
21656
+ for (const [file, hits] of byFile) {
21657
+ out.push(`${file} (${hits.length})`);
21658
+ for (const h of hits.slice(0, 8)) out.push(h);
21659
+ if (hits.length > 8) out.push(` ... +${hits.length - 8} more matches`);
21660
+ }
21661
+ return { output: out.join("\n"), filtered: true };
21662
+ }
21663
+ function filterDf(stdout) {
21664
+ const lines = stdout.split("\n");
21665
+ if (lines.length === 0) return { output: stdout, filtered: false };
21666
+ const header = lines[0];
21667
+ const out = [header.replace(/\s{2,}/g, " ")];
21668
+ for (const line of lines.slice(1)) {
21669
+ if (!line.trim()) continue;
21670
+ if (/^(tmpfs|devfs|devtmpfs|udev|overlay|map\s|none\s|\/dev\/loop)/.test(line)) continue;
21671
+ out.push(line.replace(/\s{2,}/g, " "));
21672
+ }
21673
+ return { output: out.join("\n"), filtered: true };
21674
+ }
21675
+ function filterDu(stdout) {
21676
+ const lines = stdout.split("\n").filter((l) => l.trim() !== "");
21677
+ if (lines.length <= 25) return { output: stdout, filtered: false };
21678
+ const parsed = lines.map((l) => {
21679
+ const m = l.match(/^([\d.]+[KMGT]?B?)?\s*(.*)$/);
21680
+ if (!m) return null;
21681
+ const sizeRaw = m[1] ?? "0";
21682
+ const path = m[2];
21683
+ return { sizeRaw, sizeBytes: parseDuSize(sizeRaw), path, line: l };
21684
+ }).filter((x) => x !== null);
21685
+ parsed.sort((a, b) => b.sizeBytes - a.sizeBytes);
21686
+ const top = parsed.slice(0, 20).map((p) => p.line);
21687
+ return {
21688
+ output: `(top 20 of ${parsed.length} entries by size)
21689
+ ${top.join("\n")}`,
21690
+ filtered: true
21691
+ };
21692
+ }
21693
+ function parseDuSize(s) {
21694
+ const m = s.match(/^([\d.]+)([KMGT])?B?$/i);
21695
+ if (!m) return 0;
21696
+ const n = Number.parseFloat(m[1]);
21697
+ const unit = (m[2] ?? "").toUpperCase();
21698
+ const factor = unit === "T" ? 1024 ** 4 : unit === "G" ? 1024 ** 3 : unit === "M" ? 1024 ** 2 : unit === "K" ? 1024 : 1;
21699
+ return n * factor;
21700
+ }
21701
+ function filterPs(stdout) {
21702
+ const lines = stdout.split("\n");
21703
+ if (lines.length <= 2) return { output: stdout, filtered: false };
21704
+ const header = lines[0];
21705
+ const isAux = header.includes("USER") && header.includes("%CPU") && header.includes("%MEM");
21706
+ if (!isAux) return { output: stdout, filtered: false };
21707
+ const out = ["PID %CPU %MEM CMD"];
21708
+ for (const line of lines.slice(1)) {
21709
+ if (!line.trim()) continue;
21710
+ const parts = line.trim().split(/\s+/);
21711
+ if (parts.length < 11) continue;
21712
+ const pid = parts[1];
21713
+ const cpu = parts[2];
21714
+ const mem = parts[3];
21715
+ const cmd = parts.slice(10).join(" ");
21716
+ if (/^\[.*\]$/.test(cmd)) continue;
21717
+ out.push(`${pid.padEnd(5)} ${cpu.padStart(4)} ${mem.padStart(4)} ${cmd}`);
21718
+ }
21719
+ return { output: out.join("\n"), filtered: true };
21720
+ }
21342
21721
 
21343
21722
  // src/utils.ts
21344
21723
  function detectInjectionPatterns(content) {
@@ -21389,8 +21768,9 @@ function formatBytes(bytes) {
21389
21768
  // src/executor.ts
21390
21769
  var DEFAULT_TIMEOUT = 3e4;
21391
21770
  var ANSI_RE = /\x1b\[[0-9;]*[a-zA-Z]/;
21771
+ var ANSI_RE_G = /\x1b\[[0-9;]*[a-zA-Z]/g;
21392
21772
  function stripAnsi(str) {
21393
- return str.replace(new RegExp(ANSI_RE.source, "g"), "");
21773
+ return str.replace(ANSI_RE_G, "");
21394
21774
  }
21395
21775
  var SAFE_ENV_KEYS = [
21396
21776
  "PATH",
@@ -21448,8 +21828,7 @@ function stripProgressLines(output) {
21448
21828
  const lines = output.split("\n");
21449
21829
  const filtered = lines.filter((l) => {
21450
21830
  const trimmed = l.trim();
21451
- if (ANSI_RE.test(l) && trimmed.replace(new RegExp(ANSI_RE.source, "g"), "").trim() === "")
21452
- return false;
21831
+ if (ANSI_RE.test(l) && trimmed.replace(ANSI_RE_G, "").trim() === "") return false;
21453
21832
  if (/^[\s\[│├└─═━▓░█▒▏▎▍▌▋▊▉\]>=#\-.\d%]+$/.test(trimmed) && trimmed.length > 3) return false;
21454
21833
  if (/^[⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏\-\\|/]\s/.test(trimmed)) return false;
21455
21834
  if (/(?:downloading|uploading|fetching|resolving)\s+[\d.]+\s*[kmg]?b/i.test(trimmed))
@@ -21676,7 +22055,7 @@ var SubprocessExecutor = class {
21676
22055
  return this.execute({ ...opts, code });
21677
22056
  }
21678
22057
  spawnAndCapture(cmd, args, cwd, timeout, maxOutput, useShell, shellCode) {
21679
- return new Promise((resolve2) => {
22058
+ return new Promise((resolve5) => {
21680
22059
  const hardCap = this.config.hardCapBytes;
21681
22060
  const stdoutChunks = [];
21682
22061
  const stderrChunks = [];
@@ -21716,7 +22095,7 @@ var SubprocessExecutor = class {
21716
22095
  this.activeProcesses.delete(proc);
21717
22096
  if (!resolved) {
21718
22097
  resolved = true;
21719
- resolve2({
22098
+ resolve5({
21720
22099
  stdout: "",
21721
22100
  stderr: err.message,
21722
22101
  exitCode: 1,
@@ -21740,13 +22119,13 @@ var SubprocessExecutor = class {
21740
22119
  stdout += `
21741
22120
  [output capped at ${formatBytes(hardCap)} \u2014 process killed]`;
21742
22121
  }
22122
+ stdout = stripAnsi(stdout);
21743
22123
  if (shellCode && stdout) {
21744
22124
  const filtered = applyCommandFilter(shellCode, stdout);
21745
22125
  if (filtered.filtered) {
21746
22126
  stdout = filtered.output;
21747
22127
  }
21748
22128
  }
21749
- stdout = stripAnsi(stdout);
21750
22129
  if (stdout.length > 1e4) {
21751
22130
  stdout = stripProgressLines(stdout);
21752
22131
  stdout = deduplicateLines(stdout);
@@ -21756,7 +22135,7 @@ var SubprocessExecutor = class {
21756
22135
  if (truncated) {
21757
22136
  stdout = smartTruncate(stdout, maxOutput);
21758
22137
  }
21759
- resolve2({
22138
+ resolve5({
21760
22139
  stdout,
21761
22140
  stderr,
21762
22141
  exitCode: code,
@@ -21789,65 +22168,6 @@ async function __cm_main(){${code}}
21789
22168
  __cm_main().then(()=>{${epilogue}}).catch(e=>{console.error(e);${epilogue}process.exit(1)});`;
21790
22169
  }
21791
22170
 
21792
- // src/network.ts
21793
- import dns from "node:dns";
21794
- function isPrivateHost(hostname2) {
21795
- const h = hostname2.startsWith("[") && hostname2.endsWith("]") ? hostname2.slice(1, -1) : hostname2;
21796
- const lower = h.toLowerCase();
21797
- if (lower === "localhost" || lower === "0.0.0.0") return true;
21798
- if (/^0\./.test(h)) return true;
21799
- if (/^127\./.test(h)) return true;
21800
- if (/^10\./.test(h)) return true;
21801
- if (/^172\.(1[6-9]|2\d|3[01])\./.test(h)) return true;
21802
- if (/^192\.168\./.test(h)) return true;
21803
- if (/^169\.254\./.test(h)) return true;
21804
- if (/^100\.(6[4-9]|[7-9]\d|1[01]\d|12[0-7])\./.test(h)) return true;
21805
- if (lower === "::1") return true;
21806
- if (lower === "::" || lower === "0:0:0:0:0:0:0:0") return true;
21807
- const mappedMatch = lower.match(/^::ffff:(\d+\.\d+\.\d+\.\d+)$/);
21808
- if (mappedMatch) return isPrivateHost(mappedMatch[1]);
21809
- if (/^fe[89ab]/i.test(h)) return true;
21810
- if (/^f[cd]/i.test(h)) return true;
21811
- return false;
21812
- }
21813
- async function resolveAndValidate(url) {
21814
- const parsed = new URL(url);
21815
- const hostname2 = parsed.hostname;
21816
- if (/^\d+\.\d+\.\d+\.\d+$/.test(hostname2) || hostname2.includes(":")) {
21817
- if (isPrivateHost(hostname2)) {
21818
- throw new Error(`Blocked: resolved IP ${hostname2} is a private/internal address`);
21819
- }
21820
- return { url, resolvedIp: null };
21821
- }
21822
- let resolvedIp = null;
21823
- let v4Error = false;
21824
- let v6Error = false;
21825
- const [v4Result, v6Result] = await Promise.allSettled([
21826
- dns.promises.lookup(hostname2, { family: 4 }),
21827
- dns.promises.lookup(hostname2, { family: 6 })
21828
- ]);
21829
- if (v4Result.status === "fulfilled") {
21830
- if (isPrivateHost(v4Result.value.address)) {
21831
- throw new Error(`Blocked: ${hostname2} resolved to private IP ${v4Result.value.address}`);
21832
- }
21833
- resolvedIp = v4Result.value.address;
21834
- } else {
21835
- v4Error = true;
21836
- }
21837
- if (v6Result.status === "fulfilled") {
21838
- if (isPrivateHost(v6Result.value.address)) {
21839
- throw new Error(`Blocked: ${hostname2} resolved to private IPv6 ${v6Result.value.address}`);
21840
- }
21841
- if (!resolvedIp) resolvedIp = v6Result.value.address;
21842
- } else {
21843
- v6Error = true;
21844
- }
21845
- if (v4Error && v6Error) {
21846
- throw new Error(`DNS resolution failed for ${hostname2}: unable to verify host safety`);
21847
- }
21848
- return { url, resolvedIp };
21849
- }
21850
-
21851
22171
  // src/runtime/index.ts
21852
22172
  import { exec } from "node:child_process";
21853
22173
  import { promisify } from "node:util";
@@ -22233,7 +22553,7 @@ var SessionTracker = class {
22233
22553
  cumulative.lastSeen = (/* @__PURE__ */ new Date()).toISOString();
22234
22554
  for (const [name, calls] of Object.entries(snap.calls)) {
22235
22555
  if (!cumulative.perCommand[name]) {
22236
- cumulative.perCommand[name] = { calls: 0, bytesSaved: 0 };
22556
+ cumulative.perCommand[name] = { calls: 0 };
22237
22557
  }
22238
22558
  cumulative.perCommand[name].calls += calls;
22239
22559
  }
@@ -22510,6 +22830,9 @@ var ContentStore = class {
22510
22830
  insertChunkStmt;
22511
22831
  vocabCountStmt;
22512
22832
  vocabInsertStmt;
22833
+ // Cache for getDistinctiveTerms — keyed by sourceId, with "_all" for the global query.
22834
+ // Invalidated whenever index() runs, since chunk counts/distributions change.
22835
+ distinctiveTermsCache = /* @__PURE__ */ new Map();
22513
22836
  constructor(options) {
22514
22837
  let path;
22515
22838
  if (typeof options === "string") {
@@ -22602,6 +22925,7 @@ var ContentStore = class {
22602
22925
  return sourceId2;
22603
22926
  });
22604
22927
  const sourceId = tx();
22928
+ this.distinctiveTermsCache.clear();
22605
22929
  return {
22606
22930
  sourceId,
22607
22931
  label,
@@ -22725,10 +23049,16 @@ var ContentStore = class {
22725
23049
  * Get distinctive terms for search hint.
22726
23050
  */
22727
23051
  getDistinctiveTerms(sourceId) {
23052
+ const cacheKey = sourceId ?? "_all";
23053
+ const cached2 = this.distinctiveTermsCache.get(cacheKey);
23054
+ if (cached2) return cached2;
22728
23055
  const totalChunks = this.db.prepare(
22729
23056
  sourceId ? "SELECT COUNT(*) as cnt FROM chunks WHERE source_id = ?" : "SELECT COUNT(*) as cnt FROM chunks"
22730
23057
  ).get(...sourceId ? [sourceId] : []).cnt;
22731
- if (totalChunks === 0) return [];
23058
+ if (totalChunks === 0) {
23059
+ this.distinctiveTermsCache.set(cacheKey, []);
23060
+ return [];
23061
+ }
22732
23062
  const filter = sourceId ? " WHERE source_id = ?" : "";
22733
23063
  const stmt = this.db.prepare(`SELECT content FROM chunks${filter} LIMIT 500`);
22734
23064
  const rows = sourceId ? stmt.all(sourceId) : stmt.all();
@@ -22753,7 +23083,9 @@ var ContentStore = class {
22753
23083
  scored.push({ word, score });
22754
23084
  }
22755
23085
  scored.sort((a, b) => b.score - a.score);
22756
- return scored.slice(0, 40).map((s) => s.word);
23086
+ const terms = scored.slice(0, 40).map((s) => s.word);
23087
+ this.distinctiveTermsCache.set(cacheKey, terms);
23088
+ return terms;
22757
23089
  }
22758
23090
  /**
22759
23091
  * List all indexed sources with metadata.
@@ -22899,160 +23231,238 @@ function cleanupStaleDbs() {
22899
23231
  return cleaned;
22900
23232
  }
22901
23233
 
22902
- // src/types.ts
22903
- var ALL_LANGUAGES = [
22904
- "javascript",
22905
- "typescript",
22906
- "python",
22907
- "shell",
22908
- "ruby",
22909
- "go",
22910
- "rust",
22911
- "php",
22912
- "perl",
22913
- "r",
22914
- "elixir"
22915
- ];
23234
+ // src/tools/batch-execute.ts
23235
+ function registerBatchExecuteTool(server2, ctx) {
23236
+ const { executor, store, tracker, config: config3, withExecutionLimit } = ctx;
23237
+ server2.tool(
23238
+ "batch_execute",
23239
+ "Execute multiple commands in ONE call, auto-index all output, and search with multiple queries. Returns search results directly \u2014 no follow-up calls needed.\n\nTHIS IS THE PRIMARY TOOL. Use this instead of multiple execute() calls.\n\nOne batch_execute call replaces 30+ execute calls + 10+ search calls.\nProvide all commands to run and all queries to search \u2014 everything happens in one round trip.",
23240
+ {
23241
+ commands: external_exports.array(
23242
+ external_exports.object({
23243
+ label: external_exports.string().describe("Section header for this command's output"),
23244
+ command: external_exports.string().describe("Shell command to execute")
23245
+ })
23246
+ ).describe("Commands to execute as a batch."),
23247
+ queries: external_exports.array(external_exports.string()).describe(
23248
+ "Search queries to extract information from indexed output. Use 5-8 comprehensive queries."
23249
+ ),
23250
+ timeout: external_exports.number().default(6e4).describe("Max execution time in ms (default: 60s)")
23251
+ },
23252
+ async ({ commands, queries, timeout }) => {
23253
+ const commandResults = await limitConcurrency(
23254
+ commands.map((cmd) => async () => {
23255
+ const result = await withExecutionLimit(
23256
+ () => executor.execute({
23257
+ language: "shell",
23258
+ code: cmd.command,
23259
+ timeout
23260
+ })
23261
+ );
23262
+ return { label: cmd.label, result };
23263
+ }),
23264
+ 4
23265
+ );
23266
+ let combined = "";
23267
+ const inventory = [];
23268
+ for (let i = 0; i < commandResults.length; i++) {
23269
+ const settled = commandResults[i];
23270
+ const label = commands[i].label;
23271
+ if (settled.status === "fulfilled") {
23272
+ const { result } = settled.value;
23273
+ const output2 = result.stdout || "(no output)";
23274
+ combined += `## ${label}
23275
+
23276
+ ${output2}
22916
23277
 
22917
- // src/server.ts
22918
- var LANGUAGE_ENUM = ALL_LANGUAGES;
22919
- var projectDir = process.env.CLAUDE_PROJECT_DIR ?? process.cwd();
22920
- function isWithinProject(absPath) {
22921
- try {
22922
- const normalized = realpathSync(resolve(absPath));
22923
- const realProjectDir = realpathSync(projectDir);
22924
- return normalized === realProjectDir || normalized.startsWith(`${realProjectDir}/`);
22925
- } catch {
22926
- const normalized = resolve(absPath);
22927
- return normalized === projectDir || normalized.startsWith(`${projectDir}/`);
22928
- }
22929
- }
22930
- function getVersion() {
22931
- try {
22932
- const __dirname = dirname(fileURLToPath(import.meta.url));
22933
- const pkgPath = join4(__dirname, "..", "package.json");
22934
- const pkg = JSON.parse(readFileSync3(pkgPath, "utf-8"));
22935
- return pkg.version ?? "1.0.0";
22936
- } catch {
22937
- return "1.0.0";
22938
- }
22939
- }
22940
- function compactLabel(normal, level) {
22941
- if (level === "ultra") {
22942
- return normal.replace(/\*\*/g, "").replace(/Use search\(queries: \[\.\.\.]\) to retrieve.*$/gm, "\u2192 search() for more").replace(/Searchable terms: .+$/gm, "");
22943
- }
22944
- if (level === "compact") {
22945
- return normal.replace(
22946
- /Use search\(queries: \[\.\.\.]\) to retrieve full content of any section\./,
22947
- "\u2192 search() for details"
22948
- );
22949
- }
22950
- return normal;
22951
- }
22952
- async function createServer(config3) {
22953
- const version2 = getVersion();
22954
- debug("Version:", version2);
22955
- cleanupStaleDbs();
22956
- const runtimes = await detectRuntimes();
22957
- const bunDetected = hasBun(runtimes);
22958
- debug("Runtimes detected:", runtimes.size);
22959
- const executor = new SubprocessExecutor(runtimes, config3);
22960
- let store;
22961
- let dbFallback = false;
22962
- try {
22963
- store = new ContentStore({ persistDb: config3.persistDb, dbDir: config3.dbDir });
22964
- } catch (e) {
22965
- debug("Failed to create DB, falling back to in-memory:", e);
22966
- store = new ContentStore(":memory:");
22967
- dbFallback = true;
22968
- }
22969
- const cumulativeFile = config3.persistDb ? join4(config3.dbDir ?? join4(projectDir, ".context-compress"), "stats.json") : void 0;
22970
- const tracker = new SessionTracker(cumulativeFile);
22971
- let activeExecutions = 0;
22972
- const MAX_CONCURRENT_EXECUTIONS = 8;
22973
- const EXECUTION_LIMIT_ERROR = "Error: too many concurrent executions. Try again shortly.";
22974
- async function withExecutionLimit(fn) {
22975
- if (activeExecutions >= MAX_CONCURRENT_EXECUTIONS) {
22976
- throw new Error(EXECUTION_LIMIT_ERROR);
22977
- }
22978
- activeExecutions++;
22979
- try {
22980
- return await fn();
22981
- } finally {
22982
- activeExecutions--;
22983
- }
22984
- }
22985
- function applyIntentFilter(output, intent, sourceLabel) {
22986
- if (Buffer.byteLength(output) <= config3.intentSearchThreshold) return output;
22987
- const indexed = store.index(output, sourceLabel);
22988
- tracker.trackIndexed(Buffer.byteLength(output));
22989
- const searchResults = store.search(intent, { limit: 3 });
22990
- const terms = store.getDistinctiveTerms(indexed.sourceId);
22991
- let filtered = `Indexed ${indexed.totalChunks} sections from ${sourceLabel}.
22992
23278
  `;
22993
- filtered += `${searchResults.results.length} sections matched "${intent}":
23279
+ const lineCount = output2.split("\n").length;
23280
+ inventory.push(`- **${label}**: ${lineCount} lines`);
23281
+ } else {
23282
+ combined += `## ${label}
23283
+
23284
+ (error: ${settled.reason})
22994
23285
 
22995
23286
  `;
22996
- for (const hit of searchResults.results) {
22997
- filtered += ` - **${hit.title}**: ${hit.snippet.slice(0, 200)}
23287
+ inventory.push(`- **${label}**: error`);
23288
+ }
23289
+ }
23290
+ const indexed = store.index(combined, "batch_execute");
23291
+ tracker.trackIndexed(Buffer.byteLength(combined));
23292
+ const searchResults = [];
23293
+ let totalBytes = 0;
23294
+ for (const query of queries) {
23295
+ if (totalBytes > config3.batchMaxBytes) break;
23296
+ let result = store.search(query, { source: "batch_execute", limit: 5 });
23297
+ if (result.results.length === 0) {
23298
+ result = store.search(query, { limit: 5 });
23299
+ }
23300
+ let block = `## ${query}
23301
+
22998
23302
  `;
22999
- }
23000
- if (terms.length > 0 && config3.compressionLevel !== "ultra") {
23001
- filtered += `
23002
- Searchable terms: ${terms.join(", ")}
23303
+ if (result.results.length === 0) {
23304
+ block += "No results found.\n";
23305
+ } else {
23306
+ for (const hit of result.results) {
23307
+ block += `--- [${hit.source}] ---
23308
+ ### ${hit.title}
23309
+
23310
+ ${hit.snippet}
23311
+
23003
23312
  `;
23313
+ }
23314
+ }
23315
+ searchResults.push(block);
23316
+ totalBytes += Buffer.byteLength(block);
23317
+ }
23318
+ const terms = store.getDistinctiveTerms(indexed.sourceId);
23319
+ let output = `**Inventory** (${commands.length} commands):
23320
+ ${inventory.join("\n")}
23321
+
23322
+ `;
23323
+ output += searchResults.join("\n---\n\n");
23324
+ if (terms.length > 0) {
23325
+ output += `
23326
+
23327
+ Searchable terms: ${terms.join(", ")}`;
23328
+ }
23329
+ tracker.trackCall("batch_execute", Buffer.byteLength(output));
23330
+ return { content: [{ type: "text", text: output }] };
23004
23331
  }
23005
- filtered += "\nUse search(queries: [...]) to retrieve full content of any section.";
23006
- return compactLabel(filtered, config3.compressionLevel);
23007
- }
23008
- const shutdown = () => {
23009
- try {
23010
- tracker.saveCumulative();
23011
- } catch {
23012
- }
23013
- try {
23014
- executor.shutdown();
23015
- } catch {
23016
- }
23017
- try {
23018
- store.close();
23019
- } catch {
23020
- }
23021
- };
23022
- process.on("SIGINT", shutdown);
23023
- process.on("SIGTERM", shutdown);
23024
- process.on("beforeExit", shutdown);
23025
- process.on("uncaughtException", (err) => {
23026
- debug("Uncaught exception:", err);
23027
- shutdown();
23028
- process.exit(1);
23029
- });
23030
- process.on("unhandledRejection", (err) => {
23031
- debug("Unhandled rejection:", err);
23032
- shutdown();
23033
- process.exit(1);
23034
- });
23035
- const searchCalls = [];
23036
- const server2 = new McpServer({
23037
- name: "context-compress",
23038
- version: version2
23039
- });
23332
+ );
23333
+ }
23334
+
23335
+ // src/tools/discover.ts
23336
+ function registerDiscoverTool(server2, ctx) {
23337
+ const { store, tracker, dbFallback } = ctx;
23040
23338
  server2.tool(
23041
- "execute",
23042
- `Execute code in a sandboxed subprocess. Only stdout enters context \u2014 raw data stays in the subprocess. Use instead of bash/cat when output would exceed ~5KB. ${bunDetected ? "(Bun detected \u2014 JS/TS runs 3-5x faster) " : ""}Available: ${ALL_LANGUAGES.join(", ")}.
23339
+ "discover",
23340
+ "Shows what's in the knowledge base and suggests optimization opportunities. Lists all indexed sources, chunk counts, searchable terms, and recommends next actions. Use this to understand what data is available for search.",
23341
+ {},
23342
+ async () => {
23343
+ const storeStats = store.getStats();
23344
+ const snap = tracker.getSnapshot();
23345
+ const lines = [];
23346
+ lines.push("## Knowledge Base Discovery\n");
23347
+ if (storeStats.totalSources === 0) {
23348
+ lines.push("No content indexed yet. Use these tools to build the knowledge base:\n");
23349
+ lines.push("- `batch_execute` \u2014 run commands and auto-index output");
23350
+ lines.push("- `execute` with `intent` \u2014 auto-indexes large output");
23351
+ lines.push("- `index` \u2014 index documentation or files");
23352
+ lines.push("- `fetch_and_index` \u2014 fetch and index web pages");
23353
+ } else {
23354
+ lines.push("| Metric | Value |");
23355
+ lines.push("|--------|-------|");
23356
+ lines.push(`| Indexed sources | ${storeStats.totalSources} |`);
23357
+ lines.push(`| Total chunks | ${storeStats.totalChunks} |`);
23358
+ lines.push(`| Vocabulary size | ${storeStats.vocabularySize} |`);
23359
+ lines.push(
23360
+ `| Trigram index | ${storeStats.hasTrigramTable ? "active" : "lazy (not yet needed)"} |`
23361
+ );
23362
+ const sources = store.listSources();
23363
+ if (sources.length > 0) {
23364
+ lines.push("\n### Indexed Sources\n");
23365
+ for (const src of sources) {
23366
+ lines.push(
23367
+ `- **${src.label}** \u2014 ${src.chunkCount} chunks${src.codeChunks > 0 ? ` (${src.codeChunks} with code)` : ""}`
23368
+ );
23369
+ }
23370
+ }
23371
+ const terms = store.getDistinctiveTerms();
23372
+ if (terms.length > 0) {
23373
+ lines.push("\n### Top Searchable Terms\n");
23374
+ lines.push(terms.slice(0, 20).join(", "));
23375
+ }
23376
+ }
23377
+ lines.push("\n### Optimization Suggestions\n");
23378
+ const totalCalls = Object.values(snap.calls).reduce((a, b) => a + b, 0);
23379
+ if (totalCalls === 0) {
23380
+ lines.push("- Start by using `batch_execute` to run multiple commands at once");
23381
+ } else {
23382
+ const searchCalls = snap.calls.search ?? 0;
23383
+ const executeCalls = snap.calls.execute ?? 0;
23384
+ const batchCalls = snap.calls.batch_execute ?? 0;
23385
+ if (executeCalls > 3 && batchCalls === 0) {
23386
+ lines.push(
23387
+ "- **Use batch_execute** \u2014 you've made multiple execute calls that could be batched into one"
23388
+ );
23389
+ }
23390
+ if (searchCalls > 5) {
23391
+ lines.push("- **Batch your searches** \u2014 pass multiple queries in a single search() call");
23392
+ }
23393
+ if (storeStats.totalChunks > 50) {
23394
+ lines.push(
23395
+ "- **Use source filtering** \u2014 scope searches with `source` parameter for faster, targeted results"
23396
+ );
23397
+ }
23398
+ if (storeStats.totalSources === 0 && totalCalls > 2) {
23399
+ lines.push(
23400
+ "- **Index more content** \u2014 use `intent` parameter in execute calls to auto-index large output"
23401
+ );
23402
+ }
23403
+ }
23404
+ if (dbFallback) {
23405
+ lines.push(
23406
+ "\n\u26A0 **Warning:** Persistent DB creation failed \u2014 using in-memory storage. Indexed data will not survive restarts."
23407
+ );
23408
+ }
23409
+ const output = lines.join("\n");
23410
+ tracker.trackCall("discover", Buffer.byteLength(output));
23411
+ return { content: [{ type: "text", text: output }] };
23412
+ }
23413
+ );
23414
+ }
23043
23415
 
23044
- PREFER 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.`,
23416
+ // src/tools/execute-file.ts
23417
+ import { resolve as resolve2 } from "node:path";
23418
+
23419
+ // src/types.ts
23420
+ var ALL_LANGUAGES = [
23421
+ "javascript",
23422
+ "typescript",
23423
+ "python",
23424
+ "shell",
23425
+ "ruby",
23426
+ "go",
23427
+ "rust",
23428
+ "php",
23429
+ "perl",
23430
+ "r",
23431
+ "elixir"
23432
+ ];
23433
+
23434
+ // src/util/path.ts
23435
+ import { realpathSync } from "node:fs";
23436
+ import { resolve } from "node:path";
23437
+ function isWithinProject(absPath, projectDir2) {
23438
+ try {
23439
+ const normalized = realpathSync(resolve(absPath));
23440
+ const realProjectDir = realpathSync(projectDir2);
23441
+ return normalized === realProjectDir || normalized.startsWith(`${realProjectDir}/`);
23442
+ } catch {
23443
+ const normalized = resolve(absPath);
23444
+ const normalizedProject = resolve(projectDir2);
23445
+ return normalized === normalizedProject || normalized.startsWith(`${normalizedProject}/`);
23446
+ }
23447
+ }
23448
+
23449
+ // src/tools/execute-file.ts
23450
+ var LANGUAGE_ENUM = ALL_LANGUAGES;
23451
+ function registerExecuteFileTool(server2, ctx) {
23452
+ const { executor, tracker, projectDir: projectDir2, withExecutionLimit, applyIntentFilter } = ctx;
23453
+ server2.tool(
23454
+ "execute_file",
23455
+ "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.",
23045
23456
  {
23457
+ path: external_exports.string().describe("Absolute file path or relative to project root"),
23046
23458
  language: external_exports.enum(LANGUAGE_ENUM).describe("Runtime language"),
23047
23459
  code: external_exports.string().describe(
23048
- "Source code to execute. Use console.log (JS/TS), print (Python/Ruby/Perl/R), echo (Shell), echo (PHP), fmt.Println (Go), or IO.puts (Elixir) to output a summary to context."
23049
- ),
23050
- intent: external_exports.string().optional().describe(
23051
- "What you're looking for in the output. When provided and output is large (>5KB), indexes output into knowledge base and returns section titles + previews \u2014 not full content. Use search(queries: [...]) to retrieve specific sections."
23460
+ "Code to process FILE_CONTENT. Print summary via console.log/print/echo/IO.puts."
23052
23461
  ),
23462
+ intent: external_exports.string().optional().describe("What you're looking for in the output."),
23053
23463
  timeout: external_exports.number().default(3e4).describe("Max execution time in ms")
23054
23464
  },
23055
- async ({ language, code, intent, timeout }) => {
23465
+ async ({ path: filePath, language, code, intent, timeout }) => {
23056
23466
  const codeBytes = Buffer.byteLength(code);
23057
23467
  if (codeBytes > 1024e3) {
23058
23468
  return {
@@ -23061,18 +23471,35 @@ PREFER THIS OVER BASH for: API calls (gh, curl, aws), test runners (npm test, py
23061
23471
  type: "text",
23062
23472
  text: `Error: code too large (${(codeBytes / 1024).toFixed(0)}KB). Max 1MB.`
23063
23473
  }
23064
- ]
23474
+ ],
23475
+ isError: true
23476
+ };
23477
+ }
23478
+ const absPath = resolve2(projectDir2, filePath);
23479
+ if (!isWithinProject(absPath, projectDir2)) {
23480
+ return {
23481
+ content: [
23482
+ {
23483
+ type: "text",
23484
+ text: `Error: path "${filePath}" is outside the project directory`
23485
+ }
23486
+ ],
23487
+ isError: true
23065
23488
  };
23066
23489
  }
23067
23490
  let result;
23068
23491
  try {
23069
- result = await withExecutionLimit(() => executor.execute({ language, code, timeout }));
23492
+ result = await withExecutionLimit(
23493
+ () => executor.executeFile({
23494
+ language,
23495
+ code,
23496
+ filePath: absPath,
23497
+ timeout
23498
+ })
23499
+ );
23070
23500
  } catch (e) {
23071
23501
  const msg = e instanceof Error ? e.message : String(e);
23072
- return { content: [{ type: "text", text: msg }] };
23073
- }
23074
- if (result.networkBytes) {
23075
- tracker.trackSandboxed(result.networkBytes);
23502
+ return { content: [{ type: "text", text: msg }], isError: true };
23076
23503
  }
23077
23504
  let output = result.stdout;
23078
23505
  if (result.stderr && result.exitCode !== 0) {
@@ -23082,26 +23509,35 @@ STDERR:
23082
23509
  ${result.stderr}`;
23083
23510
  }
23084
23511
  if (intent) {
23085
- output = applyIntentFilter(output, intent, `execute:${language}`);
23512
+ output = applyIntentFilter(output, intent, `file:${filePath}`);
23086
23513
  }
23087
23514
  const responseBytes = Buffer.byteLength(output);
23088
- tracker.trackCall("execute", responseBytes);
23515
+ tracker.trackCall("execute_file", responseBytes);
23089
23516
  return { content: [{ type: "text", text: output }] };
23090
23517
  }
23091
23518
  );
23519
+ }
23520
+
23521
+ // src/tools/execute.ts
23522
+ var LANGUAGE_ENUM2 = ALL_LANGUAGES;
23523
+ function registerExecuteTool(server2, ctx) {
23524
+ const { executor, tracker, withExecutionLimit, applyIntentFilter, bunDetected } = ctx;
23092
23525
  server2.tool(
23093
- "execute_file",
23094
- "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.",
23526
+ "execute",
23527
+ `Execute code in a sandboxed subprocess. Only stdout enters context \u2014 raw data stays in the subprocess. Use instead of bash/cat when output would exceed ~5KB. ${bunDetected ? "(Bun detected \u2014 JS/TS runs 3-5x faster) " : ""}Available: ${ALL_LANGUAGES.join(", ")}.
23528
+
23529
+ PREFER 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.`,
23095
23530
  {
23096
- path: external_exports.string().describe("Absolute file path or relative to project root"),
23097
- language: external_exports.enum(LANGUAGE_ENUM).describe("Runtime language"),
23531
+ language: external_exports.enum(LANGUAGE_ENUM2).describe("Runtime language"),
23098
23532
  code: external_exports.string().describe(
23099
- "Code to process FILE_CONTENT. Print summary via console.log/print/echo/IO.puts."
23533
+ "Source code to execute. Use console.log (JS/TS), print (Python/Ruby/Perl/R), echo (Shell), echo (PHP), fmt.Println (Go), or IO.puts (Elixir) to output a summary to context."
23534
+ ),
23535
+ intent: external_exports.string().optional().describe(
23536
+ "What you're looking for in the output. When provided and output is large (>5KB), indexes output into knowledge base and returns section titles + previews \u2014 not full content. Use search(queries: [...]) to retrieve specific sections."
23100
23537
  ),
23101
- intent: external_exports.string().optional().describe("What you're looking for in the output."),
23102
23538
  timeout: external_exports.number().default(3e4).describe("Max execution time in ms")
23103
23539
  },
23104
- async ({ path: filePath, language, code, intent, timeout }) => {
23540
+ async ({ language, code, intent, timeout }) => {
23105
23541
  const codeBytes = Buffer.byteLength(code);
23106
23542
  if (codeBytes > 1024e3) {
23107
23543
  return {
@@ -23110,33 +23546,19 @@ ${result.stderr}`;
23110
23546
  type: "text",
23111
23547
  text: `Error: code too large (${(codeBytes / 1024).toFixed(0)}KB). Max 1MB.`
23112
23548
  }
23113
- ]
23114
- };
23115
- }
23116
- const absPath = resolve(projectDir, filePath);
23117
- if (!isWithinProject(absPath)) {
23118
- return {
23119
- content: [
23120
- {
23121
- type: "text",
23122
- text: `Error: path "${filePath}" is outside the project directory`
23123
- }
23124
- ]
23549
+ ],
23550
+ isError: true
23125
23551
  };
23126
23552
  }
23127
23553
  let result;
23128
23554
  try {
23129
- result = await withExecutionLimit(
23130
- () => executor.executeFile({
23131
- language,
23132
- code,
23133
- filePath: absPath,
23134
- timeout
23135
- })
23136
- );
23555
+ result = await withExecutionLimit(() => executor.execute({ language, code, timeout }));
23137
23556
  } catch (e) {
23138
23557
  const msg = e instanceof Error ? e.message : String(e);
23139
- return { content: [{ type: "text", text: msg }] };
23558
+ return { content: [{ type: "text", text: msg }], isError: true };
23559
+ }
23560
+ if (result.networkBytes) {
23561
+ tracker.trackSandboxed(result.networkBytes);
23140
23562
  }
23141
23563
  let output = result.stdout;
23142
23564
  if (result.stderr && result.exitCode !== 0) {
@@ -23146,164 +23568,191 @@ STDERR:
23146
23568
  ${result.stderr}`;
23147
23569
  }
23148
23570
  if (intent) {
23149
- output = applyIntentFilter(output, intent, `file:${filePath}`);
23571
+ output = applyIntentFilter(output, intent, `execute:${language}`);
23150
23572
  }
23151
23573
  const responseBytes = Buffer.byteLength(output);
23152
- tracker.trackCall("execute_file", responseBytes);
23574
+ tracker.trackCall("execute", responseBytes);
23153
23575
  return { content: [{ type: "text", text: output }] };
23154
23576
  }
23155
23577
  );
23578
+ }
23579
+
23580
+ // src/network.ts
23581
+ import dns from "node:dns";
23582
+ function isPrivateHost(hostname2) {
23583
+ const h = hostname2.startsWith("[") && hostname2.endsWith("]") ? hostname2.slice(1, -1) : hostname2;
23584
+ const lower = h.toLowerCase();
23585
+ if (lower === "localhost" || lower === "0.0.0.0") return true;
23586
+ if (/^0\./.test(h)) return true;
23587
+ if (/^127\./.test(h)) return true;
23588
+ if (/^10\./.test(h)) return true;
23589
+ if (/^172\.(1[6-9]|2\d|3[01])\./.test(h)) return true;
23590
+ if (/^192\.168\./.test(h)) return true;
23591
+ if (/^169\.254\./.test(h)) return true;
23592
+ if (/^100\.(6[4-9]|[7-9]\d|1[01]\d|12[0-7])\./.test(h)) return true;
23593
+ if (lower === "::1") return true;
23594
+ if (lower === "::" || lower === "0:0:0:0:0:0:0:0") return true;
23595
+ const mappedMatch = lower.match(/^::ffff:(\d+\.\d+\.\d+\.\d+)$/);
23596
+ if (mappedMatch) return isPrivateHost(mappedMatch[1]);
23597
+ const hexMappedMatch = lower.match(/^::ffff:([0-9a-f]{1,4}):([0-9a-f]{1,4})$/);
23598
+ if (hexMappedMatch) {
23599
+ const g1 = hexMappedMatch[1].padStart(4, "0");
23600
+ const g2 = hexMappedMatch[2].padStart(4, "0");
23601
+ const b1 = Number.parseInt(g1.slice(0, 2), 16);
23602
+ const b2 = Number.parseInt(g1.slice(2, 4), 16);
23603
+ const b3 = Number.parseInt(g2.slice(0, 2), 16);
23604
+ const b4 = Number.parseInt(g2.slice(2, 4), 16);
23605
+ return isPrivateHost(`${b1}.${b2}.${b3}.${b4}`);
23606
+ }
23607
+ if (/^fe[89ab]/i.test(h)) return true;
23608
+ if (/^f[cd]/i.test(h)) return true;
23609
+ return false;
23610
+ }
23611
+ async function resolveAndValidate(url) {
23612
+ const parsed = new URL(url);
23613
+ const hostname2 = parsed.hostname;
23614
+ if (/^\d+\.\d+\.\d+\.\d+$/.test(hostname2) || hostname2.includes(":")) {
23615
+ if (isPrivateHost(hostname2)) {
23616
+ throw new Error(`Blocked: resolved IP ${hostname2} is a private/internal address`);
23617
+ }
23618
+ return { url, resolvedIp: null };
23619
+ }
23620
+ let resolvedIp = null;
23621
+ let v4Error = false;
23622
+ let v6Error = false;
23623
+ const [v4Result, v6Result] = await Promise.allSettled([
23624
+ dns.promises.lookup(hostname2, { family: 4 }),
23625
+ dns.promises.lookup(hostname2, { family: 6 })
23626
+ ]);
23627
+ if (v4Result.status === "fulfilled") {
23628
+ if (isPrivateHost(v4Result.value.address)) {
23629
+ throw new Error(`Blocked: ${hostname2} resolved to private IP ${v4Result.value.address}`);
23630
+ }
23631
+ resolvedIp = v4Result.value.address;
23632
+ } else {
23633
+ v4Error = true;
23634
+ }
23635
+ if (v6Result.status === "fulfilled") {
23636
+ if (isPrivateHost(v6Result.value.address)) {
23637
+ throw new Error(`Blocked: ${hostname2} resolved to private IPv6 ${v6Result.value.address}`);
23638
+ }
23639
+ if (!resolvedIp) resolvedIp = v6Result.value.address;
23640
+ } else {
23641
+ v6Error = true;
23642
+ }
23643
+ if (v4Error && v6Error) {
23644
+ throw new Error(`DNS resolution failed for ${hostname2}: unable to verify host safety`);
23645
+ }
23646
+ return { url, resolvedIp };
23647
+ }
23648
+
23649
+ // src/util/fetch-code.ts
23650
+ function buildFetchCode(url, resolvedIp) {
23651
+ let fetchSetup;
23652
+ if (resolvedIp) {
23653
+ const pinnedUrl = new URL(url);
23654
+ const originalHost = pinnedUrl.host;
23655
+ const hostnameValue = resolvedIp.includes(":") && !resolvedIp.startsWith("[") ? `[${resolvedIp}]` : resolvedIp;
23656
+ pinnedUrl.hostname = hostnameValue;
23657
+ fetchSetup = `
23658
+ const url = ${JSON.stringify(pinnedUrl.toString())};
23659
+ const resp = await fetch(url, { headers: { 'Host': ${JSON.stringify(originalHost)} }, redirect: 'error' });`;
23660
+ } else {
23661
+ fetchSetup = `
23662
+ const url = ${JSON.stringify(url)};
23663
+ const resp = await fetch(url, { redirect: 'error' });`;
23664
+ }
23665
+ return `${fetchSetup}
23666
+ if (!resp.ok) { console.error("HTTP " + resp.status); process.exit(1); }
23667
+ const cl = resp.headers.get('content-length');
23668
+ if (cl && parseInt(cl, 10) > 10 * 1024 * 1024) {
23669
+ console.error("Response too large: " + cl + " bytes"); process.exit(1);
23670
+ }
23671
+ const html = await resp.text();
23672
+ if (html.length > 10 * 1024 * 1024) {
23673
+ console.error("Response body too large: " + html.length + " chars"); process.exit(1);
23674
+ }
23675
+
23676
+ // Strip unwanted tags
23677
+ let md = html
23678
+ .replace(/<script[^>]*>[\\s\\S]*?<\\/script>/gi, "")
23679
+ .replace(/<style[^>]*>[\\s\\S]*?<\\/style>/gi, "")
23680
+ .replace(/<nav[^>]*>[\\s\\S]*?<\\/nav>/gi, "")
23681
+ .replace(/<header[^>]*>[\\s\\S]*?<\\/header>/gi, "")
23682
+ .replace(/<footer[^>]*>[\\s\\S]*?<\\/footer>/gi, "");
23683
+
23684
+ // Convert headings
23685
+ md = md.replace(/<h1[^>]*>(.*?)<\\/h1>/gi, "# $1\\n");
23686
+ md = md.replace(/<h2[^>]*>(.*?)<\\/h2>/gi, "## $1\\n");
23687
+ md = md.replace(/<h3[^>]*>(.*?)<\\/h3>/gi, "### $1\\n");
23688
+ md = md.replace(/<h4[^>]*>(.*?)<\\/h4>/gi, "#### $1\\n");
23689
+
23690
+ // Convert code blocks
23691
+ md = md.replace(/<pre[^>]*><code[^>]*>(.*?)<\\/code><\\/pre>/gis, "\`\`\`\\n$1\\n\`\`\`\\n");
23692
+ md = md.replace(/<code[^>]*>(.*?)<\\/code>/gi, "\`$1\`");
23693
+
23694
+ // Convert links
23695
+ md = md.replace(/<a[^>]*href="([^"]*)"[^>]*>(.*?)<\\/a>/gi, "[$2]($1)");
23696
+
23697
+ // Convert lists
23698
+ md = md.replace(/<li[^>]*>(.*?)<\\/li>/gi, "- $1\\n");
23699
+
23700
+ // Convert paragraphs
23701
+ md = md.replace(/<p[^>]*>(.*?)<\\/p>/gis, "$1\\n\\n");
23702
+ md = md.replace(/<br\\s*\\/?>/gi, "\\n");
23703
+
23704
+ // Strip remaining tags
23705
+ md = md.replace(/<[^>]+>/g, "");
23706
+
23707
+ // Decode entities
23708
+ md = md.replace(/&lt;/g, "<")
23709
+ .replace(/&gt;/g, ">")
23710
+ .replace(/&quot;/g, '"')
23711
+ .replace(/&#39;/g, "'")
23712
+ .replace(/&apos;/g, "'")
23713
+ .replace(/&nbsp;/g, " ")
23714
+ .replace(/&#(\\d+);/g, (_, n) => { const c = parseInt(n, 10); return c > 0 && c <= 0x10FFFF ? String.fromCodePoint(c) : ''; })
23715
+ .replace(/&#x([0-9a-fA-F]+);/g, (_, h) => { const c = parseInt(h, 16); return c > 0 && c <= 0x10FFFF ? String.fromCodePoint(c) : ''; })
23716
+ .replace(/&amp;/g, "&");
23717
+
23718
+ // Clean whitespace
23719
+ md = md.replace(/\\n{3,}/g, "\\n\\n").trim();
23720
+
23721
+ console.log(md);
23722
+ `;
23723
+ }
23724
+
23725
+ // src/tools/fetch-and-index.ts
23726
+ function registerFetchAndIndexTool(server2, ctx) {
23727
+ const { executor, store, tracker, withExecutionLimit } = ctx;
23156
23728
  server2.tool(
23157
- "index",
23158
- "Index documentation or knowledge content into a searchable BM25 knowledge base. Chunks markdown by headings (keeping code blocks intact) and stores in ephemeral FTS5 database. The full content does NOT stay in context \u2014 only a brief summary is returned.\n\nWHEN TO USE:\n- Documentation (API docs, framework guides, code examples)\n- README files, migration guides, changelog entries\n- Any content with code examples you may need to reference precisely\n\nAfter indexing, use 'search' to retrieve specific sections on-demand.",
23729
+ "fetch_and_index",
23730
+ "Fetches URL content, converts HTML to markdown, indexes into searchable knowledge base, and returns a ~3KB preview. Full content stays in sandbox \u2014 use search() for deeper lookups.\n\nBetter than WebFetch: preview is immediate, full content is searchable, raw HTML never enters context.",
23159
23731
  {
23160
- content: external_exports.string().optional().describe("Raw text/markdown to index. Provide this OR path, not both."),
23161
- path: external_exports.string().optional().describe("File path to read and index (content never enters context)."),
23732
+ url: external_exports.string().describe("The URL to fetch and index"),
23162
23733
  source: external_exports.string().optional().describe("Label for the indexed content")
23163
23734
  },
23164
- async ({ content, path: filePath, source }) => {
23165
- let text;
23166
- let label = source ?? "indexed content";
23167
- if (filePath) {
23168
- const absPath = resolve(projectDir, filePath);
23169
- if (!isWithinProject(absPath)) {
23170
- return {
23171
- content: [
23172
- {
23173
- type: "text",
23174
- text: `Error: path "${filePath}" is outside the project directory`
23175
- }
23176
- ]
23177
- };
23178
- }
23179
- try {
23180
- const fileStat = statSync(absPath);
23181
- if (fileStat.size > 50 * 1024 * 1024) {
23182
- return {
23183
- content: [
23184
- {
23185
- type: "text",
23186
- text: `Error: file "${filePath}" is too large (${(fileStat.size / 1024 / 1024).toFixed(1)}MB). Max 50MB.`
23187
- }
23188
- ]
23189
- };
23190
- }
23191
- text = readFileSync3(absPath, "utf-8");
23192
- label = source ?? filePath;
23193
- } catch (e) {
23194
- const msg = e instanceof Error ? e.message : String(e);
23735
+ async ({ url, source }) => {
23736
+ try {
23737
+ const parsed = new URL(url);
23738
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
23195
23739
  return {
23196
- content: [{ type: "text", text: `Error reading "${filePath}": ${msg}` }]
23740
+ content: [{ type: "text", text: "Error: only http/https URLs are allowed" }],
23741
+ isError: true
23197
23742
  };
23198
23743
  }
23199
- } else if (content) {
23200
- const contentBytes = Buffer.byteLength(content);
23201
- if (contentBytes > 50 * 1024 * 1024) {
23744
+ if (isPrivateHost(parsed.hostname)) {
23202
23745
  return {
23203
23746
  content: [
23204
- {
23205
- type: "text",
23206
- text: `Error: content too large (${(contentBytes / 1024 / 1024).toFixed(1)}MB). Max 50MB.`
23207
- }
23208
- ]
23747
+ { type: "text", text: "Error: internal/private URLs are not allowed" }
23748
+ ],
23749
+ isError: true
23209
23750
  };
23210
23751
  }
23211
- text = content;
23212
- } else {
23752
+ } catch {
23213
23753
  return {
23214
- content: [{ type: "text", text: "Error: provide either 'content' or 'path'" }]
23215
- };
23216
- }
23217
- const result = store.index(text, label);
23218
- tracker.trackIndexed(Buffer.byteLength(text));
23219
- const summary = `Indexed "${label}": ${result.totalChunks} chunks (${result.codeChunks} with code). Use search(queries: [...]) to retrieve sections.`;
23220
- tracker.trackCall("index", Buffer.byteLength(summary));
23221
- return { content: [{ type: "text", text: summary }] };
23222
- }
23223
- );
23224
- server2.tool(
23225
- "search",
23226
- "Search indexed content. Pass ALL search questions as queries array in ONE call.\n\nTIPS: 2-4 specific terms per query. Use 'source' to scope results.",
23227
- {
23228
- queries: external_exports.array(external_exports.string()).describe("Array of search queries. Batch ALL questions in one call."),
23229
- source: external_exports.string().optional().describe("Filter to a specific indexed source (partial match)."),
23230
- limit: external_exports.number().default(3).describe("Results per query (default: 3)")
23231
- },
23232
- async ({ queries, source, limit }) => {
23233
- const now = Date.now();
23234
- searchCalls.push(now);
23235
- while (searchCalls.length > 0 && searchCalls[0] < now - config3.searchWindowMs) {
23236
- searchCalls.shift();
23237
- }
23238
- const callCount = searchCalls.length;
23239
- if (callCount > config3.searchBlockAfter) {
23240
- const msg = "Too many search calls in quick succession. Use batch_execute instead to run commands and search in one call.";
23241
- tracker.trackCall("search", Buffer.byteLength(msg));
23242
- return { content: [{ type: "text", text: msg }] };
23243
- }
23244
- const effectiveLimit = callCount > config3.searchReduceAfter ? 1 : Math.min(limit, config3.searchLimit);
23245
- const allResults = [];
23246
- let totalBytes = 0;
23247
- for (const query of queries) {
23248
- if (totalBytes > config3.searchMaxBytes) break;
23249
- const result = store.search(query, { source, limit: effectiveLimit });
23250
- let block = `## ${query}
23251
- `;
23252
- if (result.corrected) {
23253
- block += `(corrected to: "${result.corrected}")
23254
- `;
23255
- }
23256
- if (result.results.length === 0) {
23257
- block += "No results found.\n";
23258
- } else {
23259
- for (const hit of result.results) {
23260
- block += `
23261
- --- [${hit.source}] ---
23262
- ### ${hit.title}
23263
-
23264
- ${hit.snippet}
23265
- `;
23266
- }
23267
- }
23268
- allResults.push(block);
23269
- totalBytes += Buffer.byteLength(block);
23270
- }
23271
- if (callCount > config3.searchReduceAfter) {
23272
- allResults.push(
23273
- `
23274
- \u26A0 Search rate limited (${callCount} calls in ${config3.searchWindowMs / 1e3}s). Results reduced to 1 per query.`
23275
- );
23276
- }
23277
- const output = allResults.join("\n---\n\n");
23278
- tracker.trackCall("search", Buffer.byteLength(output));
23279
- return { content: [{ type: "text", text: output }] };
23280
- }
23281
- );
23282
- server2.tool(
23283
- "fetch_and_index",
23284
- "Fetches URL content, converts HTML to markdown, indexes into searchable knowledge base, and returns a ~3KB preview. Full content stays in sandbox \u2014 use search() for deeper lookups.\n\nBetter than WebFetch: preview is immediate, full content is searchable, raw HTML never enters context.",
23285
- {
23286
- url: external_exports.string().describe("The URL to fetch and index"),
23287
- source: external_exports.string().optional().describe("Label for the indexed content")
23288
- },
23289
- async ({ url, source }) => {
23290
- try {
23291
- const parsed = new URL(url);
23292
- if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
23293
- return {
23294
- content: [{ type: "text", text: "Error: only http/https URLs are allowed" }]
23295
- };
23296
- }
23297
- if (isPrivateHost(parsed.hostname)) {
23298
- return {
23299
- content: [
23300
- { type: "text", text: "Error: internal/private URLs are not allowed" }
23301
- ]
23302
- };
23303
- }
23304
- } catch {
23305
- return {
23306
- content: [{ type: "text", text: `Error: invalid URL "${url}"` }]
23754
+ content: [{ type: "text", text: `Error: invalid URL "${url}"` }],
23755
+ isError: true
23307
23756
  };
23308
23757
  }
23309
23758
  let resolvedIp = null;
@@ -23317,7 +23766,8 @@ ${hit.snippet}
23317
23766
  type: "text",
23318
23767
  text: `Error: ${err instanceof Error ? err.message : "DNS validation failed"}`
23319
23768
  }
23320
- ]
23769
+ ],
23770
+ isError: true
23321
23771
  };
23322
23772
  }
23323
23773
  const label = source ?? url;
@@ -23333,12 +23783,12 @@ ${hit.snippet}
23333
23783
  );
23334
23784
  } catch (e) {
23335
23785
  const msg = e instanceof Error ? e.message : String(e);
23336
- return { content: [{ type: "text", text: msg }] };
23786
+ return { content: [{ type: "text", text: msg }], isError: true };
23337
23787
  }
23338
23788
  if (result.exitCode !== 0 || !result.stdout.trim()) {
23339
23789
  const errMsg = `Failed to fetch ${url}: ${result.stderr || "empty response"}`;
23340
23790
  tracker.trackCall("fetch_and_index", Buffer.byteLength(errMsg));
23341
- return { content: [{ type: "text", text: errMsg }] };
23791
+ return { content: [{ type: "text", text: errMsg }], isError: true };
23342
23792
  }
23343
23793
  const markdown = result.stdout;
23344
23794
  tracker.trackSandboxed(result.networkBytes ?? 0);
@@ -23368,102 +23818,155 @@ Searchable terms: ${terms.join(", ")}`;
23368
23818
  return { content: [{ type: "text", text: output }] };
23369
23819
  }
23370
23820
  );
23821
+ }
23822
+
23823
+ // src/tools/index-content.ts
23824
+ import { readFileSync as readFileSync3, statSync } from "node:fs";
23825
+ import { resolve as resolve3 } from "node:path";
23826
+ function registerIndexTool(server2, ctx) {
23827
+ const { store, tracker, projectDir: projectDir2 } = ctx;
23371
23828
  server2.tool(
23372
- "batch_execute",
23373
- "Execute multiple commands in ONE call, auto-index all output, and search with multiple queries. Returns search results directly \u2014 no follow-up calls needed.\n\nTHIS IS THE PRIMARY TOOL. Use this instead of multiple execute() calls.\n\nOne batch_execute call replaces 30+ execute calls + 10+ search calls.\nProvide all commands to run and all queries to search \u2014 everything happens in one round trip.",
23829
+ "index",
23830
+ "Index documentation or knowledge content into a searchable BM25 knowledge base. Chunks markdown by headings (keeping code blocks intact) and stores in ephemeral FTS5 database. The full content does NOT stay in context \u2014 only a brief summary is returned.\n\nWHEN TO USE:\n- Documentation (API docs, framework guides, code examples)\n- README files, migration guides, changelog entries\n- Any content with code examples you may need to reference precisely\n\nAfter indexing, use 'search' to retrieve specific sections on-demand.",
23374
23831
  {
23375
- commands: external_exports.array(
23376
- external_exports.object({
23377
- label: external_exports.string().describe("Section header for this command's output"),
23378
- command: external_exports.string().describe("Shell command to execute")
23379
- })
23380
- ).describe("Commands to execute as a batch."),
23381
- queries: external_exports.array(external_exports.string()).describe(
23382
- "Search queries to extract information from indexed output. Use 5-8 comprehensive queries."
23383
- ),
23384
- timeout: external_exports.number().default(6e4).describe("Max execution time in ms (default: 60s)")
23832
+ content: external_exports.string().optional().describe("Raw text/markdown to index. Provide this OR path, not both."),
23833
+ path: external_exports.string().optional().describe("File path to read and index (content never enters context)."),
23834
+ source: external_exports.string().optional().describe("Label for the indexed content")
23385
23835
  },
23386
- async ({ commands, queries, timeout }) => {
23387
- const commandResults = await limitConcurrency(
23388
- commands.map((cmd) => async () => {
23389
- const result = await withExecutionLimit(
23390
- () => executor.execute({
23391
- language: "shell",
23392
- code: cmd.command,
23393
- timeout
23394
- })
23395
- );
23396
- return { label: cmd.label, result };
23397
- }),
23398
- 4
23399
- );
23400
- let combined = "";
23401
- const inventory = [];
23402
- for (let i = 0; i < commandResults.length; i++) {
23403
- const settled = commandResults[i];
23404
- const label = commands[i].label;
23405
- if (settled.status === "fulfilled") {
23406
- const { result } = settled.value;
23407
- const output2 = result.stdout || "(no output)";
23408
- combined += `## ${label}
23409
-
23410
- ${output2}
23411
-
23412
- `;
23413
- const lineCount = output2.split("\n").length;
23414
- inventory.push(`- **${label}**: ${lineCount} lines`);
23415
- } else {
23416
- combined += `## ${label}
23417
-
23418
- (error: ${settled.reason})
23419
-
23420
- `;
23421
- inventory.push(`- **${label}**: error`);
23836
+ async ({ content, path: filePath, source }) => {
23837
+ let text;
23838
+ let label = source ?? "indexed content";
23839
+ if (filePath) {
23840
+ const absPath = resolve3(projectDir2, filePath);
23841
+ if (!isWithinProject(absPath, projectDir2)) {
23842
+ return {
23843
+ content: [
23844
+ {
23845
+ type: "text",
23846
+ text: `Error: path "${filePath}" is outside the project directory`
23847
+ }
23848
+ ],
23849
+ isError: true
23850
+ };
23851
+ }
23852
+ try {
23853
+ const fileStat = statSync(absPath);
23854
+ if (fileStat.size > 50 * 1024 * 1024) {
23855
+ return {
23856
+ content: [
23857
+ {
23858
+ type: "text",
23859
+ text: `Error: file "${filePath}" is too large (${(fileStat.size / 1024 / 1024).toFixed(1)}MB). Max 50MB.`
23860
+ }
23861
+ ],
23862
+ isError: true
23863
+ };
23864
+ }
23865
+ text = readFileSync3(absPath, "utf-8");
23866
+ label = source ?? filePath;
23867
+ } catch (e) {
23868
+ const msg = e instanceof Error ? e.message : String(e);
23869
+ return {
23870
+ content: [{ type: "text", text: `Error reading "${filePath}": ${msg}` }],
23871
+ isError: true
23872
+ };
23873
+ }
23874
+ } else if (content) {
23875
+ const contentBytes = Buffer.byteLength(content);
23876
+ if (contentBytes > 50 * 1024 * 1024) {
23877
+ return {
23878
+ content: [
23879
+ {
23880
+ type: "text",
23881
+ text: `Error: content too large (${(contentBytes / 1024 / 1024).toFixed(1)}MB). Max 50MB.`
23882
+ }
23883
+ ],
23884
+ isError: true
23885
+ };
23422
23886
  }
23887
+ text = content;
23888
+ } else {
23889
+ return {
23890
+ content: [{ type: "text", text: "Error: provide either 'content' or 'path'" }],
23891
+ isError: true
23892
+ };
23423
23893
  }
23424
- const indexed = store.index(combined, "batch_execute");
23425
- tracker.trackIndexed(Buffer.byteLength(combined));
23426
- const searchResults = [];
23894
+ const result = store.index(text, label);
23895
+ tracker.trackIndexed(Buffer.byteLength(text));
23896
+ const summary = `Indexed "${label}": ${result.totalChunks} chunks (${result.codeChunks} with code). Use search(queries: [...]) to retrieve sections.`;
23897
+ tracker.trackCall("index", Buffer.byteLength(summary));
23898
+ return { content: [{ type: "text", text: summary }] };
23899
+ }
23900
+ );
23901
+ }
23902
+
23903
+ // src/tools/search.ts
23904
+ function registerSearchTool(server2, ctx) {
23905
+ const { store, tracker, config: config3 } = ctx;
23906
+ const searchCalls = [];
23907
+ server2.tool(
23908
+ "search",
23909
+ "Search indexed content. Pass ALL search questions as queries array in ONE call.\n\nTIPS: 2-4 specific terms per query. Use 'source' to scope results.",
23910
+ {
23911
+ queries: external_exports.array(external_exports.string()).describe("Array of search queries. Batch ALL questions in one call."),
23912
+ source: external_exports.string().optional().describe("Filter to a specific indexed source (partial match)."),
23913
+ limit: external_exports.number().default(3).describe("Results per query (default: 3)")
23914
+ },
23915
+ async ({ queries, source, limit }) => {
23916
+ const now = Date.now();
23917
+ searchCalls.push(now);
23918
+ while (searchCalls.length > 0 && searchCalls[0] < now - config3.searchWindowMs) {
23919
+ searchCalls.shift();
23920
+ }
23921
+ const callCount = searchCalls.length;
23922
+ if (callCount > config3.searchBlockAfter) {
23923
+ const msg = "Too many search calls in quick succession. Use batch_execute instead to run commands and search in one call.";
23924
+ tracker.trackCall("search", Buffer.byteLength(msg));
23925
+ return { content: [{ type: "text", text: msg }] };
23926
+ }
23927
+ const effectiveLimit = callCount > config3.searchReduceAfter ? 1 : Math.min(limit, config3.searchLimit);
23928
+ const allResults = [];
23427
23929
  let totalBytes = 0;
23428
23930
  for (const query of queries) {
23429
- if (totalBytes > config3.batchMaxBytes) break;
23430
- let result = store.search(query, { source: "batch_execute", limit: 5 });
23431
- if (result.results.length === 0) {
23432
- result = store.search(query, { limit: 5 });
23433
- }
23931
+ if (totalBytes > config3.searchMaxBytes) break;
23932
+ const result = store.search(query, { source, limit: effectiveLimit });
23434
23933
  let block = `## ${query}
23435
-
23436
23934
  `;
23935
+ if (result.corrected) {
23936
+ block += `(corrected to: "${result.corrected}")
23937
+ `;
23938
+ }
23437
23939
  if (result.results.length === 0) {
23438
23940
  block += "No results found.\n";
23439
23941
  } else {
23440
23942
  for (const hit of result.results) {
23441
- block += `--- [${hit.source}] ---
23943
+ block += `
23944
+ --- [${hit.source}] ---
23442
23945
  ### ${hit.title}
23443
23946
 
23444
23947
  ${hit.snippet}
23445
-
23446
23948
  `;
23447
23949
  }
23448
23950
  }
23449
- searchResults.push(block);
23951
+ allResults.push(block);
23450
23952
  totalBytes += Buffer.byteLength(block);
23451
23953
  }
23452
- const terms = store.getDistinctiveTerms(indexed.sourceId);
23453
- let output = `**Inventory** (${commands.length} commands):
23454
- ${inventory.join("\n")}
23455
-
23456
- `;
23457
- output += searchResults.join("\n---\n\n");
23458
- if (terms.length > 0) {
23459
- output += `
23460
-
23461
- Searchable terms: ${terms.join(", ")}`;
23954
+ if (callCount > config3.searchReduceAfter) {
23955
+ allResults.push(
23956
+ `
23957
+ \u26A0 Search rate limited (${callCount} calls in ${config3.searchWindowMs / 1e3}s). Results reduced to 1 per query.`
23958
+ );
23462
23959
  }
23463
- tracker.trackCall("batch_execute", Buffer.byteLength(output));
23960
+ const output = allResults.join("\n---\n\n");
23961
+ tracker.trackCall("search", Buffer.byteLength(output));
23464
23962
  return { content: [{ type: "text", text: output }] };
23465
23963
  }
23466
23964
  );
23965
+ }
23966
+
23967
+ // src/tools/stats.ts
23968
+ function registerStatsTool(server2, ctx) {
23969
+ const { tracker } = ctx;
23467
23970
  server2.tool(
23468
23971
  "stats",
23469
23972
  "Returns context consumption statistics for the current session. Shows total bytes returned to context, breakdown by tool, call counts, estimated token usage, context savings ratio, and visual charts.",
@@ -23475,82 +23978,151 @@ Searchable terms: ${terms.join(", ")}`;
23475
23978
  return { content: [{ type: "text", text: report }] };
23476
23979
  }
23477
23980
  );
23478
- server2.tool(
23479
- "discover",
23480
- "Shows what's in the knowledge base and suggests optimization opportunities. Lists all indexed sources, chunk counts, searchable terms, and recommends next actions. Use this to understand what data is available for search.",
23481
- {},
23482
- async () => {
23483
- const storeStats = store.getStats();
23484
- const snap = tracker.getSnapshot();
23485
- const lines = [];
23486
- lines.push("## Knowledge Base Discovery\n");
23487
- if (storeStats.totalSources === 0) {
23488
- lines.push("No content indexed yet. Use these tools to build the knowledge base:\n");
23489
- lines.push("- `batch_execute` \u2014 run commands and auto-index output");
23490
- lines.push("- `execute` with `intent` \u2014 auto-indexes large output");
23491
- lines.push("- `index` \u2014 index documentation or files");
23492
- lines.push("- `fetch_and_index` \u2014 fetch and index web pages");
23493
- } else {
23494
- lines.push("| Metric | Value |");
23495
- lines.push("|--------|-------|");
23496
- lines.push(`| Indexed sources | ${storeStats.totalSources} |`);
23497
- lines.push(`| Total chunks | ${storeStats.totalChunks} |`);
23498
- lines.push(`| Vocabulary size | ${storeStats.vocabularySize} |`);
23499
- lines.push(
23500
- `| Trigram index | ${storeStats.hasTrigramTable ? "active" : "lazy (not yet needed)"} |`
23501
- );
23502
- const sources = store.listSources();
23503
- if (sources.length > 0) {
23504
- lines.push("\n### Indexed Sources\n");
23505
- for (const src of sources) {
23506
- lines.push(
23507
- `- **${src.label}** \u2014 ${src.chunkCount} chunks${src.codeChunks > 0 ? ` (${src.codeChunks} with code)` : ""}`
23508
- );
23509
- }
23510
- }
23511
- const terms = store.getDistinctiveTerms();
23512
- if (terms.length > 0) {
23513
- lines.push("\n### Top Searchable Terms\n");
23514
- lines.push(terms.slice(0, 20).join(", "));
23515
- }
23516
- }
23517
- lines.push("\n### Optimization Suggestions\n");
23518
- const totalCalls = Object.values(snap.calls).reduce((a, b) => a + b, 0);
23519
- if (totalCalls === 0) {
23520
- lines.push("- Start by using `batch_execute` to run multiple commands at once");
23521
- } else {
23522
- const searchCalls2 = snap.calls.search ?? 0;
23523
- const executeCalls = snap.calls.execute ?? 0;
23524
- const batchCalls = snap.calls.batch_execute ?? 0;
23525
- if (executeCalls > 3 && batchCalls === 0) {
23526
- lines.push(
23527
- "- **Use batch_execute** \u2014 you've made multiple execute calls that could be batched into one"
23528
- );
23529
- }
23530
- if (searchCalls2 > 5) {
23531
- lines.push("- **Batch your searches** \u2014 pass multiple queries in a single search() call");
23532
- }
23533
- if (storeStats.totalChunks > 50) {
23534
- lines.push(
23535
- "- **Use source filtering** \u2014 scope searches with `source` parameter for faster, targeted results"
23536
- );
23537
- }
23538
- if (storeStats.totalSources === 0 && totalCalls > 2) {
23539
- lines.push(
23540
- "- **Index more content** \u2014 use `intent` parameter in execute calls to auto-index large output"
23541
- );
23542
- }
23543
- }
23544
- if (dbFallback) {
23545
- lines.push(
23546
- "\n\u26A0 **Warning:** Persistent DB creation failed \u2014 using in-memory storage. Indexed data will not survive restarts."
23547
- );
23548
- }
23549
- const output = lines.join("\n");
23550
- tracker.trackCall("discover", Buffer.byteLength(output));
23551
- return { content: [{ type: "text", text: output }] };
23981
+ }
23982
+
23983
+ // src/util/label.ts
23984
+ function compactLabel(normal, level) {
23985
+ if (level === "ultra") {
23986
+ return normal.replace(/\*\*/g, "").replace(/Use search\(queries: \[\.\.\.]\) to retrieve.*$/gm, "\u2192 search() for more").replace(/Searchable terms: .+$/gm, "");
23987
+ }
23988
+ if (level === "compact") {
23989
+ return normal.replace(
23990
+ /Use search\(queries: \[\.\.\.]\) to retrieve full content of any section\./,
23991
+ "\u2192 search() for details"
23992
+ );
23993
+ }
23994
+ return normal;
23995
+ }
23996
+
23997
+ // src/util/intent-filter.ts
23998
+ function createIntentFilter(deps) {
23999
+ const { config: config3, store, tracker } = deps;
24000
+ return function applyIntentFilter(output, intent, sourceLabel) {
24001
+ if (Buffer.byteLength(output) <= config3.intentSearchThreshold) return output;
24002
+ const indexed = store.index(output, sourceLabel);
24003
+ tracker.trackIndexed(Buffer.byteLength(output));
24004
+ const searchResults = store.search(intent, { limit: 3 });
24005
+ const terms = store.getDistinctiveTerms(indexed.sourceId);
24006
+ let filtered = `Indexed ${indexed.totalChunks} sections from ${sourceLabel}.
24007
+ `;
24008
+ filtered += `${searchResults.results.length} sections matched "${intent}":
24009
+
24010
+ `;
24011
+ for (const hit of searchResults.results) {
24012
+ filtered += ` - **${hit.title}**: ${hit.snippet.slice(0, 200)}
24013
+ `;
23552
24014
  }
23553
- );
24015
+ if (terms.length > 0 && config3.compressionLevel !== "ultra") {
24016
+ filtered += `
24017
+ Searchable terms: ${terms.join(", ")}
24018
+ `;
24019
+ }
24020
+ filtered += "\nUse search(queries: [...]) to retrieve full content of any section.";
24021
+ return compactLabel(filtered, config3.compressionLevel);
24022
+ };
24023
+ }
24024
+
24025
+ // src/util/version.ts
24026
+ import { readFileSync as readFileSync4 } from "node:fs";
24027
+ import { dirname, resolve as resolve4 } from "node:path";
24028
+ import { fileURLToPath } from "node:url";
24029
+ var __dirname = dirname(fileURLToPath(import.meta.url));
24030
+ function getVersion(fallback = "1.0.0") {
24031
+ try {
24032
+ const pkgPath = resolve4(__dirname, "..", "..", "package.json");
24033
+ const pkg = JSON.parse(readFileSync4(pkgPath, "utf-8"));
24034
+ return pkg.version ?? fallback;
24035
+ } catch {
24036
+ return fallback;
24037
+ }
24038
+ }
24039
+
24040
+ // src/server.ts
24041
+ var projectDir = process.env.CLAUDE_PROJECT_DIR ?? process.cwd();
24042
+ var MAX_CONCURRENT_EXECUTIONS = 8;
24043
+ var EXECUTION_LIMIT_ERROR = "Error: too many concurrent executions. Try again shortly.";
24044
+ async function createServer(config3) {
24045
+ const version2 = getVersion();
24046
+ debug("Version:", version2);
24047
+ cleanupStaleDbs();
24048
+ const runtimes = await detectRuntimes();
24049
+ const bunDetected = hasBun(runtimes);
24050
+ debug("Runtimes detected:", runtimes.size);
24051
+ const executor = new SubprocessExecutor(runtimes, config3);
24052
+ let store;
24053
+ let dbFallback = false;
24054
+ try {
24055
+ store = new ContentStore({ persistDb: config3.persistDb, dbDir: config3.dbDir });
24056
+ } catch (e) {
24057
+ debug("Failed to create DB, falling back to in-memory:", e);
24058
+ store = new ContentStore(":memory:");
24059
+ dbFallback = true;
24060
+ }
24061
+ const cumulativeFile = config3.persistDb ? join4(config3.dbDir ?? join4(projectDir, ".context-compress"), "stats.json") : void 0;
24062
+ const tracker = new SessionTracker(cumulativeFile);
24063
+ let activeExecutions = 0;
24064
+ async function withExecutionLimit(fn) {
24065
+ if (activeExecutions >= MAX_CONCURRENT_EXECUTIONS) {
24066
+ throw new Error(EXECUTION_LIMIT_ERROR);
24067
+ }
24068
+ activeExecutions++;
24069
+ try {
24070
+ return await fn();
24071
+ } finally {
24072
+ activeExecutions--;
24073
+ }
24074
+ }
24075
+ const applyIntentFilter = createIntentFilter({ config: config3, store, tracker });
24076
+ const shutdown = () => {
24077
+ try {
24078
+ tracker.saveCumulative();
24079
+ } catch {
24080
+ }
24081
+ try {
24082
+ executor.shutdown();
24083
+ } catch {
24084
+ }
24085
+ try {
24086
+ store.close();
24087
+ } catch {
24088
+ }
24089
+ };
24090
+ process.on("SIGINT", shutdown);
24091
+ process.on("SIGTERM", shutdown);
24092
+ process.on("beforeExit", shutdown);
24093
+ process.on("uncaughtException", (err) => {
24094
+ debug("Uncaught exception:", err);
24095
+ shutdown();
24096
+ process.exit(1);
24097
+ });
24098
+ process.on("unhandledRejection", (err) => {
24099
+ debug("Unhandled rejection:", err);
24100
+ shutdown();
24101
+ process.exit(1);
24102
+ });
24103
+ const server2 = new McpServer({
24104
+ name: "context-compress",
24105
+ version: version2
24106
+ });
24107
+ const ctx = {
24108
+ config: config3,
24109
+ store,
24110
+ tracker,
24111
+ executor,
24112
+ projectDir,
24113
+ bunDetected,
24114
+ dbFallback,
24115
+ withExecutionLimit,
24116
+ applyIntentFilter
24117
+ };
24118
+ registerExecuteTool(server2, ctx);
24119
+ registerExecuteFileTool(server2, ctx);
24120
+ registerIndexTool(server2, ctx);
24121
+ registerSearchTool(server2, ctx);
24122
+ registerFetchAndIndexTool(server2, ctx);
24123
+ registerBatchExecuteTool(server2, ctx);
24124
+ registerStatsTool(server2, ctx);
24125
+ registerDiscoverTool(server2, ctx);
23554
24126
  return {
23555
24127
  async start() {
23556
24128
  const transport = new StdioServerTransport();
@@ -23559,79 +24131,6 @@ Searchable terms: ${terms.join(", ")}`;
23559
24131
  }
23560
24132
  };
23561
24133
  }
23562
- function buildFetchCode(url, resolvedIp) {
23563
- let fetchSetup;
23564
- if (resolvedIp) {
23565
- const pinnedUrl = new URL(url);
23566
- const originalHost = pinnedUrl.host;
23567
- pinnedUrl.hostname = resolvedIp;
23568
- fetchSetup = `
23569
- const url = ${JSON.stringify(pinnedUrl.toString())};
23570
- const resp = await fetch(url, { headers: { 'Host': ${JSON.stringify(originalHost)} }, redirect: 'error' });`;
23571
- } else {
23572
- fetchSetup = `
23573
- const url = ${JSON.stringify(url)};
23574
- const resp = await fetch(url, { redirect: 'error' });`;
23575
- }
23576
- return `${fetchSetup}
23577
- if (!resp.ok) { console.error("HTTP " + resp.status); process.exit(1); }
23578
- const cl = resp.headers.get('content-length');
23579
- if (cl && parseInt(cl, 10) > 10 * 1024 * 1024) {
23580
- console.error("Response too large: " + cl + " bytes"); process.exit(1);
23581
- }
23582
- const html = await resp.text();
23583
- if (html.length > 10 * 1024 * 1024) {
23584
- console.error("Response body too large: " + html.length + " chars"); process.exit(1);
23585
- }
23586
-
23587
- // Strip unwanted tags
23588
- let md = html
23589
- .replace(/<script[^>]*>[\\s\\S]*?<\\/script>/gi, "")
23590
- .replace(/<style[^>]*>[\\s\\S]*?<\\/style>/gi, "")
23591
- .replace(/<nav[^>]*>[\\s\\S]*?<\\/nav>/gi, "")
23592
- .replace(/<header[^>]*>[\\s\\S]*?<\\/header>/gi, "")
23593
- .replace(/<footer[^>]*>[\\s\\S]*?<\\/footer>/gi, "");
23594
-
23595
- // Convert headings
23596
- md = md.replace(/<h1[^>]*>(.*?)<\\/h1>/gi, "# $1\\n");
23597
- md = md.replace(/<h2[^>]*>(.*?)<\\/h2>/gi, "## $1\\n");
23598
- md = md.replace(/<h3[^>]*>(.*?)<\\/h3>/gi, "### $1\\n");
23599
- md = md.replace(/<h4[^>]*>(.*?)<\\/h4>/gi, "#### $1\\n");
23600
-
23601
- // Convert code blocks
23602
- md = md.replace(/<pre[^>]*><code[^>]*>(.*?)<\\/code><\\/pre>/gis, "\`\`\`\\n$1\\n\`\`\`\\n");
23603
- md = md.replace(/<code[^>]*>(.*?)<\\/code>/gi, "\`$1\`");
23604
-
23605
- // Convert links
23606
- md = md.replace(/<a[^>]*href="([^"]*)"[^>]*>(.*?)<\\/a>/gi, "[$2]($1)");
23607
-
23608
- // Convert lists
23609
- md = md.replace(/<li[^>]*>(.*?)<\\/li>/gi, "- $1\\n");
23610
-
23611
- // Convert paragraphs
23612
- md = md.replace(/<p[^>]*>(.*?)<\\/p>/gis, "$1\\n\\n");
23613
- md = md.replace(/<br\\s*\\/?>/gi, "\\n");
23614
-
23615
- // Strip remaining tags
23616
- md = md.replace(/<[^>]+>/g, "");
23617
-
23618
- // Decode entities
23619
- md = md.replace(/&lt;/g, "<")
23620
- .replace(/&gt;/g, ">")
23621
- .replace(/&quot;/g, '"')
23622
- .replace(/&#39;/g, "'")
23623
- .replace(/&apos;/g, "'")
23624
- .replace(/&nbsp;/g, " ")
23625
- .replace(/&#(\\d+);/g, (_, n) => { const c = parseInt(n, 10); return c > 0 && c <= 0x10FFFF ? String.fromCodePoint(c) : ''; })
23626
- .replace(/&#x([0-9a-fA-F]+);/g, (_, h) => { const c = parseInt(h, 16); return c > 0 && c <= 0x10FFFF ? String.fromCodePoint(c) : ''; })
23627
- .replace(/&amp;/g, "&");
23628
-
23629
- // Clean whitespace
23630
- md = md.replace(/\\n{3,}/g, "\\n\\n").trim();
23631
-
23632
- console.log(md);
23633
- `;
23634
- }
23635
24134
 
23636
24135
  // src/index.ts
23637
24136
  var config2 = loadConfig(process.env.CLAUDE_PROJECT_DIR);