context-compress 2026.3.21 → 2026.5.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 (115) hide show
  1. package/README.md +258 -44
  2. package/dist/cli/doctor.d.ts.map +1 -1
  3. package/dist/cli/doctor.js +2 -10
  4. package/dist/cli/doctor.js.map +1 -1
  5. package/dist/cli/filter.d.ts +52 -0
  6. package/dist/cli/filter.d.ts.map +1 -0
  7. package/dist/cli/filter.js +200 -0
  8. package/dist/cli/filter.js.map +1 -0
  9. package/dist/cli/index.d.ts +8 -4
  10. package/dist/cli/index.d.ts.map +1 -1
  11. package/dist/cli/index.js +19 -6
  12. package/dist/cli/index.js.map +1 -1
  13. package/dist/cli/lite.d.ts +15 -0
  14. package/dist/cli/lite.d.ts.map +1 -0
  15. package/dist/cli/lite.js +37 -0
  16. package/dist/cli/lite.js.map +1 -0
  17. package/dist/cli/setup.d.ts +23 -1
  18. package/dist/cli/setup.d.ts.map +1 -1
  19. package/dist/cli/setup.js +122 -21
  20. package/dist/cli/setup.js.map +1 -1
  21. package/dist/executor.d.ts +7 -1
  22. package/dist/executor.d.ts.map +1 -1
  23. package/dist/executor.js +51 -4
  24. package/dist/executor.js.map +1 -1
  25. package/dist/filters.d.ts +52 -0
  26. package/dist/filters.d.ts.map +1 -0
  27. package/dist/filters.js +719 -0
  28. package/dist/filters.js.map +1 -0
  29. package/dist/hooks/pretooluse.js +57 -0
  30. package/dist/hooks/pretooluse.js.map +1 -1
  31. package/dist/network.d.ts.map +1 -1
  32. package/dist/network.js +11 -0
  33. package/dist/network.js.map +1 -1
  34. package/dist/server.bundle.mjs +1333 -619
  35. package/dist/server.bundle.mjs.map +4 -4
  36. package/dist/server.d.ts.map +1 -1
  37. package/dist/server.js +44 -610
  38. package/dist/server.js.map +1 -1
  39. package/dist/stats.d.ts +7 -1
  40. package/dist/stats.d.ts.map +1 -1
  41. package/dist/stats.js +65 -0
  42. package/dist/stats.js.map +1 -1
  43. package/dist/store.d.ts +1 -0
  44. package/dist/store.d.ts.map +1 -1
  45. package/dist/store.js +15 -2
  46. package/dist/store.js.map +1 -1
  47. package/dist/tools/batch-execute.d.ts +4 -0
  48. package/dist/tools/batch-execute.d.ts.map +1 -0
  49. package/dist/tools/batch-execute.js +75 -0
  50. package/dist/tools/batch-execute.js.map +1 -0
  51. package/dist/tools/context.d.ts +17 -0
  52. package/dist/tools/context.d.ts.map +1 -0
  53. package/dist/tools/context.js +2 -0
  54. package/dist/tools/context.js.map +1 -0
  55. package/dist/tools/discover.d.ts +4 -0
  56. package/dist/tools/discover.d.ts.map +1 -0
  57. package/dist/tools/discover.js +65 -0
  58. package/dist/tools/discover.js.map +1 -0
  59. package/dist/tools/execute-file.d.ts +4 -0
  60. package/dist/tools/execute-file.d.ts.map +1 -0
  61. package/dist/tools/execute-file.js +66 -0
  62. package/dist/tools/execute-file.js.map +1 -0
  63. package/dist/tools/execute.d.ts +4 -0
  64. package/dist/tools/execute.d.ts.map +1 -0
  65. package/dist/tools/execute.js +54 -0
  66. package/dist/tools/execute.js.map +1 -0
  67. package/dist/tools/fetch-and-index.d.ts +4 -0
  68. package/dist/tools/fetch-and-index.d.ts.map +1 -0
  69. package/dist/tools/fetch-and-index.js +91 -0
  70. package/dist/tools/fetch-and-index.js.map +1 -0
  71. package/dist/tools/index-content.d.ts +4 -0
  72. package/dist/tools/index-content.d.ts.map +1 -0
  73. package/dist/tools/index-content.js +85 -0
  74. package/dist/tools/index-content.js.map +1 -0
  75. package/dist/tools/search.d.ts +4 -0
  76. package/dist/tools/search.d.ts.map +1 -0
  77. package/dist/tools/search.js +57 -0
  78. package/dist/tools/search.js.map +1 -0
  79. package/dist/tools/stats.d.ts +4 -0
  80. package/dist/tools/stats.d.ts.map +1 -0
  81. package/dist/tools/stats.js +10 -0
  82. package/dist/tools/stats.js.map +1 -0
  83. package/dist/types.d.ts +11 -0
  84. package/dist/types.d.ts.map +1 -1
  85. package/dist/util/auto-mode.d.ts +40 -0
  86. package/dist/util/auto-mode.d.ts.map +1 -0
  87. package/dist/util/auto-mode.js +181 -0
  88. package/dist/util/auto-mode.js.map +1 -0
  89. package/dist/util/fetch-code.d.ts +10 -0
  90. package/dist/util/fetch-code.d.ts.map +1 -0
  91. package/dist/util/fetch-code.js +87 -0
  92. package/dist/util/fetch-code.js.map +1 -0
  93. package/dist/util/intent-filter.d.ts +17 -0
  94. package/dist/util/intent-filter.d.ts.map +1 -0
  95. package/dist/util/intent-filter.js +28 -0
  96. package/dist/util/intent-filter.js.map +1 -0
  97. package/dist/util/label.d.ts +4 -0
  98. package/dist/util/label.d.ts.map +1 -0
  99. package/dist/util/label.js +14 -0
  100. package/dist/util/label.js.map +1 -0
  101. package/dist/util/path.d.ts +8 -0
  102. package/dist/util/path.d.ts.map +1 -0
  103. package/dist/util/path.js +21 -0
  104. package/dist/util/path.js.map +1 -0
  105. package/dist/util/stream-compress.d.ts +36 -0
  106. package/dist/util/stream-compress.d.ts.map +1 -0
  107. package/dist/util/stream-compress.js +104 -0
  108. package/dist/util/stream-compress.js.map +1 -0
  109. package/dist/util/version.d.ts +2 -0
  110. package/dist/util/version.d.ts.map +1 -0
  111. package/dist/util/version.js +15 -0
  112. package/dist/util/version.js.map +1 -0
  113. package/docs/token-reduction-report.md +164 -88
  114. package/hooks/pretooluse.mjs +38 -0
  115. package/package.json +5 -4
@@ -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 readFileSync2, 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
  }
@@ -21217,6 +21215,510 @@ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
21217
21215
  import { tmpdir } from "node:os";
21218
21216
  import { join as join2 } from "node:path";
21219
21217
 
21218
+ // src/filters.ts
21219
+ var DEFAULT_MODE = "balanced";
21220
+ function applyCommandFilter(code, stdout, mode = DEFAULT_MODE) {
21221
+ if (mode === "conservative") return { output: stdout, filtered: false };
21222
+ const cmd = code.trim().split(/\s+/)[0];
21223
+ const fullCmd = code.trim();
21224
+ if (cmd === "git") return filterGit(fullCmd, stdout, mode);
21225
+ if (cmd === "npm" || cmd === "yarn" || cmd === "pnpm" || cmd === "bun")
21226
+ return filterPackageManager(fullCmd, stdout, mode);
21227
+ if (fullCmd.includes("test") || fullCmd.includes("jest") || fullCmd.includes("vitest") || fullCmd.includes("pytest") || fullCmd.includes("cargo test")) {
21228
+ return filterTestOutput(stdout);
21229
+ }
21230
+ if (cmd === "cargo" || cmd === "make" || cmd === "gradle")
21231
+ return filterBuildOutput(fullCmd, stdout);
21232
+ if (cmd === "docker" || cmd === "kubectl") return filterContainerOutput(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
+ }
21243
+ return { output: stdout, filtered: false };
21244
+ }
21245
+ function filterGit(cmd, stdout, mode = DEFAULT_MODE) {
21246
+ if (/git\s+(push|pull|fetch|clone)/.test(cmd)) {
21247
+ const lines = stdout.split("\n");
21248
+ const filtered = lines.filter(
21249
+ (l) => !l.startsWith("remote: Counting") && !l.startsWith("remote: Compressing") && !l.startsWith("remote: Total") && !l.includes("Unpacking objects:") && !l.includes("Receiving objects:") && !l.includes("Resolving deltas:") && !/^\s*\d+%/.test(l)
21250
+ );
21251
+ return { output: filtered.join("\n"), filtered: true };
21252
+ }
21253
+ if (/git\s+status/.test(cmd)) {
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 };
21272
+ }
21273
+ return { output: stdout, filtered: false };
21274
+ }
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) {
21441
+ if (/\b(install|add|i)\b/.test(cmd)) {
21442
+ const lines = stdout.split("\n");
21443
+ const filtered = lines.filter(
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
21445
+ !/^\s*$/.test(l)
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
+ }
21453
+ return { output: filtered.join("\n"), filtered: true };
21454
+ }
21455
+ if (/\btest\b/.test(cmd)) {
21456
+ return filterTestOutput(stdout);
21457
+ }
21458
+ if (mode === "aggressive" && /\b(ls|list|ll)\b/.test(cmd)) {
21459
+ return filterNpmLs(stdout);
21460
+ }
21461
+ return { output: stdout, filtered: false };
21462
+ }
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/;
21479
+ var FAIL_WORD_RE = /\bFAIL\b/;
21480
+ var FAILED_RE = /\bfailed?\b/i;
21481
+ var ERROR_RE = /\bERROR\b/;
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;
21483
+ function isFailMarker(line) {
21484
+ return FAIL_MARKER_RE.test(line) || FAIL_WORD_RE.test(line) || FAILED_RE.test(line) || ERROR_RE.test(line);
21485
+ }
21486
+ function isSummaryLine(line) {
21487
+ return SUMMARY_RE.test(line);
21488
+ }
21489
+ function filterTestOutput(stdout) {
21490
+ const lines = stdout.split("\n");
21491
+ const failures = [];
21492
+ const summary = [];
21493
+ let inFailure = false;
21494
+ let failCount = 0;
21495
+ for (const line of lines) {
21496
+ if (isFailMarker(line)) {
21497
+ inFailure = true;
21498
+ failCount++;
21499
+ }
21500
+ if (inFailure) {
21501
+ failures.push(line);
21502
+ if (line.trim() === "" && failures.length > 3) inFailure = false;
21503
+ }
21504
+ if (isSummaryLine(line)) {
21505
+ summary.push(line);
21506
+ }
21507
+ }
21508
+ if (failCount === 0 && summary.length > 0) {
21509
+ return { output: summary.join("\n"), filtered: true };
21510
+ }
21511
+ if (failures.length > 0) {
21512
+ const rollup = summary.filter((l) => !/^PASS\s/i.test(l));
21513
+ const result = [...failures, "", ...rollup].join("\n");
21514
+ return { output: result, filtered: true };
21515
+ }
21516
+ return { output: stdout, filtered: false };
21517
+ }
21518
+ function filterBuildOutput(cmd, stdout) {
21519
+ const lines = stdout.split("\n");
21520
+ const filtered = lines.filter(
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)
21522
+ );
21523
+ return { output: filtered.join("\n"), filtered: filtered.length < lines.length };
21524
+ }
21525
+ function filterContainerOutput(cmd, stdout) {
21526
+ if (/docker\s+build/.test(cmd)) {
21527
+ const lines = stdout.split("\n");
21528
+ const filtered = lines.filter(
21529
+ (l) => !l.startsWith(" ---> ") && !l.startsWith("Sending build context") && !/^\s*$/.test(l)
21530
+ );
21531
+ return { output: filtered.join("\n"), filtered: true };
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
+ }
21556
+ return { output: stdout, filtered: false };
21557
+ }
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 };
21568
+ const lines = stdout.split("\n").filter((l) => l.trim() !== "");
21569
+ if (lines.length <= summarizeAt) return { output: stdout, filtered: false };
21570
+ if (cmd.includes("-R") || cmd.startsWith("find")) {
21571
+ const dirs = /* @__PURE__ */ new Map();
21572
+ for (const line of lines) {
21573
+ const parts = line.split("/");
21574
+ const dir = parts.length > 1 ? parts.slice(0, -1).join("/") : ".";
21575
+ dirs.set(dir, (dirs.get(dir) ?? 0) + 1);
21576
+ }
21577
+ if (dirs.size > minDirs) {
21578
+ const summary = Array.from(dirs.entries()).sort((a, b) => b[1] - a[1]).map(([dir, count]) => ` ${dir}/ (${count} files)`).join("\n");
21579
+ return {
21580
+ output: `${lines.length} files found:
21581
+ ${summary}`,
21582
+ filtered: true
21583
+ };
21584
+ }
21585
+ }
21586
+ return { output: stdout, filtered: false };
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
+ }
21721
+
21220
21722
  // src/utils.ts
21221
21723
  function detectInjectionPatterns(content) {
21222
21724
  const warnings = [];
@@ -21265,6 +21767,11 @@ function formatBytes(bytes) {
21265
21767
 
21266
21768
  // src/executor.ts
21267
21769
  var DEFAULT_TIMEOUT = 3e4;
21770
+ var ANSI_RE = /\x1b\[[0-9;]*[a-zA-Z]/;
21771
+ var ANSI_RE_G = /\x1b\[[0-9;]*[a-zA-Z]/g;
21772
+ function stripAnsi(str) {
21773
+ return str.replace(ANSI_RE_G, "");
21774
+ }
21268
21775
  var SAFE_ENV_KEYS = [
21269
21776
  "PATH",
21270
21777
  "HOME",
@@ -21317,6 +21824,20 @@ function killProcessTree(pid) {
21317
21824
  }
21318
21825
  }
21319
21826
  }
21827
+ function stripProgressLines(output) {
21828
+ const lines = output.split("\n");
21829
+ const filtered = lines.filter((l) => {
21830
+ const trimmed = l.trim();
21831
+ if (ANSI_RE.test(l) && trimmed.replace(ANSI_RE_G, "").trim() === "") return false;
21832
+ if (/^[\s\[│├└─═━▓░█▒▏▎▍▌▋▊▉\]>=#\-.\d%]+$/.test(trimmed) && trimmed.length > 3) return false;
21833
+ if (/^[⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏\-\\|/]\s/.test(trimmed)) return false;
21834
+ if (/(?:downloading|uploading|fetching|resolving)\s+[\d.]+\s*[kmg]?b/i.test(trimmed))
21835
+ return false;
21836
+ if (/\d+\.?\d*\s*[kmg]?b\/s/i.test(trimmed) && /eta|remaining/i.test(trimmed)) return false;
21837
+ return true;
21838
+ });
21839
+ return filtered.join("\n");
21840
+ }
21320
21841
  function deduplicateLines(output) {
21321
21842
  const lines = output.split("\n");
21322
21843
  if (lines.length < 3) return output;
@@ -21348,12 +21869,12 @@ function deduplicateLines(output) {
21348
21869
  function groupErrorLines(output) {
21349
21870
  const lines = output.split("\n");
21350
21871
  if (lines.length < 5) return output;
21351
- const ERROR_RE = /^(.*?(?:error|warning|Error|Warning|ERR|WARN)[:\s])\s*(.+?)(?:\s+(?:at|in|on)\s+(?:line\s+)?(\d+))?$/i;
21872
+ const ERROR_RE2 = /^(.*?(?:error|warning|Error|Warning|ERR|WARN)[:\s])\s*(.+?)(?:\s+(?:at|in|on)\s+(?:line\s+)?(\d+))?$/i;
21352
21873
  const errorGroups = /* @__PURE__ */ new Map();
21353
21874
  const resultLines = [];
21354
21875
  let groupedCount = 0;
21355
21876
  for (const line of lines) {
21356
- const match = line.match(ERROR_RE);
21877
+ const match = line.match(ERROR_RE2);
21357
21878
  if (match) {
21358
21879
  const prefix = match[1].trim();
21359
21880
  const msg = match[2].trim();
@@ -21505,7 +22026,8 @@ var SubprocessExecutor = class {
21505
22026
  tmpDir,
21506
22027
  timeout,
21507
22028
  maxOutput,
21508
- plugin.needsShell
22029
+ plugin.needsShell,
22030
+ opts.language === "shell" ? opts.code : void 0
21509
22031
  );
21510
22032
  } finally {
21511
22033
  setTimeout(() => this.cleanupTempDir(tmpDir), 100).unref();
@@ -21532,8 +22054,8 @@ var SubprocessExecutor = class {
21532
22054
  }
21533
22055
  return this.execute({ ...opts, code });
21534
22056
  }
21535
- spawnAndCapture(cmd, args, cwd, timeout, maxOutput, useShell) {
21536
- return new Promise((resolve2) => {
22057
+ spawnAndCapture(cmd, args, cwd, timeout, maxOutput, useShell, shellCode) {
22058
+ return new Promise((resolve5) => {
21537
22059
  const hardCap = this.config.hardCapBytes;
21538
22060
  const stdoutChunks = [];
21539
22061
  const stderrChunks = [];
@@ -21573,7 +22095,7 @@ var SubprocessExecutor = class {
21573
22095
  this.activeProcesses.delete(proc);
21574
22096
  if (!resolved) {
21575
22097
  resolved = true;
21576
- resolve2({
22098
+ resolve5({
21577
22099
  stdout: "",
21578
22100
  stderr: err.message,
21579
22101
  exitCode: 1,
@@ -21597,7 +22119,15 @@ var SubprocessExecutor = class {
21597
22119
  stdout += `
21598
22120
  [output capped at ${formatBytes(hardCap)} \u2014 process killed]`;
21599
22121
  }
22122
+ stdout = stripAnsi(stdout);
22123
+ if (shellCode && stdout) {
22124
+ const filtered = applyCommandFilter(shellCode, stdout);
22125
+ if (filtered.filtered) {
22126
+ stdout = filtered.output;
22127
+ }
22128
+ }
21600
22129
  if (stdout.length > 1e4) {
22130
+ stdout = stripProgressLines(stdout);
21601
22131
  stdout = deduplicateLines(stdout);
21602
22132
  stdout = groupErrorLines(stdout);
21603
22133
  }
@@ -21605,7 +22135,7 @@ var SubprocessExecutor = class {
21605
22135
  if (truncated) {
21606
22136
  stdout = smartTruncate(stdout, maxOutput);
21607
22137
  }
21608
- resolve2({
22138
+ resolve5({
21609
22139
  stdout,
21610
22140
  stderr,
21611
22141
  exitCode: code,
@@ -21638,65 +22168,6 @@ async function __cm_main(){${code}}
21638
22168
  __cm_main().then(()=>{${epilogue}}).catch(e=>{console.error(e);${epilogue}process.exit(1)});`;
21639
22169
  }
21640
22170
 
21641
- // src/network.ts
21642
- import dns from "node:dns";
21643
- function isPrivateHost(hostname2) {
21644
- const h = hostname2.startsWith("[") && hostname2.endsWith("]") ? hostname2.slice(1, -1) : hostname2;
21645
- const lower = h.toLowerCase();
21646
- if (lower === "localhost" || lower === "0.0.0.0") return true;
21647
- if (/^0\./.test(h)) return true;
21648
- if (/^127\./.test(h)) return true;
21649
- if (/^10\./.test(h)) return true;
21650
- if (/^172\.(1[6-9]|2\d|3[01])\./.test(h)) return true;
21651
- if (/^192\.168\./.test(h)) return true;
21652
- if (/^169\.254\./.test(h)) return true;
21653
- if (/^100\.(6[4-9]|[7-9]\d|1[01]\d|12[0-7])\./.test(h)) return true;
21654
- if (lower === "::1") return true;
21655
- if (lower === "::" || lower === "0:0:0:0:0:0:0:0") return true;
21656
- const mappedMatch = lower.match(/^::ffff:(\d+\.\d+\.\d+\.\d+)$/);
21657
- if (mappedMatch) return isPrivateHost(mappedMatch[1]);
21658
- if (/^fe[89ab]/i.test(h)) return true;
21659
- if (/^f[cd]/i.test(h)) return true;
21660
- return false;
21661
- }
21662
- async function resolveAndValidate(url) {
21663
- const parsed = new URL(url);
21664
- const hostname2 = parsed.hostname;
21665
- if (/^\d+\.\d+\.\d+\.\d+$/.test(hostname2) || hostname2.includes(":")) {
21666
- if (isPrivateHost(hostname2)) {
21667
- throw new Error(`Blocked: resolved IP ${hostname2} is a private/internal address`);
21668
- }
21669
- return { url, resolvedIp: null };
21670
- }
21671
- let resolvedIp = null;
21672
- let v4Error = false;
21673
- let v6Error = false;
21674
- const [v4Result, v6Result] = await Promise.allSettled([
21675
- dns.promises.lookup(hostname2, { family: 4 }),
21676
- dns.promises.lookup(hostname2, { family: 6 })
21677
- ]);
21678
- if (v4Result.status === "fulfilled") {
21679
- if (isPrivateHost(v4Result.value.address)) {
21680
- throw new Error(`Blocked: ${hostname2} resolved to private IP ${v4Result.value.address}`);
21681
- }
21682
- resolvedIp = v4Result.value.address;
21683
- } else {
21684
- v4Error = true;
21685
- }
21686
- if (v6Result.status === "fulfilled") {
21687
- if (isPrivateHost(v6Result.value.address)) {
21688
- throw new Error(`Blocked: ${hostname2} resolved to private IPv6 ${v6Result.value.address}`);
21689
- }
21690
- if (!resolvedIp) resolvedIp = v6Result.value.address;
21691
- } else {
21692
- v6Error = true;
21693
- }
21694
- if (v4Error && v6Error) {
21695
- throw new Error(`DNS resolution failed for ${hostname2}: unable to verify host safety`);
21696
- }
21697
- return { url, resolvedIp };
21698
- }
21699
-
21700
22171
  // src/runtime/index.ts
21701
22172
  import { exec } from "node:child_process";
21702
22173
  import { promisify } from "node:util";
@@ -22013,6 +22484,7 @@ function hasBun(runtimes) {
22013
22484
  }
22014
22485
 
22015
22486
  // src/stats.ts
22487
+ import { readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "node:fs";
22016
22488
  var BAR_WIDTH = 20;
22017
22489
  function asciiBar(ratio, width = BAR_WIDTH) {
22018
22490
  const filled = Math.round(ratio * width);
@@ -22032,6 +22504,10 @@ var SessionTracker = class {
22032
22504
  bytesSandboxed: 0,
22033
22505
  sessionStart: Date.now()
22034
22506
  };
22507
+ cumulativeFile;
22508
+ constructor(cumulativeFile) {
22509
+ this.cumulativeFile = cumulativeFile ?? null;
22510
+ }
22035
22511
  trackCall(toolName, responseBytes) {
22036
22512
  this.stats.calls[toolName] = (this.stats.calls[toolName] ?? 0) + 1;
22037
22513
  this.stats.bytesReturned[toolName] = (this.stats.bytesReturned[toolName] ?? 0) + responseBytes;
@@ -22045,6 +22521,47 @@ var SessionTracker = class {
22045
22521
  getSnapshot() {
22046
22522
  return { ...this.stats };
22047
22523
  }
22524
+ /** Load cumulative stats from disk */
22525
+ loadCumulative() {
22526
+ if (!this.cumulativeFile) return null;
22527
+ try {
22528
+ const data = readFileSync2(this.cumulativeFile, "utf-8");
22529
+ return JSON.parse(data);
22530
+ } catch {
22531
+ return null;
22532
+ }
22533
+ }
22534
+ /** Save current session stats to cumulative file */
22535
+ saveCumulative() {
22536
+ if (!this.cumulativeFile) return;
22537
+ const snap = this.stats;
22538
+ const keptOut = snap.bytesIndexed + snap.bytesSandboxed;
22539
+ const totalReturned = Object.values(snap.bytesReturned).reduce((a, b) => a + b, 0);
22540
+ const cumulative = this.loadCumulative() ?? {
22541
+ totalBytesSaved: 0,
22542
+ totalBytesProcessed: 0,
22543
+ totalCalls: 0,
22544
+ totalSessions: 0,
22545
+ firstSeen: (/* @__PURE__ */ new Date()).toISOString(),
22546
+ lastSeen: (/* @__PURE__ */ new Date()).toISOString(),
22547
+ perCommand: {}
22548
+ };
22549
+ cumulative.totalBytesSaved += keptOut;
22550
+ cumulative.totalBytesProcessed += keptOut + totalReturned;
22551
+ cumulative.totalCalls += Object.values(snap.calls).reduce((a, b) => a + b, 0);
22552
+ cumulative.totalSessions += 1;
22553
+ cumulative.lastSeen = (/* @__PURE__ */ new Date()).toISOString();
22554
+ for (const [name, calls] of Object.entries(snap.calls)) {
22555
+ if (!cumulative.perCommand[name]) {
22556
+ cumulative.perCommand[name] = { calls: 0 };
22557
+ }
22558
+ cumulative.perCommand[name].calls += calls;
22559
+ }
22560
+ try {
22561
+ writeFileSync2(this.cumulativeFile, JSON.stringify(cumulative, null, 2));
22562
+ } catch {
22563
+ }
22564
+ }
22048
22565
  formatReport() {
22049
22566
  const snap = this.stats;
22050
22567
  const elapsed = Date.now() - snap.sessionStart;
@@ -22105,6 +22622,18 @@ var SessionTracker = class {
22105
22622
  `
22106
22623
  Context-compress kept ${formatBytes(keptOut)} out of context (${reductionPct}% savings).`
22107
22624
  );
22625
+ const cumulative = this.loadCumulative();
22626
+ if (cumulative) {
22627
+ lines.push("\n## Cumulative Savings (All Sessions)\n");
22628
+ lines.push("| Metric | Value |");
22629
+ lines.push("|--------|-------|");
22630
+ lines.push(`| Sessions tracked | ${cumulative.totalSessions} |`);
22631
+ lines.push(`| Total data processed | ${formatBytes(cumulative.totalBytesProcessed)} |`);
22632
+ lines.push(`| Total kept out of context | ${formatBytes(cumulative.totalBytesSaved)} |`);
22633
+ const cumTokensMid = Math.round(cumulative.totalBytesSaved / 4);
22634
+ lines.push(`| Est. total tokens saved | ~${cumTokensMid.toLocaleString()} |`);
22635
+ lines.push(`| Tracking since | ${cumulative.firstSeen.split("T")[0]} |`);
22636
+ }
22108
22637
  return lines.join("\n");
22109
22638
  }
22110
22639
  };
@@ -22301,6 +22830,9 @@ var ContentStore = class {
22301
22830
  insertChunkStmt;
22302
22831
  vocabCountStmt;
22303
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();
22304
22836
  constructor(options) {
22305
22837
  let path;
22306
22838
  if (typeof options === "string") {
@@ -22393,6 +22925,7 @@ var ContentStore = class {
22393
22925
  return sourceId2;
22394
22926
  });
22395
22927
  const sourceId = tx();
22928
+ this.distinctiveTermsCache.clear();
22396
22929
  return {
22397
22930
  sourceId,
22398
22931
  label,
@@ -22516,10 +23049,16 @@ var ContentStore = class {
22516
23049
  * Get distinctive terms for search hint.
22517
23050
  */
22518
23051
  getDistinctiveTerms(sourceId) {
23052
+ const cacheKey = sourceId ?? "_all";
23053
+ const cached2 = this.distinctiveTermsCache.get(cacheKey);
23054
+ if (cached2) return cached2;
22519
23055
  const totalChunks = this.db.prepare(
22520
23056
  sourceId ? "SELECT COUNT(*) as cnt FROM chunks WHERE source_id = ?" : "SELECT COUNT(*) as cnt FROM chunks"
22521
23057
  ).get(...sourceId ? [sourceId] : []).cnt;
22522
- if (totalChunks === 0) return [];
23058
+ if (totalChunks === 0) {
23059
+ this.distinctiveTermsCache.set(cacheKey, []);
23060
+ return [];
23061
+ }
22523
23062
  const filter = sourceId ? " WHERE source_id = ?" : "";
22524
23063
  const stmt = this.db.prepare(`SELECT content FROM chunks${filter} LIMIT 500`);
22525
23064
  const rows = sourceId ? stmt.all(sourceId) : stmt.all();
@@ -22544,7 +23083,9 @@ var ContentStore = class {
22544
23083
  scored.push({ word, score });
22545
23084
  }
22546
23085
  scored.sort((a, b) => b.score - a.score);
22547
- 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;
22548
23089
  }
22549
23090
  /**
22550
23091
  * List all indexed sources with metadata.
@@ -22690,155 +23231,238 @@ function cleanupStaleDbs() {
22690
23231
  return cleaned;
22691
23232
  }
22692
23233
 
22693
- // src/types.ts
22694
- var ALL_LANGUAGES = [
22695
- "javascript",
22696
- "typescript",
22697
- "python",
22698
- "shell",
22699
- "ruby",
22700
- "go",
22701
- "rust",
22702
- "php",
22703
- "perl",
22704
- "r",
22705
- "elixir"
22706
- ];
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}
22707
23277
 
22708
- // src/server.ts
22709
- var LANGUAGE_ENUM = ALL_LANGUAGES;
22710
- var projectDir = process.env.CLAUDE_PROJECT_DIR ?? process.cwd();
22711
- function isWithinProject(absPath) {
22712
- try {
22713
- const normalized = realpathSync(resolve(absPath));
22714
- const realProjectDir = realpathSync(projectDir);
22715
- return normalized === realProjectDir || normalized.startsWith(`${realProjectDir}/`);
22716
- } catch {
22717
- const normalized = resolve(absPath);
22718
- return normalized === projectDir || normalized.startsWith(`${projectDir}/`);
22719
- }
22720
- }
22721
- function getVersion() {
22722
- try {
22723
- const __dirname = dirname(fileURLToPath(import.meta.url));
22724
- const pkgPath = join4(__dirname, "..", "package.json");
22725
- const pkg = JSON.parse(readFileSync2(pkgPath, "utf-8"));
22726
- return pkg.version ?? "1.0.0";
22727
- } catch {
22728
- return "1.0.0";
22729
- }
22730
- }
22731
- function compactLabel(normal, level) {
22732
- if (level === "ultra") {
22733
- return normal.replace(/\*\*/g, "").replace(/Use search\(queries: \[\.\.\.]\) to retrieve.*$/gm, "\u2192 search() for more").replace(/Searchable terms: .+$/gm, "");
22734
- }
22735
- if (level === "compact") {
22736
- return normal.replace(
22737
- /Use search\(queries: \[\.\.\.]\) to retrieve full content of any section\./,
22738
- "\u2192 search() for details"
22739
- );
22740
- }
22741
- return normal;
22742
- }
22743
- async function createServer(config3) {
22744
- const version2 = getVersion();
22745
- debug("Version:", version2);
22746
- cleanupStaleDbs();
22747
- const runtimes = await detectRuntimes();
22748
- const bunDetected = hasBun(runtimes);
22749
- debug("Runtimes detected:", runtimes.size);
22750
- const executor = new SubprocessExecutor(runtimes, config3);
22751
- let store;
22752
- let dbFallback = false;
22753
- try {
22754
- store = new ContentStore({ persistDb: config3.persistDb, dbDir: config3.dbDir });
22755
- } catch (e) {
22756
- debug("Failed to create DB, falling back to in-memory:", e);
22757
- store = new ContentStore(":memory:");
22758
- dbFallback = true;
22759
- }
22760
- const tracker = new SessionTracker();
22761
- let activeExecutions = 0;
22762
- const MAX_CONCURRENT_EXECUTIONS = 8;
22763
- const EXECUTION_LIMIT_ERROR = "Error: too many concurrent executions. Try again shortly.";
22764
- async function withExecutionLimit(fn) {
22765
- if (activeExecutions >= MAX_CONCURRENT_EXECUTIONS) {
22766
- throw new Error(EXECUTION_LIMIT_ERROR);
22767
- }
22768
- activeExecutions++;
22769
- try {
22770
- return await fn();
22771
- } finally {
22772
- activeExecutions--;
22773
- }
22774
- }
22775
- function applyIntentFilter(output, intent, sourceLabel) {
22776
- if (Buffer.byteLength(output) <= config3.intentSearchThreshold) return output;
22777
- const indexed = store.index(output, sourceLabel);
22778
- tracker.trackIndexed(Buffer.byteLength(output));
22779
- const searchResults = store.search(intent, { limit: 3 });
22780
- const terms = store.getDistinctiveTerms(indexed.sourceId);
22781
- let filtered = `Indexed ${indexed.totalChunks} sections from ${sourceLabel}.
22782
23278
  `;
22783
- 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})
22784
23285
 
22785
23286
  `;
22786
- for (const hit of searchResults.results) {
22787
- 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
+
22788
23302
  `;
22789
- }
22790
- if (terms.length > 0 && config3.compressionLevel !== "ultra") {
22791
- filtered += `
22792
- 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
+
22793
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 }] };
22794
23331
  }
22795
- filtered += "\nUse search(queries: [...]) to retrieve full content of any section.";
22796
- return compactLabel(filtered, config3.compressionLevel);
22797
- }
22798
- const shutdown = () => {
22799
- try {
22800
- executor.shutdown();
22801
- } catch {
22802
- }
22803
- try {
22804
- store.close();
22805
- } catch {
22806
- }
22807
- };
22808
- process.on("SIGINT", shutdown);
22809
- process.on("SIGTERM", shutdown);
22810
- process.on("beforeExit", shutdown);
22811
- process.on("uncaughtException", (err) => {
22812
- debug("Uncaught exception:", err);
22813
- shutdown();
22814
- process.exit(1);
22815
- });
22816
- process.on("unhandledRejection", (err) => {
22817
- debug("Unhandled rejection:", err);
22818
- shutdown();
22819
- process.exit(1);
22820
- });
22821
- const searchCalls = [];
22822
- const server2 = new McpServer({
22823
- name: "context-compress",
22824
- version: version2
22825
- });
23332
+ );
23333
+ }
23334
+
23335
+ // src/tools/discover.ts
23336
+ function registerDiscoverTool(server2, ctx) {
23337
+ const { store, tracker, dbFallback } = ctx;
22826
23338
  server2.tool(
22827
- "execute",
22828
- `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
+ }
22829
23415
 
22830
- 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.",
22831
23456
  {
23457
+ path: external_exports.string().describe("Absolute file path or relative to project root"),
22832
23458
  language: external_exports.enum(LANGUAGE_ENUM).describe("Runtime language"),
22833
23459
  code: external_exports.string().describe(
22834
- "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."
22835
- ),
22836
- intent: external_exports.string().optional().describe(
22837
- "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."
22838
23461
  ),
23462
+ intent: external_exports.string().optional().describe("What you're looking for in the output."),
22839
23463
  timeout: external_exports.number().default(3e4).describe("Max execution time in ms")
22840
23464
  },
22841
- async ({ language, code, intent, timeout }) => {
23465
+ async ({ path: filePath, language, code, intent, timeout }) => {
22842
23466
  const codeBytes = Buffer.byteLength(code);
22843
23467
  if (codeBytes > 1024e3) {
22844
23468
  return {
@@ -22847,18 +23471,35 @@ PREFER THIS OVER BASH for: API calls (gh, curl, aws), test runners (npm test, py
22847
23471
  type: "text",
22848
23472
  text: `Error: code too large (${(codeBytes / 1024).toFixed(0)}KB). Max 1MB.`
22849
23473
  }
22850
- ]
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
22851
23488
  };
22852
23489
  }
22853
23490
  let result;
22854
23491
  try {
22855
- result = await withExecutionLimit(() => executor.execute({ language, code, timeout }));
22856
- } catch (e) {
22857
- const msg = e instanceof Error ? e.message : String(e);
22858
- return { content: [{ type: "text", text: msg }] };
22859
- }
22860
- if (result.networkBytes) {
22861
- tracker.trackSandboxed(result.networkBytes);
23492
+ result = await withExecutionLimit(
23493
+ () => executor.executeFile({
23494
+ language,
23495
+ code,
23496
+ filePath: absPath,
23497
+ timeout
23498
+ })
23499
+ );
23500
+ } catch (e) {
23501
+ const msg = e instanceof Error ? e.message : String(e);
23502
+ return { content: [{ type: "text", text: msg }], isError: true };
22862
23503
  }
22863
23504
  let output = result.stdout;
22864
23505
  if (result.stderr && result.exitCode !== 0) {
@@ -22868,26 +23509,35 @@ STDERR:
22868
23509
  ${result.stderr}`;
22869
23510
  }
22870
23511
  if (intent) {
22871
- output = applyIntentFilter(output, intent, `execute:${language}`);
23512
+ output = applyIntentFilter(output, intent, `file:${filePath}`);
22872
23513
  }
22873
23514
  const responseBytes = Buffer.byteLength(output);
22874
- tracker.trackCall("execute", responseBytes);
23515
+ tracker.trackCall("execute_file", responseBytes);
22875
23516
  return { content: [{ type: "text", text: output }] };
22876
23517
  }
22877
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;
22878
23525
  server2.tool(
22879
- "execute_file",
22880
- "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.`,
22881
23530
  {
22882
- path: external_exports.string().describe("Absolute file path or relative to project root"),
22883
- language: external_exports.enum(LANGUAGE_ENUM).describe("Runtime language"),
23531
+ language: external_exports.enum(LANGUAGE_ENUM2).describe("Runtime language"),
22884
23532
  code: external_exports.string().describe(
22885
- "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."
22886
23537
  ),
22887
- intent: external_exports.string().optional().describe("What you're looking for in the output."),
22888
23538
  timeout: external_exports.number().default(3e4).describe("Max execution time in ms")
22889
23539
  },
22890
- async ({ path: filePath, language, code, intent, timeout }) => {
23540
+ async ({ language, code, intent, timeout }) => {
22891
23541
  const codeBytes = Buffer.byteLength(code);
22892
23542
  if (codeBytes > 1024e3) {
22893
23543
  return {
@@ -22896,33 +23546,19 @@ ${result.stderr}`;
22896
23546
  type: "text",
22897
23547
  text: `Error: code too large (${(codeBytes / 1024).toFixed(0)}KB). Max 1MB.`
22898
23548
  }
22899
- ]
22900
- };
22901
- }
22902
- const absPath = resolve(projectDir, filePath);
22903
- if (!isWithinProject(absPath)) {
22904
- return {
22905
- content: [
22906
- {
22907
- type: "text",
22908
- text: `Error: path "${filePath}" is outside the project directory`
22909
- }
22910
- ]
23549
+ ],
23550
+ isError: true
22911
23551
  };
22912
23552
  }
22913
23553
  let result;
22914
23554
  try {
22915
- result = await withExecutionLimit(
22916
- () => executor.executeFile({
22917
- language,
22918
- code,
22919
- filePath: absPath,
22920
- timeout
22921
- })
22922
- );
23555
+ result = await withExecutionLimit(() => executor.execute({ language, code, timeout }));
22923
23556
  } catch (e) {
22924
23557
  const msg = e instanceof Error ? e.message : String(e);
22925
- 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);
22926
23562
  }
22927
23563
  let output = result.stdout;
22928
23564
  if (result.stderr && result.exitCode !== 0) {
@@ -22932,164 +23568,191 @@ STDERR:
22932
23568
  ${result.stderr}`;
22933
23569
  }
22934
23570
  if (intent) {
22935
- output = applyIntentFilter(output, intent, `file:${filePath}`);
23571
+ output = applyIntentFilter(output, intent, `execute:${language}`);
22936
23572
  }
22937
23573
  const responseBytes = Buffer.byteLength(output);
22938
- tracker.trackCall("execute_file", responseBytes);
23574
+ tracker.trackCall("execute", responseBytes);
22939
23575
  return { content: [{ type: "text", text: output }] };
22940
23576
  }
22941
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;
22942
23728
  server2.tool(
22943
- "index",
22944
- "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.",
22945
23731
  {
22946
- content: external_exports.string().optional().describe("Raw text/markdown to index. Provide this OR path, not both."),
22947
- 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"),
22948
23733
  source: external_exports.string().optional().describe("Label for the indexed content")
22949
23734
  },
22950
- async ({ content, path: filePath, source }) => {
22951
- let text;
22952
- let label = source ?? "indexed content";
22953
- if (filePath) {
22954
- const absPath = resolve(projectDir, filePath);
22955
- if (!isWithinProject(absPath)) {
22956
- return {
22957
- content: [
22958
- {
22959
- type: "text",
22960
- text: `Error: path "${filePath}" is outside the project directory`
22961
- }
22962
- ]
22963
- };
22964
- }
22965
- try {
22966
- const fileStat = statSync(absPath);
22967
- if (fileStat.size > 50 * 1024 * 1024) {
22968
- return {
22969
- content: [
22970
- {
22971
- type: "text",
22972
- text: `Error: file "${filePath}" is too large (${(fileStat.size / 1024 / 1024).toFixed(1)}MB). Max 50MB.`
22973
- }
22974
- ]
22975
- };
22976
- }
22977
- text = readFileSync2(absPath, "utf-8");
22978
- label = source ?? filePath;
22979
- } catch (e) {
22980
- 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:") {
22981
23739
  return {
22982
- content: [{ type: "text", text: `Error reading "${filePath}": ${msg}` }]
23740
+ content: [{ type: "text", text: "Error: only http/https URLs are allowed" }],
23741
+ isError: true
22983
23742
  };
22984
23743
  }
22985
- } else if (content) {
22986
- const contentBytes = Buffer.byteLength(content);
22987
- if (contentBytes > 50 * 1024 * 1024) {
23744
+ if (isPrivateHost(parsed.hostname)) {
22988
23745
  return {
22989
23746
  content: [
22990
- {
22991
- type: "text",
22992
- text: `Error: content too large (${(contentBytes / 1024 / 1024).toFixed(1)}MB). Max 50MB.`
22993
- }
22994
- ]
23747
+ { type: "text", text: "Error: internal/private URLs are not allowed" }
23748
+ ],
23749
+ isError: true
22995
23750
  };
22996
23751
  }
22997
- text = content;
22998
- } else {
23752
+ } catch {
22999
23753
  return {
23000
- content: [{ type: "text", text: "Error: provide either 'content' or 'path'" }]
23001
- };
23002
- }
23003
- const result = store.index(text, label);
23004
- tracker.trackIndexed(Buffer.byteLength(text));
23005
- const summary = `Indexed "${label}": ${result.totalChunks} chunks (${result.codeChunks} with code). Use search(queries: [...]) to retrieve sections.`;
23006
- tracker.trackCall("index", Buffer.byteLength(summary));
23007
- return { content: [{ type: "text", text: summary }] };
23008
- }
23009
- );
23010
- server2.tool(
23011
- "search",
23012
- "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.",
23013
- {
23014
- queries: external_exports.array(external_exports.string()).describe("Array of search queries. Batch ALL questions in one call."),
23015
- source: external_exports.string().optional().describe("Filter to a specific indexed source (partial match)."),
23016
- limit: external_exports.number().default(3).describe("Results per query (default: 3)")
23017
- },
23018
- async ({ queries, source, limit }) => {
23019
- const now = Date.now();
23020
- searchCalls.push(now);
23021
- while (searchCalls.length > 0 && searchCalls[0] < now - config3.searchWindowMs) {
23022
- searchCalls.shift();
23023
- }
23024
- const callCount = searchCalls.length;
23025
- if (callCount > config3.searchBlockAfter) {
23026
- const msg = "Too many search calls in quick succession. Use batch_execute instead to run commands and search in one call.";
23027
- tracker.trackCall("search", Buffer.byteLength(msg));
23028
- return { content: [{ type: "text", text: msg }] };
23029
- }
23030
- const effectiveLimit = callCount > config3.searchReduceAfter ? 1 : Math.min(limit, config3.searchLimit);
23031
- const allResults = [];
23032
- let totalBytes = 0;
23033
- for (const query of queries) {
23034
- if (totalBytes > config3.searchMaxBytes) break;
23035
- const result = store.search(query, { source, limit: effectiveLimit });
23036
- let block = `## ${query}
23037
- `;
23038
- if (result.corrected) {
23039
- block += `(corrected to: "${result.corrected}")
23040
- `;
23041
- }
23042
- if (result.results.length === 0) {
23043
- block += "No results found.\n";
23044
- } else {
23045
- for (const hit of result.results) {
23046
- block += `
23047
- --- [${hit.source}] ---
23048
- ### ${hit.title}
23049
-
23050
- ${hit.snippet}
23051
- `;
23052
- }
23053
- }
23054
- allResults.push(block);
23055
- totalBytes += Buffer.byteLength(block);
23056
- }
23057
- if (callCount > config3.searchReduceAfter) {
23058
- allResults.push(
23059
- `
23060
- \u26A0 Search rate limited (${callCount} calls in ${config3.searchWindowMs / 1e3}s). Results reduced to 1 per query.`
23061
- );
23062
- }
23063
- const output = allResults.join("\n---\n\n");
23064
- tracker.trackCall("search", Buffer.byteLength(output));
23065
- return { content: [{ type: "text", text: output }] };
23066
- }
23067
- );
23068
- server2.tool(
23069
- "fetch_and_index",
23070
- "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.",
23071
- {
23072
- url: external_exports.string().describe("The URL to fetch and index"),
23073
- source: external_exports.string().optional().describe("Label for the indexed content")
23074
- },
23075
- async ({ url, source }) => {
23076
- try {
23077
- const parsed = new URL(url);
23078
- if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
23079
- return {
23080
- content: [{ type: "text", text: "Error: only http/https URLs are allowed" }]
23081
- };
23082
- }
23083
- if (isPrivateHost(parsed.hostname)) {
23084
- return {
23085
- content: [
23086
- { type: "text", text: "Error: internal/private URLs are not allowed" }
23087
- ]
23088
- };
23089
- }
23090
- } catch {
23091
- return {
23092
- content: [{ type: "text", text: `Error: invalid URL "${url}"` }]
23754
+ content: [{ type: "text", text: `Error: invalid URL "${url}"` }],
23755
+ isError: true
23093
23756
  };
23094
23757
  }
23095
23758
  let resolvedIp = null;
@@ -23103,7 +23766,8 @@ ${hit.snippet}
23103
23766
  type: "text",
23104
23767
  text: `Error: ${err instanceof Error ? err.message : "DNS validation failed"}`
23105
23768
  }
23106
- ]
23769
+ ],
23770
+ isError: true
23107
23771
  };
23108
23772
  }
23109
23773
  const label = source ?? url;
@@ -23119,12 +23783,12 @@ ${hit.snippet}
23119
23783
  );
23120
23784
  } catch (e) {
23121
23785
  const msg = e instanceof Error ? e.message : String(e);
23122
- return { content: [{ type: "text", text: msg }] };
23786
+ return { content: [{ type: "text", text: msg }], isError: true };
23123
23787
  }
23124
23788
  if (result.exitCode !== 0 || !result.stdout.trim()) {
23125
23789
  const errMsg = `Failed to fetch ${url}: ${result.stderr || "empty response"}`;
23126
23790
  tracker.trackCall("fetch_and_index", Buffer.byteLength(errMsg));
23127
- return { content: [{ type: "text", text: errMsg }] };
23791
+ return { content: [{ type: "text", text: errMsg }], isError: true };
23128
23792
  }
23129
23793
  const markdown = result.stdout;
23130
23794
  tracker.trackSandboxed(result.networkBytes ?? 0);
@@ -23154,188 +23818,311 @@ Searchable terms: ${terms.join(", ")}`;
23154
23818
  return { content: [{ type: "text", text: output }] };
23155
23819
  }
23156
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;
23157
23828
  server2.tool(
23158
- "batch_execute",
23159
- "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.",
23160
23831
  {
23161
- commands: external_exports.array(
23162
- external_exports.object({
23163
- label: external_exports.string().describe("Section header for this command's output"),
23164
- command: external_exports.string().describe("Shell command to execute")
23165
- })
23166
- ).describe("Commands to execute as a batch."),
23167
- queries: external_exports.array(external_exports.string()).describe(
23168
- "Search queries to extract information from indexed output. Use 5-8 comprehensive queries."
23169
- ),
23170
- 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")
23171
23835
  },
23172
- async ({ commands, queries, timeout }) => {
23173
- const commandResults = await limitConcurrency(
23174
- commands.map((cmd) => async () => {
23175
- const result = await withExecutionLimit(
23176
- () => executor.execute({
23177
- language: "shell",
23178
- code: cmd.command,
23179
- timeout
23180
- })
23181
- );
23182
- return { label: cmd.label, result };
23183
- }),
23184
- 4
23185
- );
23186
- let combined = "";
23187
- const inventory = [];
23188
- for (let i = 0; i < commandResults.length; i++) {
23189
- const settled = commandResults[i];
23190
- const label = commands[i].label;
23191
- if (settled.status === "fulfilled") {
23192
- const { result } = settled.value;
23193
- const output2 = result.stdout || "(no output)";
23194
- combined += `## ${label}
23195
-
23196
- ${output2}
23197
-
23198
- `;
23199
- const lineCount = output2.split("\n").length;
23200
- inventory.push(`- **${label}**: ${lineCount} lines`);
23201
- } else {
23202
- combined += `## ${label}
23203
-
23204
- (error: ${settled.reason})
23205
-
23206
- `;
23207
- 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
+ };
23208
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
+ };
23886
+ }
23887
+ text = content;
23888
+ } else {
23889
+ return {
23890
+ content: [{ type: "text", text: "Error: provide either 'content' or 'path'" }],
23891
+ isError: true
23892
+ };
23209
23893
  }
23210
- const indexed = store.index(combined, "batch_execute");
23211
- tracker.trackIndexed(Buffer.byteLength(combined));
23212
- 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 = [];
23213
23929
  let totalBytes = 0;
23214
23930
  for (const query of queries) {
23215
- if (totalBytes > config3.batchMaxBytes) break;
23216
- let result = store.search(query, { source: "batch_execute", limit: 5 });
23217
- if (result.results.length === 0) {
23218
- result = store.search(query, { limit: 5 });
23219
- }
23931
+ if (totalBytes > config3.searchMaxBytes) break;
23932
+ const result = store.search(query, { source, limit: effectiveLimit });
23220
23933
  let block = `## ${query}
23221
-
23222
23934
  `;
23935
+ if (result.corrected) {
23936
+ block += `(corrected to: "${result.corrected}")
23937
+ `;
23938
+ }
23223
23939
  if (result.results.length === 0) {
23224
23940
  block += "No results found.\n";
23225
23941
  } else {
23226
23942
  for (const hit of result.results) {
23227
- block += `--- [${hit.source}] ---
23943
+ block += `
23944
+ --- [${hit.source}] ---
23228
23945
  ### ${hit.title}
23229
23946
 
23230
23947
  ${hit.snippet}
23231
-
23232
23948
  `;
23233
23949
  }
23234
23950
  }
23235
- searchResults.push(block);
23951
+ allResults.push(block);
23236
23952
  totalBytes += Buffer.byteLength(block);
23237
23953
  }
23238
- const terms = store.getDistinctiveTerms(indexed.sourceId);
23239
- let output = `**Inventory** (${commands.length} commands):
23240
- ${inventory.join("\n")}
23241
-
23242
- `;
23243
- output += searchResults.join("\n---\n\n");
23244
- if (terms.length > 0) {
23245
- output += `
23246
-
23247
- 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
+ );
23248
23959
  }
23249
- tracker.trackCall("batch_execute", Buffer.byteLength(output));
23960
+ const output = allResults.join("\n---\n\n");
23961
+ tracker.trackCall("search", Buffer.byteLength(output));
23250
23962
  return { content: [{ type: "text", text: output }] };
23251
23963
  }
23252
23964
  );
23965
+ }
23966
+
23967
+ // src/tools/stats.ts
23968
+ function registerStatsTool(server2, ctx) {
23969
+ const { tracker } = ctx;
23253
23970
  server2.tool(
23254
23971
  "stats",
23255
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.",
23256
23973
  {},
23257
23974
  async () => {
23975
+ tracker.saveCumulative();
23258
23976
  const report = tracker.formatReport();
23259
23977
  tracker.trackCall("stats", Buffer.byteLength(report));
23260
23978
  return { content: [{ type: "text", text: report }] };
23261
23979
  }
23262
23980
  );
23263
- server2.tool(
23264
- "discover",
23265
- "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.",
23266
- {},
23267
- async () => {
23268
- const storeStats = store.getStats();
23269
- const snap = tracker.getSnapshot();
23270
- const lines = [];
23271
- lines.push("## Knowledge Base Discovery\n");
23272
- if (storeStats.totalSources === 0) {
23273
- lines.push("No content indexed yet. Use these tools to build the knowledge base:\n");
23274
- lines.push("- `batch_execute` \u2014 run commands and auto-index output");
23275
- lines.push("- `execute` with `intent` \u2014 auto-indexes large output");
23276
- lines.push("- `index` \u2014 index documentation or files");
23277
- lines.push("- `fetch_and_index` \u2014 fetch and index web pages");
23278
- } else {
23279
- lines.push("| Metric | Value |");
23280
- lines.push("|--------|-------|");
23281
- lines.push(`| Indexed sources | ${storeStats.totalSources} |`);
23282
- lines.push(`| Total chunks | ${storeStats.totalChunks} |`);
23283
- lines.push(`| Vocabulary size | ${storeStats.vocabularySize} |`);
23284
- lines.push(
23285
- `| Trigram index | ${storeStats.hasTrigramTable ? "active" : "lazy (not yet needed)"} |`
23286
- );
23287
- const sources = store.listSources();
23288
- if (sources.length > 0) {
23289
- lines.push("\n### Indexed Sources\n");
23290
- for (const src of sources) {
23291
- lines.push(
23292
- `- **${src.label}** \u2014 ${src.chunkCount} chunks${src.codeChunks > 0 ? ` (${src.codeChunks} with code)` : ""}`
23293
- );
23294
- }
23295
- }
23296
- const terms = store.getDistinctiveTerms();
23297
- if (terms.length > 0) {
23298
- lines.push("\n### Top Searchable Terms\n");
23299
- lines.push(terms.slice(0, 20).join(", "));
23300
- }
23301
- }
23302
- lines.push("\n### Optimization Suggestions\n");
23303
- const totalCalls = Object.values(snap.calls).reduce((a, b) => a + b, 0);
23304
- if (totalCalls === 0) {
23305
- lines.push("- Start by using `batch_execute` to run multiple commands at once");
23306
- } else {
23307
- const searchCalls2 = snap.calls.search ?? 0;
23308
- const executeCalls = snap.calls.execute ?? 0;
23309
- const batchCalls = snap.calls.batch_execute ?? 0;
23310
- if (executeCalls > 3 && batchCalls === 0) {
23311
- lines.push(
23312
- "- **Use batch_execute** \u2014 you've made multiple execute calls that could be batched into one"
23313
- );
23314
- }
23315
- if (searchCalls2 > 5) {
23316
- lines.push("- **Batch your searches** \u2014 pass multiple queries in a single search() call");
23317
- }
23318
- if (storeStats.totalChunks > 50) {
23319
- lines.push(
23320
- "- **Use source filtering** \u2014 scope searches with `source` parameter for faster, targeted results"
23321
- );
23322
- }
23323
- if (storeStats.totalSources === 0 && totalCalls > 2) {
23324
- lines.push(
23325
- "- **Index more content** \u2014 use `intent` parameter in execute calls to auto-index large output"
23326
- );
23327
- }
23328
- }
23329
- if (dbFallback) {
23330
- lines.push(
23331
- "\n\u26A0 **Warning:** Persistent DB creation failed \u2014 using in-memory storage. Indexed data will not survive restarts."
23332
- );
23333
- }
23334
- const output = lines.join("\n");
23335
- tracker.trackCall("discover", Buffer.byteLength(output));
23336
- 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
+ `;
23337
24014
  }
23338
- );
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);
23339
24126
  return {
23340
24127
  async start() {
23341
24128
  const transport = new StdioServerTransport();
@@ -23344,79 +24131,6 @@ Searchable terms: ${terms.join(", ")}`;
23344
24131
  }
23345
24132
  };
23346
24133
  }
23347
- function buildFetchCode(url, resolvedIp) {
23348
- let fetchSetup;
23349
- if (resolvedIp) {
23350
- const pinnedUrl = new URL(url);
23351
- const originalHost = pinnedUrl.host;
23352
- pinnedUrl.hostname = resolvedIp;
23353
- fetchSetup = `
23354
- const url = ${JSON.stringify(pinnedUrl.toString())};
23355
- const resp = await fetch(url, { headers: { 'Host': ${JSON.stringify(originalHost)} }, redirect: 'error' });`;
23356
- } else {
23357
- fetchSetup = `
23358
- const url = ${JSON.stringify(url)};
23359
- const resp = await fetch(url, { redirect: 'error' });`;
23360
- }
23361
- return `${fetchSetup}
23362
- if (!resp.ok) { console.error("HTTP " + resp.status); process.exit(1); }
23363
- const cl = resp.headers.get('content-length');
23364
- if (cl && parseInt(cl, 10) > 10 * 1024 * 1024) {
23365
- console.error("Response too large: " + cl + " bytes"); process.exit(1);
23366
- }
23367
- const html = await resp.text();
23368
- if (html.length > 10 * 1024 * 1024) {
23369
- console.error("Response body too large: " + html.length + " chars"); process.exit(1);
23370
- }
23371
-
23372
- // Strip unwanted tags
23373
- let md = html
23374
- .replace(/<script[^>]*>[\\s\\S]*?<\\/script>/gi, "")
23375
- .replace(/<style[^>]*>[\\s\\S]*?<\\/style>/gi, "")
23376
- .replace(/<nav[^>]*>[\\s\\S]*?<\\/nav>/gi, "")
23377
- .replace(/<header[^>]*>[\\s\\S]*?<\\/header>/gi, "")
23378
- .replace(/<footer[^>]*>[\\s\\S]*?<\\/footer>/gi, "");
23379
-
23380
- // Convert headings
23381
- md = md.replace(/<h1[^>]*>(.*?)<\\/h1>/gi, "# $1\\n");
23382
- md = md.replace(/<h2[^>]*>(.*?)<\\/h2>/gi, "## $1\\n");
23383
- md = md.replace(/<h3[^>]*>(.*?)<\\/h3>/gi, "### $1\\n");
23384
- md = md.replace(/<h4[^>]*>(.*?)<\\/h4>/gi, "#### $1\\n");
23385
-
23386
- // Convert code blocks
23387
- md = md.replace(/<pre[^>]*><code[^>]*>(.*?)<\\/code><\\/pre>/gis, "\`\`\`\\n$1\\n\`\`\`\\n");
23388
- md = md.replace(/<code[^>]*>(.*?)<\\/code>/gi, "\`$1\`");
23389
-
23390
- // Convert links
23391
- md = md.replace(/<a[^>]*href="([^"]*)"[^>]*>(.*?)<\\/a>/gi, "[$2]($1)");
23392
-
23393
- // Convert lists
23394
- md = md.replace(/<li[^>]*>(.*?)<\\/li>/gi, "- $1\\n");
23395
-
23396
- // Convert paragraphs
23397
- md = md.replace(/<p[^>]*>(.*?)<\\/p>/gis, "$1\\n\\n");
23398
- md = md.replace(/<br\\s*\\/?>/gi, "\\n");
23399
-
23400
- // Strip remaining tags
23401
- md = md.replace(/<[^>]+>/g, "");
23402
-
23403
- // Decode entities
23404
- md = md.replace(/&lt;/g, "<")
23405
- .replace(/&gt;/g, ">")
23406
- .replace(/&quot;/g, '"')
23407
- .replace(/&#39;/g, "'")
23408
- .replace(/&apos;/g, "'")
23409
- .replace(/&nbsp;/g, " ")
23410
- .replace(/&#(\\d+);/g, (_, n) => { const c = parseInt(n, 10); return c > 0 && c <= 0x10FFFF ? String.fromCodePoint(c) : ''; })
23411
- .replace(/&#x([0-9a-fA-F]+);/g, (_, h) => { const c = parseInt(h, 16); return c > 0 && c <= 0x10FFFF ? String.fromCodePoint(c) : ''; })
23412
- .replace(/&amp;/g, "&");
23413
-
23414
- // Clean whitespace
23415
- md = md.replace(/\\n{3,}/g, "\\n\\n").trim();
23416
-
23417
- console.log(md);
23418
- `;
23419
- }
23420
24134
 
23421
24135
  // src/index.ts
23422
24136
  var config2 = loadConfig(process.env.CLAUDE_PROJECT_DIR);