context-compress 1.0.0 → 2026.3.13

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 (45) hide show
  1. package/README.md +41 -5
  2. package/dist/cli/doctor.d.ts.map +1 -1
  3. package/dist/cli/doctor.js +22 -1
  4. package/dist/cli/doctor.js.map +1 -1
  5. package/dist/config.d.ts +3 -0
  6. package/dist/config.d.ts.map +1 -1
  7. package/dist/config.js +97 -4
  8. package/dist/config.js.map +1 -1
  9. package/dist/executor.d.ts +12 -0
  10. package/dist/executor.d.ts.map +1 -1
  11. package/dist/executor.js +129 -14
  12. package/dist/executor.js.map +1 -1
  13. package/dist/hooks/pretooluse.js +12 -6
  14. package/dist/hooks/pretooluse.js.map +1 -1
  15. package/dist/network.d.ts +5 -0
  16. package/dist/network.d.ts.map +1 -0
  17. package/dist/network.js +42 -0
  18. package/dist/network.js.map +1 -0
  19. package/dist/runtime/languages/go.js +3 -3
  20. package/dist/runtime/languages/go.js.map +1 -1
  21. package/dist/runtime/languages/javascript.js +1 -1
  22. package/dist/runtime/languages/javascript.js.map +1 -1
  23. package/dist/runtime/languages/r.js +1 -1
  24. package/dist/runtime/languages/rust.js +2 -2
  25. package/dist/runtime/languages/rust.js.map +1 -1
  26. package/dist/runtime/languages/typescript.js +1 -1
  27. package/dist/runtime/languages/typescript.js.map +1 -1
  28. package/dist/server.bundle.mjs +531 -152
  29. package/dist/server.bundle.mjs.map +4 -4
  30. package/dist/server.d.ts.map +1 -1
  31. package/dist/server.js +160 -25
  32. package/dist/server.js.map +1 -1
  33. package/dist/snippet.d.ts.map +1 -1
  34. package/dist/snippet.js +1 -8
  35. package/dist/snippet.js.map +1 -1
  36. package/dist/stats.d.ts.map +1 -1
  37. package/dist/stats.js +28 -11
  38. package/dist/stats.js.map +1 -1
  39. package/dist/store.d.ts +14 -0
  40. package/dist/store.d.ts.map +1 -1
  41. package/dist/store.js +44 -22
  42. package/dist/store.js.map +1 -1
  43. package/docs/token-reduction-report.md +459 -0
  44. package/hooks/pretooluse.mjs +10 -5
  45. package/package.json +7 -4
@@ -6800,85 +6800,8 @@ var require_dist = __commonJS({
6800
6800
 
6801
6801
  // src/config.ts
6802
6802
  import { readFileSync } from "node:fs";
6803
+ import { homedir } from "node:os";
6803
6804
  import { join } from "node:path";
6804
- var DEFAULTS = {
6805
- passthroughEnvVars: [],
6806
- debug: false,
6807
- blockCurl: true,
6808
- blockWebFetch: true,
6809
- nudgeOnRead: true,
6810
- nudgeOnGrep: true,
6811
- intentSearchThreshold: 5e3,
6812
- maxOutputBytes: 102400,
6813
- hardCapBytes: 100 * 1024 * 1024,
6814
- searchMaxBytes: 40960,
6815
- batchMaxBytes: 81920,
6816
- searchLimit: 3,
6817
- searchWindowMs: 6e4,
6818
- searchReduceAfter: 3,
6819
- searchBlockAfter: 8
6820
- };
6821
- function loadFileConfig(projectDir) {
6822
- const paths = [
6823
- projectDir && join(projectDir, ".context-compress.json"),
6824
- join(process.env.HOME ?? "~", ".context-compress.json")
6825
- ].filter(Boolean);
6826
- for (const p of paths) {
6827
- try {
6828
- const raw = readFileSync(p, "utf-8");
6829
- return JSON.parse(raw);
6830
- } catch {
6831
- }
6832
- }
6833
- return {};
6834
- }
6835
- function loadEnvConfig() {
6836
- const partial2 = {};
6837
- if (process.env.CONTEXT_COMPRESS_DEBUG === "1") {
6838
- partial2.debug = true;
6839
- }
6840
- if (process.env.CONTEXT_COMPRESS_PASSTHROUGH_ENV) {
6841
- partial2.passthroughEnvVars = process.env.CONTEXT_COMPRESS_PASSTHROUGH_ENV.split(",").map((s) => s.trim()).filter(Boolean);
6842
- }
6843
- if (process.env.CONTEXT_COMPRESS_BLOCK_CURL !== void 0) {
6844
- partial2.blockCurl = process.env.CONTEXT_COMPRESS_BLOCK_CURL !== "0";
6845
- }
6846
- if (process.env.CONTEXT_COMPRESS_BLOCK_WEBFETCH !== void 0) {
6847
- partial2.blockWebFetch = process.env.CONTEXT_COMPRESS_BLOCK_WEBFETCH !== "0";
6848
- }
6849
- if (process.env.CONTEXT_COMPRESS_NUDGE_READ !== void 0) {
6850
- partial2.nudgeOnRead = process.env.CONTEXT_COMPRESS_NUDGE_READ !== "0";
6851
- }
6852
- if (process.env.CONTEXT_COMPRESS_NUDGE_GREP !== void 0) {
6853
- partial2.nudgeOnGrep = process.env.CONTEXT_COMPRESS_NUDGE_GREP !== "0";
6854
- }
6855
- return partial2;
6856
- }
6857
- var _config = null;
6858
- function loadConfig(projectDir) {
6859
- if (_config) return _config;
6860
- const fileConfig = loadFileConfig(projectDir);
6861
- const envConfig = loadEnvConfig();
6862
- _config = { ...DEFAULTS, ...fileConfig, ...envConfig };
6863
- return _config;
6864
- }
6865
- function getConfig() {
6866
- if (!_config) return loadConfig();
6867
- return _config;
6868
- }
6869
-
6870
- // src/logger.ts
6871
- function debug(...args) {
6872
- if (getConfig().debug) {
6873
- process.stderr.write(`[context-compress] ${args.map(String).join(" ")}
6874
- `);
6875
- }
6876
- }
6877
-
6878
- // src/server.ts
6879
- import { readFileSync as readFileSync2 } from "node:fs";
6880
- import { dirname, join as join4, resolve } from "node:path";
6881
- import { fileURLToPath } from "node:url";
6882
6805
 
6883
6806
  // node_modules/zod/v3/external.js
6884
6807
  var external_exports = {};
@@ -10921,6 +10844,163 @@ var coerce = {
10921
10844
  };
10922
10845
  var NEVER = INVALID;
10923
10846
 
10847
+ // src/config.ts
10848
+ var DEFAULTS = {
10849
+ passthroughEnvVars: [],
10850
+ debug: false,
10851
+ blockCurl: true,
10852
+ blockWebFetch: true,
10853
+ nudgeOnRead: true,
10854
+ nudgeOnGrep: true,
10855
+ intentSearchThreshold: 5e3,
10856
+ maxOutputBytes: 102400,
10857
+ hardCapBytes: 100 * 1024 * 1024,
10858
+ searchMaxBytes: 40960,
10859
+ batchMaxBytes: 81920,
10860
+ searchLimit: 3,
10861
+ searchWindowMs: 6e4,
10862
+ searchReduceAfter: 3,
10863
+ searchBlockAfter: 8,
10864
+ compressionLevel: "normal"
10865
+ };
10866
+ var LEVEL_OVERRIDES = {
10867
+ normal: {},
10868
+ compact: {
10869
+ maxOutputBytes: 51200,
10870
+ searchMaxBytes: 20480,
10871
+ batchMaxBytes: 40960,
10872
+ searchLimit: 2,
10873
+ intentSearchThreshold: 3e3
10874
+ },
10875
+ ultra: {
10876
+ maxOutputBytes: 25600,
10877
+ searchMaxBytes: 10240,
10878
+ batchMaxBytes: 20480,
10879
+ searchLimit: 1,
10880
+ intentSearchThreshold: 2e3
10881
+ }
10882
+ };
10883
+ var ConfigSchema = external_exports.object({
10884
+ passthroughEnvVars: external_exports.array(external_exports.string()).optional(),
10885
+ debug: external_exports.boolean().optional(),
10886
+ blockCurl: external_exports.boolean().optional(),
10887
+ blockWebFetch: external_exports.boolean().optional(),
10888
+ nudgeOnRead: external_exports.boolean().optional(),
10889
+ nudgeOnGrep: external_exports.boolean().optional(),
10890
+ intentSearchThreshold: external_exports.number().int().positive().optional(),
10891
+ maxOutputBytes: external_exports.number().int().positive().optional(),
10892
+ hardCapBytes: external_exports.number().int().positive().optional(),
10893
+ searchMaxBytes: external_exports.number().int().positive().optional(),
10894
+ batchMaxBytes: external_exports.number().int().positive().optional(),
10895
+ searchLimit: external_exports.number().int().positive().optional(),
10896
+ searchWindowMs: external_exports.number().int().positive().optional(),
10897
+ searchReduceAfter: external_exports.number().int().nonnegative().optional(),
10898
+ searchBlockAfter: external_exports.number().int().positive().optional(),
10899
+ compressionLevel: external_exports.enum(["normal", "compact", "ultra"]).optional()
10900
+ });
10901
+ function parseIntEnv(key) {
10902
+ const val = process.env[key];
10903
+ if (val === void 0) return void 0;
10904
+ const n = Number.parseInt(val, 10);
10905
+ return Number.isNaN(n) ? void 0 : n;
10906
+ }
10907
+ function loadFileConfig(projectDir2) {
10908
+ const paths = [
10909
+ projectDir2 && join(projectDir2, ".context-compress.json"),
10910
+ join(homedir(), ".context-compress.json")
10911
+ ].filter(Boolean);
10912
+ for (const p of paths) {
10913
+ try {
10914
+ const raw = readFileSync(p, "utf-8");
10915
+ const parsed = JSON.parse(raw);
10916
+ const result = ConfigSchema.safeParse(parsed);
10917
+ if (result.success) {
10918
+ return result.data;
10919
+ }
10920
+ return {};
10921
+ } catch {
10922
+ }
10923
+ }
10924
+ return {};
10925
+ }
10926
+ function loadEnvConfig() {
10927
+ const partial2 = {};
10928
+ if (process.env.CONTEXT_COMPRESS_DEBUG === "1") {
10929
+ partial2.debug = true;
10930
+ }
10931
+ if (process.env.CONTEXT_COMPRESS_PASSTHROUGH_ENV) {
10932
+ partial2.passthroughEnvVars = process.env.CONTEXT_COMPRESS_PASSTHROUGH_ENV.split(",").map((s) => s.trim()).filter(Boolean);
10933
+ }
10934
+ if (process.env.CONTEXT_COMPRESS_BLOCK_CURL !== void 0) {
10935
+ partial2.blockCurl = process.env.CONTEXT_COMPRESS_BLOCK_CURL !== "0";
10936
+ }
10937
+ if (process.env.CONTEXT_COMPRESS_BLOCK_WEBFETCH !== void 0) {
10938
+ partial2.blockWebFetch = process.env.CONTEXT_COMPRESS_BLOCK_WEBFETCH !== "0";
10939
+ }
10940
+ if (process.env.CONTEXT_COMPRESS_NUDGE_READ !== void 0) {
10941
+ partial2.nudgeOnRead = process.env.CONTEXT_COMPRESS_NUDGE_READ !== "0";
10942
+ }
10943
+ if (process.env.CONTEXT_COMPRESS_NUDGE_GREP !== void 0) {
10944
+ partial2.nudgeOnGrep = process.env.CONTEXT_COMPRESS_NUDGE_GREP !== "0";
10945
+ }
10946
+ const maxOutput = parseIntEnv("CONTEXT_COMPRESS_MAX_OUTPUT_BYTES");
10947
+ if (maxOutput !== void 0) partial2.maxOutputBytes = maxOutput;
10948
+ const hardCap = parseIntEnv("CONTEXT_COMPRESS_HARD_CAP_BYTES");
10949
+ if (hardCap !== void 0) partial2.hardCapBytes = hardCap;
10950
+ const searchMax = parseIntEnv("CONTEXT_COMPRESS_SEARCH_MAX_BYTES");
10951
+ if (searchMax !== void 0) partial2.searchMaxBytes = searchMax;
10952
+ const batchMax = parseIntEnv("CONTEXT_COMPRESS_BATCH_MAX_BYTES");
10953
+ if (batchMax !== void 0) partial2.batchMaxBytes = batchMax;
10954
+ const searchLimit = parseIntEnv("CONTEXT_COMPRESS_SEARCH_LIMIT");
10955
+ if (searchLimit !== void 0) partial2.searchLimit = searchLimit;
10956
+ const searchWindow = parseIntEnv("CONTEXT_COMPRESS_SEARCH_WINDOW_MS");
10957
+ if (searchWindow !== void 0) partial2.searchWindowMs = searchWindow;
10958
+ const searchReduce = parseIntEnv("CONTEXT_COMPRESS_SEARCH_REDUCE_AFTER");
10959
+ if (searchReduce !== void 0) partial2.searchReduceAfter = searchReduce;
10960
+ const searchBlock = parseIntEnv("CONTEXT_COMPRESS_SEARCH_BLOCK_AFTER");
10961
+ if (searchBlock !== void 0) partial2.searchBlockAfter = searchBlock;
10962
+ const intentThreshold = parseIntEnv("CONTEXT_COMPRESS_INTENT_SEARCH_THRESHOLD");
10963
+ if (intentThreshold !== void 0) partial2.intentSearchThreshold = intentThreshold;
10964
+ const level = process.env.CONTEXT_COMPRESS_LEVEL;
10965
+ if (level === "normal" || level === "compact" || level === "ultra") {
10966
+ partial2.compressionLevel = level;
10967
+ }
10968
+ return partial2;
10969
+ }
10970
+ var _config = null;
10971
+ function loadConfig(projectDir2) {
10972
+ if (_config) return _config;
10973
+ const fileConfig = loadFileConfig(projectDir2);
10974
+ const envConfig = loadEnvConfig();
10975
+ const merged = { ...DEFAULTS, ...fileConfig, ...envConfig };
10976
+ const levelOverrides = LEVEL_OVERRIDES[merged.compressionLevel];
10977
+ for (const [key, value] of Object.entries(levelOverrides)) {
10978
+ const k = key;
10979
+ if (!(k in fileConfig) && !(k in envConfig)) {
10980
+ merged[k] = value;
10981
+ }
10982
+ }
10983
+ _config = merged;
10984
+ return _config;
10985
+ }
10986
+ function getConfig() {
10987
+ if (!_config) return loadConfig();
10988
+ return _config;
10989
+ }
10990
+
10991
+ // src/logger.ts
10992
+ function debug(...args) {
10993
+ if (getConfig().debug) {
10994
+ process.stderr.write(`[context-compress] ${args.map(String).join(" ")}
10995
+ `);
10996
+ }
10997
+ }
10998
+
10999
+ // src/server.ts
11000
+ import { readFileSync as readFileSync2, statSync } from "node:fs";
11001
+ import { dirname, join as join4, resolve } from "node:path";
11002
+ import { fileURLToPath } from "node:url";
11003
+
10924
11004
  // node_modules/zod/v4/core/core.js
10925
11005
  var NEVER2 = Object.freeze({
10926
11006
  status: "aborted"
@@ -21073,7 +21153,6 @@ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
21073
21153
  import { tmpdir } from "node:os";
21074
21154
  import { join as join2 } from "node:path";
21075
21155
  var DEFAULT_TIMEOUT = 3e4;
21076
- var DEFAULT_HARD_CAP = 100 * 1024 * 1024;
21077
21156
  var SAFE_ENV_KEYS = [
21078
21157
  "PATH",
21079
21158
  "HOME",
@@ -21126,6 +21205,86 @@ function killProcessTree(pid) {
21126
21205
  }
21127
21206
  }
21128
21207
  }
21208
+ function deduplicateLines(output) {
21209
+ const lines = output.split("\n");
21210
+ if (lines.length < 3) return output;
21211
+ const result = [];
21212
+ let prevLine = lines[0];
21213
+ let count = 1;
21214
+ for (let i = 1; i < lines.length; i++) {
21215
+ if (lines[i] === prevLine && prevLine.trim().length > 0) {
21216
+ count++;
21217
+ } else {
21218
+ if (count > 2) {
21219
+ result.push(prevLine);
21220
+ result.push(` ... (\xD7${count} identical lines)`);
21221
+ } else {
21222
+ for (let j = 0; j < count; j++) result.push(prevLine);
21223
+ }
21224
+ prevLine = lines[i];
21225
+ count = 1;
21226
+ }
21227
+ }
21228
+ if (count > 2) {
21229
+ result.push(prevLine);
21230
+ result.push(` ... (\xD7${count} identical lines)`);
21231
+ } else {
21232
+ for (let j = 0; j < count; j++) result.push(prevLine);
21233
+ }
21234
+ return result.join("\n");
21235
+ }
21236
+ function groupErrorLines(output) {
21237
+ const lines = output.split("\n");
21238
+ if (lines.length < 5) return output;
21239
+ const ERROR_RE = /^(.*?(?:error|warning|Error|Warning|ERR|WARN)[:\s])\s*(.+?)(?:\s+(?:at|in|on)\s+(?:line\s+)?(\d+))?$/i;
21240
+ const errorGroups = /* @__PURE__ */ new Map();
21241
+ const resultLines = [];
21242
+ let groupedCount = 0;
21243
+ for (const line of lines) {
21244
+ const match = line.match(ERROR_RE);
21245
+ if (match) {
21246
+ const prefix = match[1].trim();
21247
+ const msg = match[2].trim();
21248
+ const key = `${prefix}|${msg}`;
21249
+ const existing = errorGroups.get(key);
21250
+ if (existing) {
21251
+ existing.count++;
21252
+ if (match[3]) existing.locations.push(match[3]);
21253
+ groupedCount++;
21254
+ continue;
21255
+ }
21256
+ errorGroups.set(key, {
21257
+ message: `${prefix} ${msg}`,
21258
+ locations: match[3] ? [match[3]] : [],
21259
+ count: 1
21260
+ });
21261
+ groupedCount++;
21262
+ continue;
21263
+ }
21264
+ resultLines.push(line);
21265
+ }
21266
+ if (groupedCount < 4 || errorGroups.size === groupedCount) return output;
21267
+ const grouped = [];
21268
+ for (const [, group] of errorGroups) {
21269
+ if (group.count === 1) {
21270
+ grouped.push(
21271
+ group.message + (group.locations.length ? ` at line ${group.locations[0]}` : "")
21272
+ );
21273
+ } else {
21274
+ let line = `${group.message} (\xD7${group.count})`;
21275
+ if (group.locations.length > 0) {
21276
+ line += ` [lines: ${group.locations.join(", ")}]`;
21277
+ }
21278
+ grouped.push(line);
21279
+ }
21280
+ }
21281
+ if (grouped.length > 0) {
21282
+ resultLines.push("");
21283
+ resultLines.push(`\u2500\u2500 Grouped errors/warnings (${groupedCount} \u2192 ${errorGroups.size}) \u2500\u2500`);
21284
+ resultLines.push(...grouped);
21285
+ }
21286
+ return resultLines.join("\n");
21287
+ }
21129
21288
  function smartTruncate(output, maxBytes) {
21130
21289
  if (Buffer.byteLength(output) <= maxBytes) return output;
21131
21290
  const lines = output.split("\n");
@@ -21153,11 +21312,11 @@ function smartTruncate(output, maxBytes) {
21153
21312
  const truncatedLines = lines.length - headEnd - (lines.length - tailStart);
21154
21313
  const truncatedBytes = Buffer.byteLength(output) - headBytes - tailBytes;
21155
21314
  const separator = `
21156
- ... [${truncatedLines} lines / ${formatSize(truncatedBytes)} truncated \u2014 showing first ${headEnd} + last ${lines.length - tailStart} lines] ...
21315
+ ... [${truncatedLines} lines / ${formatBytes(truncatedBytes)} truncated \u2014 showing first ${headEnd} + last ${lines.length - tailStart} lines] ...
21157
21316
  `;
21158
21317
  return headLines.join("\n") + separator + tailLines.join("\n");
21159
21318
  }
21160
- function formatSize(bytes) {
21319
+ function formatBytes(bytes) {
21161
21320
  if (bytes < 1024) return `${bytes}B`;
21162
21321
  if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
21163
21322
  return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
@@ -21231,7 +21390,7 @@ var SubprocessExecutor = class {
21231
21390
  plugin.needsShell
21232
21391
  );
21233
21392
  } finally {
21234
- setTimeout(() => this.cleanupTempDir(tmpDir), 100);
21393
+ setTimeout(() => this.cleanupTempDir(tmpDir), 100).unref();
21235
21394
  }
21236
21395
  }
21237
21396
  /**
@@ -21258,11 +21417,12 @@ var SubprocessExecutor = class {
21258
21417
  spawnAndCapture(cmd, args, cwd, timeout, maxOutput, useShell) {
21259
21418
  return new Promise((resolve2) => {
21260
21419
  const hardCap = this.config.hardCapBytes;
21261
- let stdoutBuf = Buffer.alloc(0);
21262
- let stderrBuf = Buffer.alloc(0);
21420
+ const stdoutChunks = [];
21421
+ const stderrChunks = [];
21263
21422
  let totalBytes = 0;
21264
21423
  let killed = false;
21265
21424
  let networkBytes;
21425
+ let resolved = false;
21266
21426
  const proc = spawn(cmd, args, {
21267
21427
  cwd,
21268
21428
  env: { ...this.env, TMPDIR: cwd },
@@ -21278,7 +21438,7 @@ var SubprocessExecutor = class {
21278
21438
  if (proc.pid) killProcessTree(proc.pid);
21279
21439
  return;
21280
21440
  }
21281
- stdoutBuf = Buffer.concat([stdoutBuf, chunk]);
21441
+ stdoutChunks.push(chunk);
21282
21442
  });
21283
21443
  proc.stderr?.on("data", (chunk) => {
21284
21444
  totalBytes += chunk.length;
@@ -21287,14 +21447,26 @@ var SubprocessExecutor = class {
21287
21447
  if (proc.pid) killProcessTree(proc.pid);
21288
21448
  return;
21289
21449
  }
21290
- stderrBuf = Buffer.concat([stderrBuf, chunk]);
21450
+ stderrChunks.push(chunk);
21291
21451
  });
21292
21452
  proc.on("error", (err) => {
21293
21453
  debug("Process error:", err.message);
21454
+ if (!resolved) {
21455
+ resolved = true;
21456
+ resolve2({
21457
+ stdout: "",
21458
+ stderr: err.message,
21459
+ exitCode: 1,
21460
+ truncated: false,
21461
+ killed: false
21462
+ });
21463
+ }
21294
21464
  });
21295
21465
  proc.on("close", (code) => {
21296
- let stdout = stdoutBuf.toString("utf-8");
21297
- let stderr = stderrBuf.toString("utf-8");
21466
+ if (resolved) return;
21467
+ resolved = true;
21468
+ let stdout = Buffer.concat(stdoutChunks).toString("utf-8");
21469
+ let stderr = Buffer.concat(stderrChunks).toString("utf-8");
21298
21470
  const netMatch = stderr.match(/__CM_NET__:(\d+)/);
21299
21471
  if (netMatch) {
21300
21472
  networkBytes = Number.parseInt(netMatch[1], 10);
@@ -21302,8 +21474,10 @@ var SubprocessExecutor = class {
21302
21474
  }
21303
21475
  if (killed) {
21304
21476
  stdout += `
21305
- [output capped at ${formatSize(hardCap)} \u2014 process killed]`;
21477
+ [output capped at ${formatBytes(hardCap)} \u2014 process killed]`;
21306
21478
  }
21479
+ stdout = deduplicateLines(stdout);
21480
+ stdout = groupErrorLines(stdout);
21307
21481
  const truncated = Buffer.byteLength(stdout) > maxOutput;
21308
21482
  if (truncated) {
21309
21483
  stdout = smartTruncate(stdout, maxOutput);
@@ -21333,7 +21507,7 @@ var SubprocessExecutor = class {
21333
21507
  }
21334
21508
  };
21335
21509
  function wrapWithNetworkTracking(code) {
21336
- const preamble = "let __cm_net=0;const __cm_f=globalThis.fetch;if(__cm_f){globalThis.fetch=async(...a)=>{const r=await __cm_f(...a);try{const cl=r.clone();const b=await cl.arrayBuffer();__cm_net+=b.byteLength}catch{}return r};}";
21510
+ const preamble = "let __cm_net=0;const __cm_f=globalThis.fetch;if(__cm_f){globalThis.fetch=async(...a)=>{const r=await __cm_f(...a);try{const cl=r.headers.get('content-length');if(cl){__cm_net+=parseInt(cl,10)}else{const b=await r.clone().arrayBuffer();__cm_net+=b.byteLength}}catch{}return r};}";
21337
21511
  const epilogue = `
21338
21512
  process.stderr.write('__CM_NET__:'+__cm_net+'\\n');`;
21339
21513
  return `${preamble}
@@ -21341,6 +21515,25 @@ async function __cm_main(){${code}}
21341
21515
  __cm_main().then(()=>{${epilogue}}).catch(e=>{console.error(e);${epilogue}process.exit(1)});`;
21342
21516
  }
21343
21517
 
21518
+ // src/network.ts
21519
+ function isPrivateHost(hostname2) {
21520
+ const h = hostname2.startsWith("[") && hostname2.endsWith("]") ? hostname2.slice(1, -1) : hostname2;
21521
+ const lower = h.toLowerCase();
21522
+ if (lower === "localhost" || lower === "0.0.0.0") return true;
21523
+ if (/^127\./.test(h)) return true;
21524
+ if (/^10\./.test(h)) return true;
21525
+ if (/^172\.(1[6-9]|2\d|3[01])\./.test(h)) return true;
21526
+ if (/^192\.168\./.test(h)) return true;
21527
+ if (/^169\.254\./.test(h)) return true;
21528
+ if (/^100\.(6[4-9]|[7-9]\d|1[01]\d|12[0-7])\./.test(h)) return true;
21529
+ if (lower === "::1") return true;
21530
+ const mappedMatch = lower.match(/^::ffff:(\d+\.\d+\.\d+\.\d+)$/);
21531
+ if (mappedMatch) return isPrivateHost(mappedMatch[1]);
21532
+ if (/^fe[89ab]/i.test(h)) return true;
21533
+ if (/^f[cd]/i.test(h)) return true;
21534
+ return false;
21535
+ }
21536
+
21344
21537
  // src/runtime/index.ts
21345
21538
  import { exec } from "node:child_process";
21346
21539
  import { promisify } from "node:util";
@@ -21372,8 +21565,8 @@ var goPlugin = {
21372
21565
  return [runtime, "run", filePath];
21373
21566
  },
21374
21567
  preprocessCode(code) {
21375
- if (!code.includes("package ")) {
21376
- const hasImport = code.includes("import ");
21568
+ if (!/^package\s/m.test(code)) {
21569
+ const hasImport = /^import\s/m.test(code);
21377
21570
  if (hasImport) {
21378
21571
  return `package main
21379
21572
 
@@ -21392,7 +21585,7 @@ _ = fmt.Sprintf("")
21392
21585
  },
21393
21586
  wrapWithFileContent(code, filePath) {
21394
21587
  const escaped = JSON.stringify(filePath);
21395
- const hasPackage = code.includes("package ");
21588
+ const hasPackage = /^package\s/m.test(code);
21396
21589
  if (hasPackage) {
21397
21590
  return code.replace(
21398
21591
  /(package\s+\w+\n)/,
@@ -21433,8 +21626,9 @@ var javascriptPlugin = {
21433
21626
  },
21434
21627
  wrapWithFileContent(code, filePath) {
21435
21628
  const escaped = JSON.stringify(filePath);
21436
- return `const FILE_CONTENT_PATH = ${escaped};
21437
- const FILE_CONTENT = require("fs").readFileSync(FILE_CONTENT_PATH, "utf-8");
21629
+ return `const {readFileSync: __cm_readFileSync} = await import("node:fs");
21630
+ const FILE_CONTENT_PATH = ${escaped};
21631
+ const FILE_CONTENT = __cm_readFileSync(FILE_CONTENT_PATH, "utf-8");
21438
21632
  ${code}`;
21439
21633
  }
21440
21634
  };
@@ -21506,7 +21700,7 @@ ${code}`;
21506
21700
  // src/runtime/languages/r.ts
21507
21701
  var rPlugin = {
21508
21702
  language: "r",
21509
- runtimeCandidates: ["Rscript", "r"],
21703
+ runtimeCandidates: ["Rscript", "R"],
21510
21704
  fileExtension: ".R",
21511
21705
  buildCommand(runtime, filePath) {
21512
21706
  return [runtime, filePath];
@@ -21549,7 +21743,7 @@ var rustPlugin = {
21549
21743
  return [runtime, srcPath, "-o", binPath];
21550
21744
  },
21551
21745
  preprocessCode(code) {
21552
- if (!code.includes("fn main")) {
21746
+ if (!/^fn\s+main\s*\(/m.test(code)) {
21553
21747
  return `fn main() {
21554
21748
  ${code}
21555
21749
  }`;
@@ -21562,7 +21756,7 @@ ${code}
21562
21756
  let file_content_path = ${escaped};
21563
21757
  let file_content = fs::read_to_string(file_content_path).unwrap();
21564
21758
  `;
21565
- if (code.includes("fn main")) {
21759
+ if (/^fn\s+main\s*\(/m.test(code)) {
21566
21760
  return code.replace(/fn main\s*\(\s*\)\s*\{/, `fn main() {
21567
21761
  ${preamble}`);
21568
21762
  }
@@ -21598,8 +21792,9 @@ var typescriptPlugin = {
21598
21792
  },
21599
21793
  wrapWithFileContent(code, filePath) {
21600
21794
  const escaped = JSON.stringify(filePath);
21601
- return `const FILE_CONTENT_PATH = ${escaped};
21602
- const FILE_CONTENT = require("fs").readFileSync(FILE_CONTENT_PATH, "utf-8");
21795
+ return `const {readFileSync: __cm_readFileSync} = await import("node:fs");
21796
+ const FILE_CONTENT_PATH = ${escaped};
21797
+ const FILE_CONTENT = __cm_readFileSync(FILE_CONTENT_PATH, "utf-8");
21603
21798
  ${code}`;
21604
21799
  },
21605
21800
  // tsx and ts-node may be .cmd shims on Windows
@@ -21655,6 +21850,16 @@ function hasBun(runtimes) {
21655
21850
  }
21656
21851
 
21657
21852
  // src/stats.ts
21853
+ var BAR_WIDTH = 20;
21854
+ function asciiBar(ratio, width = BAR_WIDTH) {
21855
+ const filled = Math.round(ratio * width);
21856
+ const empty = width - filled;
21857
+ return `[${"\u2588".repeat(filled)}${"\u2591".repeat(empty)}] ${(ratio * 100).toFixed(0)}%`;
21858
+ }
21859
+ function tokenCost(tokens) {
21860
+ const cost = tokens / 1e6 * 3;
21861
+ return cost >= 0.01 ? `~$${cost.toFixed(2)}` : "<$0.01";
21862
+ }
21658
21863
  var SessionTracker = class {
21659
21864
  stats = {
21660
21865
  calls: {},
@@ -21688,6 +21893,7 @@ var SessionTracker = class {
21688
21893
  const savingsRatio = totalReturned > 0 ? totalProcessed / totalReturned : 1;
21689
21894
  const reductionPct = totalProcessed > 0 ? ((1 - totalReturned / totalProcessed) * 100).toFixed(1) : "0.0";
21690
21895
  const estTokens = Math.round(totalReturned / 4);
21896
+ const estTokensAvoided = Math.round(keptOut / 4);
21691
21897
  const lines = [];
21692
21898
  lines.push("## Session Statistics\n");
21693
21899
  lines.push("| Metric | Value |");
@@ -21697,18 +21903,31 @@ var SessionTracker = class {
21697
21903
  lines.push(`| Total data processed | ${formatBytes(totalProcessed)} |`);
21698
21904
  lines.push(`| Kept in sandbox | ${formatBytes(keptOut)} |`);
21699
21905
  lines.push(`| Context consumed | ${formatBytes(totalReturned)} |`);
21700
- lines.push(`| Est. tokens | ~${estTokens.toLocaleString()} |`);
21906
+ lines.push(`| Est. tokens used | ~${estTokens.toLocaleString()} (${tokenCost(estTokens)}) |`);
21907
+ lines.push(
21908
+ `| Est. tokens saved | ~${estTokensAvoided.toLocaleString()} (${tokenCost(estTokensAvoided)}) |`
21909
+ );
21701
21910
  lines.push(
21702
21911
  `| **Savings ratio** | **${savingsRatio.toFixed(1)}x** (${reductionPct}% reduction) |`
21703
21912
  );
21913
+ if (totalProcessed > 0) {
21914
+ const savingsBar = asciiBar(keptOut / totalProcessed);
21915
+ lines.push(`
21916
+ **Context savings:** ${savingsBar}`);
21917
+ lines.push(
21918
+ ` Sandbox: ${formatBytes(keptOut)} kept out | Context: ${formatBytes(totalReturned)} entered`
21919
+ );
21920
+ }
21704
21921
  if (totalCalls > 0) {
21705
21922
  lines.push("\n## Per-Tool Breakdown\n");
21706
- lines.push("| Tool | Calls | Context bytes | Est. tokens |");
21707
- lines.push("|------|-------|--------------|-------------|");
21923
+ const maxBytes = Math.max(...Object.values(snap.bytesReturned));
21708
21924
  for (const [name, calls] of Object.entries(snap.calls)) {
21709
21925
  const bytes = snap.bytesReturned[name] ?? 0;
21926
+ const tokens = Math.round(bytes / 4);
21927
+ const barRatio = maxBytes > 0 ? bytes / maxBytes : 0;
21928
+ const bar = "\u2588".repeat(Math.max(1, Math.round(barRatio * 15)));
21710
21929
  lines.push(
21711
- `| ${name} | ${calls} | ${formatBytes(bytes)} | ~${Math.round(bytes / 4).toLocaleString()} |`
21930
+ ` ${name.padEnd(16)} ${String(calls).padStart(3)} calls ${bar} ${formatBytes(bytes)} (~${tokens.toLocaleString()} tok)`
21712
21931
  );
21713
21932
  }
21714
21933
  }
@@ -21719,11 +21938,6 @@ Context-compress kept ${formatBytes(keptOut)} out of context (${reductionPct}% s
21719
21938
  return lines.join("\n");
21720
21939
  }
21721
21940
  };
21722
- function formatBytes(bytes) {
21723
- if (bytes < 1024) return `${bytes}B`;
21724
- if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
21725
- return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
21726
- }
21727
21941
 
21728
21942
  // src/store.ts
21729
21943
  import { readdirSync, unlinkSync } from "node:fs";
@@ -21754,14 +21968,7 @@ function positionsFromHighlight(highlighted) {
21754
21968
  return positions;
21755
21969
  }
21756
21970
  function stripMarkers(text) {
21757
- let result = "";
21758
- for (let i = 0; i < text.length; i++) {
21759
- const ch = text[i];
21760
- if (ch !== STX && ch !== ETX) {
21761
- result += ch;
21762
- }
21763
- }
21764
- return result;
21971
+ return text.replaceAll(STX, "").replaceAll(ETX, "");
21765
21972
  }
21766
21973
  function extractSnippet(highlighted, maxLen = DEFAULT_MAX_LEN) {
21767
21974
  const positions = positionsFromHighlight(highlighted);
@@ -21916,7 +22123,7 @@ var WORD_SPLIT_RE = /[^\p{L}\p{N}_-]+/u;
21916
22123
  function sanitizeQuery(raw) {
21917
22124
  const q = raw.replace(FTS_SPECIAL_RE, " ").replace(FTS_OPERATORS_RE, " ").trim();
21918
22125
  const words = q.split(/\s+/).filter((w) => w.length >= 2).map((w) => `"${w}"`);
21919
- return words.length > 0 ? words.join(" OR ") : '""';
22126
+ return words.length > 0 ? words.join(" OR ") : "";
21920
22127
  }
21921
22128
  function levenshtein(a, b) {
21922
22129
  if (a.length === 0) return b.length;
@@ -21936,6 +22143,11 @@ function levenshtein(a, b) {
21936
22143
  var ContentStore = class {
21937
22144
  db;
21938
22145
  hasTrigramTable = false;
22146
+ // Cached prepared statements (initialized in initSchema, always available after constructor)
22147
+ insertSourceStmt;
22148
+ insertChunkStmt;
22149
+ vocabCountStmt;
22150
+ vocabInsertStmt;
21939
22151
  constructor(dbPath) {
21940
22152
  const path = dbPath ?? join3(tmpdir2(), `context-compress-${process.pid}.db`);
21941
22153
  this.db = new Database(path);
@@ -21965,6 +22177,14 @@ var ContentStore = class {
21965
22177
  word TEXT PRIMARY KEY
21966
22178
  );
21967
22179
  `);
22180
+ this.insertSourceStmt = this.db.prepare(
22181
+ "INSERT INTO sources (label, chunk_count, code_chunk_count) VALUES (?, ?, ?)"
22182
+ );
22183
+ this.insertChunkStmt = this.db.prepare(
22184
+ "INSERT INTO chunks (title, content, source_id, content_type) VALUES (?, ?, ?, ?)"
22185
+ );
22186
+ this.vocabCountStmt = this.db.prepare("SELECT COUNT(*) as cnt FROM vocabulary");
22187
+ this.vocabInsertStmt = this.db.prepare("INSERT OR IGNORE INTO vocabulary (word) VALUES (?)");
21968
22188
  }
21969
22189
  /** Lazily create trigram table only when porter search returns 0 results */
21970
22190
  ensureTrigramTable() {
@@ -21979,16 +22199,9 @@ var ContentStore = class {
21979
22199
  tokenize='trigram'
21980
22200
  );
21981
22201
  `);
21982
- const rows = this.db.prepare("SELECT title, content, source_id, content_type FROM chunks").all();
21983
- const insert = this.db.prepare(
21984
- "INSERT INTO chunks_trigram (title, content, source_id, content_type) VALUES (?, ?, ?, ?)"
22202
+ this.db.exec(
22203
+ "INSERT INTO chunks_trigram (title, content, source_id, content_type) SELECT title, content, source_id, content_type FROM chunks"
21985
22204
  );
21986
- const tx = this.db.transaction(() => {
21987
- for (const row of rows) {
21988
- insert.run(row.title, row.content, row.source_id, row.content_type);
21989
- }
21990
- });
21991
- tx();
21992
22205
  this.hasTrigramTable = true;
21993
22206
  }
21994
22207
  /**
@@ -21997,12 +22210,8 @@ var ContentStore = class {
21997
22210
  index(content, label) {
21998
22211
  const isMarkdown = HEADING_RE.test(content) || content.includes("```") || content.includes("---");
21999
22212
  const chunks = isMarkdown ? chunkMarkdown(content) : chunkPlainText(content);
22000
- const insertSource = this.db.prepare(
22001
- "INSERT INTO sources (label, chunk_count, code_chunk_count) VALUES (?, ?, ?)"
22002
- );
22003
- const insertChunk = this.db.prepare(
22004
- "INSERT INTO chunks (title, content, source_id, content_type) VALUES (?, ?, ?, ?)"
22005
- );
22213
+ const insertSource = this.insertSourceStmt;
22214
+ const insertChunk = this.insertChunkStmt;
22006
22215
  const insertTrigram = this.hasTrigramTable ? this.db.prepare(
22007
22216
  "INSERT INTO chunks_trigram (title, content, source_id, content_type) VALUES (?, ?, ?, ?)"
22008
22217
  ) : null;
@@ -22034,6 +22243,9 @@ var ContentStore = class {
22034
22243
  search(query, options) {
22035
22244
  const limit = options?.limit ?? 3;
22036
22245
  const sanitized = sanitizeQuery(query);
22246
+ if (!sanitized) {
22247
+ return { query, results: [] };
22248
+ }
22037
22249
  let hits = this.porterSearch(sanitized, options?.source, limit);
22038
22250
  if (hits.length > 0) {
22039
22251
  return { query, results: hits };
@@ -22046,9 +22258,11 @@ var ContentStore = class {
22046
22258
  const corrected = this.fuzzyCorrect(query);
22047
22259
  if (corrected && corrected !== query) {
22048
22260
  const correctedSanitized = sanitizeQuery(corrected);
22049
- hits = this.porterSearch(correctedSanitized, options?.source, limit);
22050
- if (hits.length > 0) {
22051
- return { query, results: hits, corrected };
22261
+ if (correctedSanitized) {
22262
+ hits = this.porterSearch(correctedSanitized, options?.source, limit);
22263
+ if (hits.length > 0) {
22264
+ return { query, results: hits, corrected };
22265
+ }
22052
22266
  }
22053
22267
  }
22054
22268
  return { query, results: [] };
@@ -22129,7 +22343,7 @@ var ContentStore = class {
22129
22343
  const maxDist = word.length <= 4 ? 1 : word.length <= 12 ? 2 : 3;
22130
22344
  const minLen = word.length - maxDist;
22131
22345
  const maxLen = word.length + maxDist;
22132
- const candidates = this.db.prepare("SELECT word FROM vocabulary WHERE length(word) BETWEEN ? AND ?").all(minLen, maxLen);
22346
+ const candidates = this.db.prepare("SELECT word FROM vocabulary WHERE length(word) BETWEEN ? AND ? LIMIT 500").all(minLen, maxLen);
22133
22347
  let bestWord = word;
22134
22348
  let bestDist = maxDist + 1;
22135
22349
  for (const { word: candidate } of candidates) {
@@ -22148,11 +22362,11 @@ var ContentStore = class {
22148
22362
  * Update vocabulary table from content (bounded to MAX_VOCABULARY).
22149
22363
  */
22150
22364
  updateVocabulary(content) {
22151
- const currentCount = this.db.prepare("SELECT COUNT(*) as cnt FROM vocabulary").get().cnt;
22365
+ const currentCount = this.vocabCountStmt.get().cnt;
22152
22366
  if (currentCount >= MAX_VOCABULARY) return;
22153
22367
  const words = content.split(WORD_SPLIT_RE).filter((w) => w.length >= 3 && !STOPWORDS.has(w.toLowerCase()));
22154
22368
  const unique = new Set(words.map((w) => w.toLowerCase()));
22155
- const insert = this.db.prepare("INSERT OR IGNORE INTO vocabulary (word) VALUES (?)");
22369
+ const insert = this.vocabInsertStmt;
22156
22370
  let added = 0;
22157
22371
  for (const word of unique) {
22158
22372
  if (currentCount + added >= MAX_VOCABULARY) break;
@@ -22169,7 +22383,7 @@ var ContentStore = class {
22169
22383
  ).get(...sourceId ? [sourceId] : []).cnt;
22170
22384
  if (totalChunks === 0) return [];
22171
22385
  const filter = sourceId ? " WHERE source_id = ?" : "";
22172
- const stmt = this.db.prepare(`SELECT content FROM chunks${filter}`);
22386
+ const stmt = this.db.prepare(`SELECT content FROM chunks${filter} LIMIT 500`);
22173
22387
  const rows = sourceId ? stmt.all(sourceId) : stmt.all();
22174
22388
  const docFreq = /* @__PURE__ */ new Map();
22175
22389
  for (const row of rows) {
@@ -22194,6 +22408,21 @@ var ContentStore = class {
22194
22408
  scored.sort((a, b) => b.score - a.score);
22195
22409
  return scored.slice(0, 40).map((s) => s.word);
22196
22410
  }
22411
+ /**
22412
+ * List all indexed sources with metadata.
22413
+ */
22414
+ listSources() {
22415
+ const rows = this.db.prepare(
22416
+ "SELECT id, label, chunk_count, code_chunk_count, indexed_at FROM sources ORDER BY indexed_at DESC"
22417
+ ).all();
22418
+ return rows.map((row) => ({
22419
+ id: row.id,
22420
+ label: row.label,
22421
+ chunkCount: row.chunk_count,
22422
+ codeChunks: row.code_chunk_count,
22423
+ indexedAt: row.indexed_at
22424
+ }));
22425
+ }
22197
22426
  /**
22198
22427
  * Get store statistics.
22199
22428
  */
@@ -22319,8 +22548,8 @@ function cleanupStaleDbs() {
22319
22548
  return cleaned;
22320
22549
  }
22321
22550
 
22322
- // src/server.ts
22323
- var LANGUAGES = [
22551
+ // src/types.ts
22552
+ var ALL_LANGUAGES = [
22324
22553
  "javascript",
22325
22554
  "typescript",
22326
22555
  "python",
@@ -22333,6 +22562,14 @@ var LANGUAGES = [
22333
22562
  "r",
22334
22563
  "elixir"
22335
22564
  ];
22565
+
22566
+ // src/server.ts
22567
+ var LANGUAGE_ENUM = ALL_LANGUAGES;
22568
+ var projectDir = process.env.CLAUDE_PROJECT_DIR ?? process.cwd();
22569
+ function isWithinProject(absPath) {
22570
+ const normalized = resolve(absPath);
22571
+ return normalized === projectDir || normalized.startsWith(`${projectDir}/`);
22572
+ }
22336
22573
  function getVersion() {
22337
22574
  try {
22338
22575
  const __dirname = dirname(fileURLToPath(import.meta.url));
@@ -22343,6 +22580,18 @@ function getVersion() {
22343
22580
  return "1.0.0";
22344
22581
  }
22345
22582
  }
22583
+ function compactLabel(normal, level) {
22584
+ if (level === "ultra") {
22585
+ return normal.replace(/\*\*/g, "").replace(/Use search\(queries: \[\.\.\.]\) to retrieve.*$/gm, "\u2192 search() for more").replace(/Searchable terms: .+$/gm, "");
22586
+ }
22587
+ if (level === "compact") {
22588
+ return normal.replace(
22589
+ /Use search\(queries: \[\.\.\.]\) to retrieve full content of any section\./,
22590
+ "\u2192 search() for details"
22591
+ );
22592
+ }
22593
+ return normal;
22594
+ }
22346
22595
  async function createServer(config3) {
22347
22596
  const version2 = getVersion();
22348
22597
  debug("Version:", version2);
@@ -22353,6 +22602,15 @@ async function createServer(config3) {
22353
22602
  const executor = new SubprocessExecutor(runtimes, config3);
22354
22603
  const store = new ContentStore();
22355
22604
  const tracker = new SessionTracker();
22605
+ const shutdown = () => {
22606
+ try {
22607
+ store.close();
22608
+ } catch {
22609
+ }
22610
+ };
22611
+ process.on("SIGINT", shutdown);
22612
+ process.on("SIGTERM", shutdown);
22613
+ process.on("beforeExit", shutdown);
22356
22614
  const searchCalls = [];
22357
22615
  const server2 = new McpServer({
22358
22616
  name: "context-compress",
@@ -22360,11 +22618,11 @@ async function createServer(config3) {
22360
22618
  });
22361
22619
  server2.tool(
22362
22620
  "execute",
22363
- `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 20 lines. ${bunDetected ? "(Bun detected \u2014 JS/TS runs 3-5x faster) " : ""}Available: ${LANGUAGES.join(", ")}.
22621
+ `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 20 lines. ${bunDetected ? "(Bun detected \u2014 JS/TS runs 3-5x faster) " : ""}Available: ${ALL_LANGUAGES.join(", ")}.
22364
22622
 
22365
22623
  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.`,
22366
22624
  {
22367
- language: external_exports.enum(LANGUAGES).describe("Runtime language"),
22625
+ language: external_exports.enum(LANGUAGE_ENUM).describe("Runtime language"),
22368
22626
  code: external_exports.string().describe(
22369
22627
  "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."
22370
22628
  ),
@@ -22399,13 +22657,13 @@ ${result.stderr}`;
22399
22657
  filtered += ` - **${hit.title}**: ${hit.snippet.slice(0, 200)}
22400
22658
  `;
22401
22659
  }
22402
- if (terms.length > 0) {
22660
+ if (terms.length > 0 && config3.compressionLevel !== "ultra") {
22403
22661
  filtered += `
22404
22662
  Searchable terms: ${terms.join(", ")}
22405
22663
  `;
22406
22664
  }
22407
22665
  filtered += "\nUse search(queries: [...]) to retrieve full content of any section.";
22408
- output = filtered;
22666
+ output = compactLabel(filtered, config3.compressionLevel);
22409
22667
  }
22410
22668
  const responseBytes = Buffer.byteLength(output);
22411
22669
  tracker.trackCall("execute", responseBytes);
@@ -22417,7 +22675,7 @@ Searchable terms: ${terms.join(", ")}
22417
22675
  "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.",
22418
22676
  {
22419
22677
  path: external_exports.string().describe("Absolute file path or relative to project root"),
22420
- language: external_exports.enum(LANGUAGES).describe("Runtime language"),
22678
+ language: external_exports.enum(LANGUAGE_ENUM).describe("Runtime language"),
22421
22679
  code: external_exports.string().describe(
22422
22680
  "Code to process FILE_CONTENT. Print summary via console.log/print/echo/IO.puts."
22423
22681
  ),
@@ -22425,7 +22683,17 @@ Searchable terms: ${terms.join(", ")}
22425
22683
  timeout: external_exports.number().default(3e4).describe("Max execution time in ms")
22426
22684
  },
22427
22685
  async ({ path: filePath, language, code, intent, timeout }) => {
22428
- const absPath = resolve(process.env.CLAUDE_PROJECT_DIR ?? process.cwd(), filePath);
22686
+ const absPath = resolve(projectDir, filePath);
22687
+ if (!isWithinProject(absPath)) {
22688
+ return {
22689
+ content: [
22690
+ {
22691
+ type: "text",
22692
+ text: `Error: path "${filePath}" is outside the project directory`
22693
+ }
22694
+ ]
22695
+ };
22696
+ }
22429
22697
  const result = await executor.executeFile({
22430
22698
  language,
22431
22699
  code,
@@ -22453,13 +22721,13 @@ ${result.stderr}`;
22453
22721
  filtered += ` - **${hit.title}**: ${hit.snippet.slice(0, 200)}
22454
22722
  `;
22455
22723
  }
22456
- if (terms.length > 0) {
22724
+ if (terms.length > 0 && config3.compressionLevel !== "ultra") {
22457
22725
  filtered += `
22458
22726
  Searchable terms: ${terms.join(", ")}
22459
22727
  `;
22460
22728
  }
22461
22729
  filtered += "\nUse search(queries: [...]) to retrieve full content of any section.";
22462
- output = filtered;
22730
+ output = compactLabel(filtered, config3.compressionLevel);
22463
22731
  }
22464
22732
  const responseBytes = Buffer.byteLength(output);
22465
22733
  tracker.trackCall("execute_file", responseBytes);
@@ -22478,7 +22746,28 @@ Searchable terms: ${terms.join(", ")}
22478
22746
  let text;
22479
22747
  let label = source ?? "indexed content";
22480
22748
  if (filePath) {
22481
- const absPath = resolve(process.env.CLAUDE_PROJECT_DIR ?? process.cwd(), filePath);
22749
+ const absPath = resolve(projectDir, filePath);
22750
+ if (!isWithinProject(absPath)) {
22751
+ return {
22752
+ content: [
22753
+ {
22754
+ type: "text",
22755
+ text: `Error: path "${filePath}" is outside the project directory`
22756
+ }
22757
+ ]
22758
+ };
22759
+ }
22760
+ const fileStat = statSync(absPath);
22761
+ if (fileStat.size > 50 * 1024 * 1024) {
22762
+ return {
22763
+ content: [
22764
+ {
22765
+ type: "text",
22766
+ text: `Error: file "${filePath}" is too large (${(fileStat.size / 1024 / 1024).toFixed(1)}MB). Max 50MB.`
22767
+ }
22768
+ ]
22769
+ };
22770
+ }
22482
22771
  text = readFileSync2(absPath, "utf-8");
22483
22772
  label = source ?? filePath;
22484
22773
  } else if (content) {
@@ -22561,6 +22850,25 @@ ${hit.snippet}
22561
22850
  source: external_exports.string().optional().describe("Label for the indexed content")
22562
22851
  },
22563
22852
  async ({ url, source }) => {
22853
+ try {
22854
+ const parsed = new URL(url);
22855
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
22856
+ return {
22857
+ content: [{ type: "text", text: "Error: only http/https URLs are allowed" }]
22858
+ };
22859
+ }
22860
+ if (isPrivateHost(parsed.hostname)) {
22861
+ return {
22862
+ content: [
22863
+ { type: "text", text: "Error: internal/private URLs are not allowed" }
22864
+ ]
22865
+ };
22866
+ }
22867
+ } catch {
22868
+ return {
22869
+ content: [{ type: "text", text: `Error: invalid URL "${url}"` }]
22870
+ };
22871
+ }
22564
22872
  const label = source ?? url;
22565
22873
  const fetchCode = buildFetchCode(url);
22566
22874
  const result = await executor.execute({
@@ -22690,7 +22998,7 @@ Searchable terms: ${terms.join(", ")}`;
22690
22998
  );
22691
22999
  server2.tool(
22692
23000
  "stats",
22693
- "Returns context consumption statistics for the current session. Shows total bytes returned to context, breakdown by tool, call counts, estimated token usage, and context savings ratio.",
23001
+ "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.",
22694
23002
  {},
22695
23003
  async () => {
22696
23004
  const report = tracker.formatReport();
@@ -22698,6 +23006,77 @@ Searchable terms: ${terms.join(", ")}`;
22698
23006
  return { content: [{ type: "text", text: report }] };
22699
23007
  }
22700
23008
  );
23009
+ server2.tool(
23010
+ "discover",
23011
+ "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.",
23012
+ {},
23013
+ async () => {
23014
+ const storeStats = store.getStats();
23015
+ const snap = tracker.getSnapshot();
23016
+ const lines = [];
23017
+ lines.push("## Knowledge Base Discovery\n");
23018
+ if (storeStats.totalSources === 0) {
23019
+ lines.push("No content indexed yet. Use these tools to build the knowledge base:\n");
23020
+ lines.push("- `batch_execute` \u2014 run commands and auto-index output");
23021
+ lines.push("- `execute` with `intent` \u2014 auto-indexes large output");
23022
+ lines.push("- `index` \u2014 index documentation or files");
23023
+ lines.push("- `fetch_and_index` \u2014 fetch and index web pages");
23024
+ } else {
23025
+ lines.push("| Metric | Value |");
23026
+ lines.push("|--------|-------|");
23027
+ lines.push(`| Indexed sources | ${storeStats.totalSources} |`);
23028
+ lines.push(`| Total chunks | ${storeStats.totalChunks} |`);
23029
+ lines.push(`| Vocabulary size | ${storeStats.vocabularySize} |`);
23030
+ lines.push(
23031
+ `| Trigram index | ${storeStats.hasTrigramTable ? "active" : "lazy (not yet needed)"} |`
23032
+ );
23033
+ const sources = store.listSources();
23034
+ if (sources.length > 0) {
23035
+ lines.push("\n### Indexed Sources\n");
23036
+ for (const src of sources) {
23037
+ lines.push(
23038
+ `- **${src.label}** \u2014 ${src.chunkCount} chunks${src.codeChunks > 0 ? ` (${src.codeChunks} with code)` : ""}`
23039
+ );
23040
+ }
23041
+ }
23042
+ const terms = store.getDistinctiveTerms();
23043
+ if (terms.length > 0) {
23044
+ lines.push("\n### Top Searchable Terms\n");
23045
+ lines.push(terms.slice(0, 20).join(", "));
23046
+ }
23047
+ }
23048
+ lines.push("\n### Optimization Suggestions\n");
23049
+ const totalCalls = Object.values(snap.calls).reduce((a, b) => a + b, 0);
23050
+ if (totalCalls === 0) {
23051
+ lines.push("- Start by using `batch_execute` to run multiple commands at once");
23052
+ } else {
23053
+ const searchCalls2 = snap.calls.search ?? 0;
23054
+ const executeCalls = snap.calls.execute ?? 0;
23055
+ const batchCalls = snap.calls.batch_execute ?? 0;
23056
+ if (executeCalls > 3 && batchCalls === 0) {
23057
+ lines.push(
23058
+ "- **Use batch_execute** \u2014 you've made multiple execute calls that could be batched into one"
23059
+ );
23060
+ }
23061
+ if (searchCalls2 > 5) {
23062
+ lines.push("- **Batch your searches** \u2014 pass multiple queries in a single search() call");
23063
+ }
23064
+ if (storeStats.totalChunks > 50) {
23065
+ lines.push(
23066
+ "- **Use source filtering** \u2014 scope searches with `source` parameter for faster, targeted results"
23067
+ );
23068
+ }
23069
+ if (storeStats.totalSources === 0 && totalCalls > 2) {
23070
+ lines.push(
23071
+ "- **Index more content** \u2014 use `intent` parameter in execute calls to auto-index large output"
23072
+ );
23073
+ }
23074
+ }
23075
+ const output = lines.join("\n");
23076
+ tracker.trackCall("discover", Buffer.byteLength(output));
23077
+ return { content: [{ type: "text", text: output }] };
23078
+ }
23079
+ );
22701
23080
  return {
22702
23081
  async start() {
22703
23082
  const transport = new StdioServerTransport();