context-compress 2026.3.13 → 2026.3.20

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.
@@ -10861,7 +10861,9 @@ var DEFAULTS = {
10861
10861
  searchWindowMs: 6e4,
10862
10862
  searchReduceAfter: 3,
10863
10863
  searchBlockAfter: 8,
10864
- compressionLevel: "normal"
10864
+ compressionLevel: "normal",
10865
+ persistDb: false,
10866
+ dbDir: null
10865
10867
  };
10866
10868
  var LEVEL_OVERRIDES = {
10867
10869
  normal: {},
@@ -10896,7 +10898,9 @@ var ConfigSchema = external_exports.object({
10896
10898
  searchWindowMs: external_exports.number().int().positive().optional(),
10897
10899
  searchReduceAfter: external_exports.number().int().nonnegative().optional(),
10898
10900
  searchBlockAfter: external_exports.number().int().positive().optional(),
10899
- compressionLevel: external_exports.enum(["normal", "compact", "ultra"]).optional()
10901
+ compressionLevel: external_exports.enum(["normal", "compact", "ultra"]).optional(),
10902
+ persistDb: external_exports.boolean().optional(),
10903
+ dbDir: external_exports.string().nullable().optional()
10900
10904
  });
10901
10905
  function parseIntEnv(key) {
10902
10906
  const val = process.env[key];
@@ -10965,6 +10969,12 @@ function loadEnvConfig() {
10965
10969
  if (level === "normal" || level === "compact" || level === "ultra") {
10966
10970
  partial2.compressionLevel = level;
10967
10971
  }
10972
+ if (process.env.CONTEXT_COMPRESS_PERSIST_DB === "1") {
10973
+ partial2.persistDb = true;
10974
+ }
10975
+ if (process.env.CONTEXT_COMPRESS_DB_DIR) {
10976
+ partial2.dbDir = process.env.CONTEXT_COMPRESS_DB_DIR;
10977
+ }
10968
10978
  return partial2;
10969
10979
  }
10970
10980
  var _config = null;
@@ -10980,6 +10990,60 @@ function loadConfig(projectDir2) {
10980
10990
  merged[k] = value;
10981
10991
  }
10982
10992
  }
10993
+ if (merged.maxOutputBytes < 1024) {
10994
+ console.error(
10995
+ `[context-compress] Config: maxOutputBytes clamped from ${merged.maxOutputBytes} to 1024`
10996
+ );
10997
+ merged.maxOutputBytes = 1024;
10998
+ }
10999
+ if (merged.hardCapBytes < merged.maxOutputBytes) {
11000
+ console.error(
11001
+ `[context-compress] Config: hardCapBytes clamped from ${merged.hardCapBytes} to ${merged.maxOutputBytes}`
11002
+ );
11003
+ merged.hardCapBytes = merged.maxOutputBytes;
11004
+ }
11005
+ if (merged.intentSearchThreshold < 0) {
11006
+ console.error(
11007
+ `[context-compress] Config: intentSearchThreshold clamped from ${merged.intentSearchThreshold} to 0`
11008
+ );
11009
+ merged.intentSearchThreshold = 0;
11010
+ }
11011
+ if (merged.searchLimit < 1) {
11012
+ console.error(`[context-compress] Config: searchLimit clamped from ${merged.searchLimit} to 1`);
11013
+ merged.searchLimit = 1;
11014
+ }
11015
+ if (merged.searchWindowMs < 1e3) {
11016
+ console.error(
11017
+ `[context-compress] Config: searchWindowMs clamped from ${merged.searchWindowMs} to 1000`
11018
+ );
11019
+ merged.searchWindowMs = 1e3;
11020
+ }
11021
+ if (merged.searchReduceAfter < 1) {
11022
+ console.error(
11023
+ `[context-compress] Config: searchReduceAfter clamped from ${merged.searchReduceAfter} to 1`
11024
+ );
11025
+ merged.searchReduceAfter = 1;
11026
+ }
11027
+ if (merged.searchBlockAfter < merged.searchReduceAfter + 1) {
11028
+ const minVal = merged.searchReduceAfter + 1;
11029
+ console.error(
11030
+ `[context-compress] Config: searchBlockAfter clamped from ${merged.searchBlockAfter} to ${minVal}`
11031
+ );
11032
+ merged.searchBlockAfter = minVal;
11033
+ }
11034
+ if (merged.searchMaxBytes < 1024) {
11035
+ console.error(
11036
+ `[context-compress] Config: searchMaxBytes clamped from ${merged.searchMaxBytes} to 1024`
11037
+ );
11038
+ merged.searchMaxBytes = 1024;
11039
+ }
11040
+ if (merged.batchMaxBytes < 1024) {
11041
+ console.error(
11042
+ `[context-compress] Config: batchMaxBytes clamped from ${merged.batchMaxBytes} to 1024`
11043
+ );
11044
+ merged.batchMaxBytes = 1024;
11045
+ }
11046
+ if (merged.dbDir) merged.persistDb = true;
10983
11047
  _config = merged;
10984
11048
  return _config;
10985
11049
  }
@@ -10997,7 +11061,7 @@ function debug(...args) {
10997
11061
  }
10998
11062
 
10999
11063
  // src/server.ts
11000
- import { readFileSync as readFileSync2, statSync } from "node:fs";
11064
+ import { readFileSync as readFileSync2, realpathSync, statSync } from "node:fs";
11001
11065
  import { dirname, join as join4, resolve } from "node:path";
11002
11066
  import { fileURLToPath } from "node:url";
11003
11067
 
@@ -21516,10 +21580,12 @@ __cm_main().then(()=>{${epilogue}}).catch(e=>{console.error(e);${epilogue}proces
21516
21580
  }
21517
21581
 
21518
21582
  // src/network.ts
21583
+ import dns from "node:dns";
21519
21584
  function isPrivateHost(hostname2) {
21520
21585
  const h = hostname2.startsWith("[") && hostname2.endsWith("]") ? hostname2.slice(1, -1) : hostname2;
21521
21586
  const lower = h.toLowerCase();
21522
21587
  if (lower === "localhost" || lower === "0.0.0.0") return true;
21588
+ if (/^0\./.test(h)) return true;
21523
21589
  if (/^127\./.test(h)) return true;
21524
21590
  if (/^10\./.test(h)) return true;
21525
21591
  if (/^172\.(1[6-9]|2\d|3[01])\./.test(h)) return true;
@@ -21527,12 +21593,50 @@ function isPrivateHost(hostname2) {
21527
21593
  if (/^169\.254\./.test(h)) return true;
21528
21594
  if (/^100\.(6[4-9]|[7-9]\d|1[01]\d|12[0-7])\./.test(h)) return true;
21529
21595
  if (lower === "::1") return true;
21596
+ if (lower === "::" || lower === "0:0:0:0:0:0:0:0") return true;
21530
21597
  const mappedMatch = lower.match(/^::ffff:(\d+\.\d+\.\d+\.\d+)$/);
21531
21598
  if (mappedMatch) return isPrivateHost(mappedMatch[1]);
21532
21599
  if (/^fe[89ab]/i.test(h)) return true;
21533
21600
  if (/^f[cd]/i.test(h)) return true;
21534
21601
  return false;
21535
21602
  }
21603
+ async function resolveAndValidate(url) {
21604
+ const parsed = new URL(url);
21605
+ const hostname2 = parsed.hostname;
21606
+ if (/^\d+\.\d+\.\d+\.\d+$/.test(hostname2) || hostname2.includes(":")) {
21607
+ if (isPrivateHost(hostname2)) {
21608
+ throw new Error(`Blocked: resolved IP ${hostname2} is a private/internal address`);
21609
+ }
21610
+ return { url, resolvedIp: null };
21611
+ }
21612
+ let resolvedIp = null;
21613
+ let v4Error = false;
21614
+ let v6Error = false;
21615
+ try {
21616
+ const { address } = await dns.promises.lookup(hostname2, { family: 4 });
21617
+ if (isPrivateHost(address)) {
21618
+ throw new Error(`Blocked: ${hostname2} resolved to private IP ${address}`);
21619
+ }
21620
+ resolvedIp = address;
21621
+ } catch (err) {
21622
+ if (err instanceof Error && err.message.startsWith("Blocked:")) throw err;
21623
+ v4Error = true;
21624
+ }
21625
+ try {
21626
+ const { address } = await dns.promises.lookup(hostname2, { family: 6 });
21627
+ if (isPrivateHost(address)) {
21628
+ throw new Error(`Blocked: ${hostname2} resolved to private IPv6 ${address}`);
21629
+ }
21630
+ if (!resolvedIp) resolvedIp = address;
21631
+ } catch (err) {
21632
+ if (err instanceof Error && err.message.startsWith("Blocked:")) throw err;
21633
+ v6Error = true;
21634
+ }
21635
+ if (v4Error && v6Error) {
21636
+ throw new Error(`DNS resolution failed for ${hostname2}: unable to verify host safety`);
21637
+ }
21638
+ return { url, resolvedIp };
21639
+ }
21536
21640
 
21537
21641
  // src/runtime/index.ts
21538
21642
  import { exec } from "node:child_process";
@@ -21857,8 +21961,9 @@ function asciiBar(ratio, width = BAR_WIDTH) {
21857
21961
  return `[${"\u2588".repeat(filled)}${"\u2591".repeat(empty)}] ${(ratio * 100).toFixed(0)}%`;
21858
21962
  }
21859
21963
  function tokenCost(tokens) {
21860
- const cost = tokens / 1e6 * 3;
21861
- return cost >= 0.01 ? `~$${cost.toFixed(2)}` : "<$0.01";
21964
+ const sonnetCost = tokens / 1e6 * 3;
21965
+ if (sonnetCost < 0.01) return "<$0.01";
21966
+ return `~$${sonnetCost.toFixed(2)} (Sonnet)`;
21862
21967
  }
21863
21968
  var SessionTracker = class {
21864
21969
  stats = {
@@ -21890,10 +21995,14 @@ var SessionTracker = class {
21890
21995
  const totalReturned = Object.values(snap.bytesReturned).reduce((a, b) => a + b, 0);
21891
21996
  const keptOut = snap.bytesIndexed + snap.bytesSandboxed;
21892
21997
  const totalProcessed = keptOut + totalReturned;
21893
- const savingsRatio = totalReturned > 0 ? totalProcessed / totalReturned : 1;
21998
+ const savingsRatio = totalReturned > 0 ? totalProcessed / totalReturned : keptOut > 0 ? Number.POSITIVE_INFINITY : 1;
21894
21999
  const reductionPct = totalProcessed > 0 ? ((1 - totalReturned / totalProcessed) * 100).toFixed(1) : "0.0";
21895
- const estTokens = Math.round(totalReturned / 4);
21896
- const estTokensAvoided = Math.round(keptOut / 4);
22000
+ const estTokensLo = Math.round(totalReturned / 5);
22001
+ const estTokensHi = Math.round(totalReturned / 3);
22002
+ const estTokensAvoidedLo = Math.round(keptOut / 5);
22003
+ const estTokensAvoidedHi = Math.round(keptOut / 3);
22004
+ const estTokensMid = Math.round(totalReturned / 4);
22005
+ const estTokensAvoidedMid = Math.round(keptOut / 4);
21897
22006
  const lines = [];
21898
22007
  lines.push("## Session Statistics\n");
21899
22008
  lines.push("| Metric | Value |");
@@ -21903,13 +22012,14 @@ var SessionTracker = class {
21903
22012
  lines.push(`| Total data processed | ${formatBytes(totalProcessed)} |`);
21904
22013
  lines.push(`| Kept in sandbox | ${formatBytes(keptOut)} |`);
21905
22014
  lines.push(`| Context consumed | ${formatBytes(totalReturned)} |`);
21906
- lines.push(`| Est. tokens used | ~${estTokens.toLocaleString()} (${tokenCost(estTokens)}) |`);
21907
22015
  lines.push(
21908
- `| Est. tokens saved | ~${estTokensAvoided.toLocaleString()} (${tokenCost(estTokensAvoided)}) |`
22016
+ `| Est. tokens used | ~${estTokensLo.toLocaleString()}-${estTokensHi.toLocaleString()} tokens (${tokenCost(estTokensMid)}) |`
21909
22017
  );
21910
22018
  lines.push(
21911
- `| **Savings ratio** | **${savingsRatio.toFixed(1)}x** (${reductionPct}% reduction) |`
22019
+ `| Est. tokens saved | ~${estTokensAvoidedLo.toLocaleString()}-${estTokensAvoidedHi.toLocaleString()} tokens (${tokenCost(estTokensAvoidedMid)}) |`
21912
22020
  );
22021
+ const savingsLabel = Number.isFinite(savingsRatio) ? `${savingsRatio.toFixed(1)}x` : "\u221E";
22022
+ lines.push(`| **Savings ratio** | **${savingsLabel}** (${reductionPct}% reduction) |`);
21913
22023
  if (totalProcessed > 0) {
21914
22024
  const savingsBar = asciiBar(keptOut / totalProcessed);
21915
22025
  lines.push(`
@@ -21923,11 +22033,12 @@ var SessionTracker = class {
21923
22033
  const maxBytes = Math.max(...Object.values(snap.bytesReturned));
21924
22034
  for (const [name, calls] of Object.entries(snap.calls)) {
21925
22035
  const bytes = snap.bytesReturned[name] ?? 0;
21926
- const tokens = Math.round(bytes / 4);
22036
+ const tokLo = Math.round(bytes / 5);
22037
+ const tokHi = Math.round(bytes / 3);
21927
22038
  const barRatio = maxBytes > 0 ? bytes / maxBytes : 0;
21928
22039
  const bar = "\u2588".repeat(Math.max(1, Math.round(barRatio * 15)));
21929
22040
  lines.push(
21930
- ` ${name.padEnd(16)} ${String(calls).padStart(3)} calls ${bar} ${formatBytes(bytes)} (~${tokens.toLocaleString()} tok)`
22041
+ ` ${name.padEnd(16)} ${String(calls).padStart(3)} calls ${bar} ${formatBytes(bytes)} (~${tokLo.toLocaleString()}-${tokHi.toLocaleString()} tok)`
21931
22042
  );
21932
22043
  }
21933
22044
  }
@@ -21940,7 +22051,7 @@ Context-compress kept ${formatBytes(keptOut)} out of context (${reductionPct}% s
21940
22051
  };
21941
22052
 
21942
22053
  // src/store.ts
21943
- import { readdirSync, unlinkSync } from "node:fs";
22054
+ import { mkdirSync as mkdirSync2, readdirSync, unlinkSync } from "node:fs";
21944
22055
  import { tmpdir as tmpdir2 } from "node:os";
21945
22056
  import { join as join3 } from "node:path";
21946
22057
  import Database from "better-sqlite3";
@@ -22031,20 +22142,15 @@ var STOPWORDS = /* @__PURE__ */ new Set([
22031
22142
  "how",
22032
22143
  "its",
22033
22144
  "may",
22034
- "new",
22035
22145
  "now",
22036
22146
  "old",
22037
22147
  "see",
22038
22148
  "way",
22039
22149
  "who",
22040
22150
  "did",
22041
- "get",
22042
- "got",
22043
- "let",
22044
22151
  "say",
22045
22152
  "she",
22046
22153
  "too",
22047
- "use",
22048
22154
  "will",
22049
22155
  "with",
22050
22156
  "this",
@@ -22058,7 +22164,6 @@ var STOPWORDS = /* @__PURE__ */ new Set([
22058
22164
  "them",
22059
22165
  "than",
22060
22166
  "each",
22061
- "make",
22062
22167
  "like",
22063
22168
  "just",
22064
22169
  "over",
@@ -22098,21 +22203,7 @@ var STOPWORDS = /* @__PURE__ */ new Set([
22098
22203
  "where",
22099
22204
  "here",
22100
22205
  "were",
22101
- "much",
22102
- "update",
22103
- "updates",
22104
- "updated",
22105
- "deps",
22106
- "dev",
22107
- "tests",
22108
- "test",
22109
- "add",
22110
- "added",
22111
- "fix",
22112
- "fixed",
22113
- "run",
22114
- "running",
22115
- "using"
22206
+ "much"
22116
22207
  ]);
22117
22208
  var HEADING_RE = /^(#{1,4})\s+(.+)$/;
22118
22209
  var SEPARATOR_RE = /^[-_*]{3,}\s*$/;
@@ -22148,8 +22239,18 @@ var ContentStore = class {
22148
22239
  insertChunkStmt;
22149
22240
  vocabCountStmt;
22150
22241
  vocabInsertStmt;
22151
- constructor(dbPath) {
22152
- const path = dbPath ?? join3(tmpdir2(), `context-compress-${process.pid}.db`);
22242
+ constructor(options) {
22243
+ let path;
22244
+ if (typeof options === "string") {
22245
+ path = options;
22246
+ } else if (options?.persistDb || options?.dbDir) {
22247
+ const dir = options.dbDir ?? join3(process.env.CLAUDE_PROJECT_DIR ?? process.cwd(), ".context-compress");
22248
+ mkdirSync2(dir, { recursive: true });
22249
+ path = join3(dir, "store.db");
22250
+ debug("Using persistent DB at", path);
22251
+ } else {
22252
+ path = (typeof options === "object" ? options?.dbPath : void 0) ?? join3(tmpdir2(), `context-compress-${process.pid}.db`);
22253
+ }
22153
22254
  this.db = new Database(path);
22154
22255
  this.db.pragma("journal_mode = WAL");
22155
22256
  this.db.pragma("synchronous = NORMAL");
@@ -22267,22 +22368,22 @@ var ContentStore = class {
22267
22368
  }
22268
22369
  return { query, results: [] };
22269
22370
  }
22270
- porterSearch(sanitized, source, limit) {
22371
+ ftsSearch(table, sanitized, source, limit) {
22271
22372
  const sourceFilter = source ? "AND sources.label LIKE '%' || ? || '%'" : "";
22272
22373
  const params = [sanitized];
22273
22374
  if (source) params.push(source);
22274
22375
  params.push(limit);
22275
22376
  const sql = `
22276
22377
  SELECT
22277
- chunks.title,
22278
- chunks.content,
22279
- chunks.content_type,
22378
+ ${table}.title,
22379
+ ${table}.content,
22380
+ ${table}.content_type,
22280
22381
  sources.label,
22281
- bm25(chunks, 2.0, 1.0) AS rank,
22282
- highlight(chunks, 1, char(2), char(3)) AS highlighted
22283
- FROM chunks
22284
- JOIN sources ON sources.id = chunks.source_id
22285
- WHERE chunks MATCH ? ${sourceFilter}
22382
+ bm25(${table}, 2.0, 1.0) AS rank,
22383
+ highlight(${table}, 1, char(2), char(3)) AS highlighted
22384
+ FROM ${table}
22385
+ JOIN sources ON sources.id = ${table}.source_id
22386
+ WHERE ${table} MATCH ? ${sourceFilter}
22286
22387
  ORDER BY rank
22287
22388
  LIMIT ?
22288
22389
  `;
@@ -22295,41 +22396,15 @@ var ContentStore = class {
22295
22396
  score: Math.abs(row.rank)
22296
22397
  }));
22297
22398
  } catch (e) {
22298
- debug("Porter search error:", e);
22399
+ debug(`FTS search error (${table}):`, e);
22299
22400
  return [];
22300
22401
  }
22301
22402
  }
22403
+ porterSearch(sanitized, source, limit) {
22404
+ return this.ftsSearch("chunks", sanitized, source, limit);
22405
+ }
22302
22406
  trigramSearch(sanitized, source, limit) {
22303
- const sourceFilter = source ? "AND sources.label LIKE '%' || ? || '%'" : "";
22304
- const params = [sanitized];
22305
- if (source) params.push(source);
22306
- params.push(limit);
22307
- const sql = `
22308
- SELECT
22309
- chunks_trigram.title,
22310
- chunks_trigram.content,
22311
- chunks_trigram.content_type,
22312
- sources.label,
22313
- bm25(chunks_trigram, 2.0, 1.0) AS rank,
22314
- highlight(chunks_trigram, 1, char(2), char(3)) AS highlighted
22315
- FROM chunks_trigram
22316
- JOIN sources ON sources.id = chunks_trigram.source_id
22317
- WHERE chunks_trigram MATCH ? ${sourceFilter}
22318
- ORDER BY rank
22319
- LIMIT ?
22320
- `;
22321
- try {
22322
- const rows = this.db.prepare(sql).all(...params);
22323
- return rows.map((row) => ({
22324
- title: row.title,
22325
- snippet: extractSnippet(row.highlighted),
22326
- source: row.label,
22327
- score: Math.abs(row.rank)
22328
- }));
22329
- } catch (e) {
22330
- debug("Trigram search error:", e);
22331
- return [];
22332
- }
22407
+ return this.ftsSearch("chunks_trigram", sanitized, source, limit);
22333
22408
  }
22334
22409
  /**
22335
22410
  * Fuzzy correction using vocabulary + Levenshtein distance.
@@ -22486,6 +22561,10 @@ function chunkMarkdown(content) {
22486
22561
  }
22487
22562
  currentLines.push(line);
22488
22563
  }
22564
+ if (inFence) {
22565
+ debug("Warning: unclosed code fence detected during markdown chunking");
22566
+ hasCode = true;
22567
+ }
22489
22568
  flush();
22490
22569
  return chunks;
22491
22570
  }
@@ -22499,7 +22578,7 @@ function chunkPlainText(content, linesPerChunk = 20, overlap = 2) {
22499
22578
  return {
22500
22579
  title: trimmed.split("\n")[0].slice(0, 80),
22501
22580
  content: trimmed,
22502
- hasCode: FENCE_RE.test(trimmed)
22581
+ hasCode: /`{3,}/.test(trimmed)
22503
22582
  };
22504
22583
  }).filter(Boolean);
22505
22584
  }
@@ -22567,8 +22646,14 @@ var ALL_LANGUAGES = [
22567
22646
  var LANGUAGE_ENUM = ALL_LANGUAGES;
22568
22647
  var projectDir = process.env.CLAUDE_PROJECT_DIR ?? process.cwd();
22569
22648
  function isWithinProject(absPath) {
22570
- const normalized = resolve(absPath);
22571
- return normalized === projectDir || normalized.startsWith(`${projectDir}/`);
22649
+ try {
22650
+ const normalized = realpathSync(resolve(absPath));
22651
+ const realProjectDir = realpathSync(projectDir);
22652
+ return normalized === realProjectDir || normalized.startsWith(`${realProjectDir}/`);
22653
+ } catch {
22654
+ const normalized = resolve(absPath);
22655
+ return normalized === projectDir || normalized.startsWith(`${projectDir}/`);
22656
+ }
22572
22657
  }
22573
22658
  function getVersion() {
22574
22659
  try {
@@ -22592,6 +22677,45 @@ function compactLabel(normal, level) {
22592
22677
  }
22593
22678
  return normal;
22594
22679
  }
22680
+ async function limitConcurrency(tasks, limit) {
22681
+ const results = new Array(tasks.length);
22682
+ let nextIndex = 0;
22683
+ async function runNext() {
22684
+ while (nextIndex < tasks.length) {
22685
+ const index = nextIndex++;
22686
+ try {
22687
+ const value = await tasks[index]();
22688
+ results[index] = { status: "fulfilled", value };
22689
+ } catch (reason) {
22690
+ results[index] = { status: "rejected", reason };
22691
+ }
22692
+ }
22693
+ }
22694
+ const workers = Array.from({ length: Math.min(limit, tasks.length) }, () => runNext());
22695
+ await Promise.all(workers);
22696
+ return results;
22697
+ }
22698
+ function detectInjectionPatterns(content) {
22699
+ const warnings = [];
22700
+ const patterns = [
22701
+ { re: /ignore\s+(all\s+)?previous\s+instructions/i, label: "instruction override" },
22702
+ { re: /you\s+are\s+now\s+/i, label: "role reassignment" },
22703
+ {
22704
+ re: /(?:^|\n)\s*system\s*:\s*(?:you are|you're|as an? )/im,
22705
+ label: "system prompt injection"
22706
+ },
22707
+ { re: /\[INST\]|\[\/INST\]|<\|im_start\|>|<\|im_end\|>/i, label: "chat template injection" },
22708
+ { re: /\n\n(?:Human|Assistant):/m, label: "chat delimiter injection" },
22709
+ { re: /reveal\s+(your|the)\s+(system|secret|confidential)/i, label: "data exfiltration" },
22710
+ { re: /act\s+as\s+(if\s+you\s+are|a)\s+/i, label: "role manipulation" }
22711
+ ];
22712
+ for (const { re, label } of patterns) {
22713
+ if (re.test(content)) {
22714
+ warnings.push(label);
22715
+ }
22716
+ }
22717
+ return warnings;
22718
+ }
22595
22719
  async function createServer(config3) {
22596
22720
  const version2 = getVersion();
22597
22721
  debug("Version:", version2);
@@ -22600,8 +22724,39 @@ async function createServer(config3) {
22600
22724
  const bunDetected = hasBun(runtimes);
22601
22725
  debug("Runtimes detected:", runtimes.size);
22602
22726
  const executor = new SubprocessExecutor(runtimes, config3);
22603
- const store = new ContentStore();
22727
+ let store;
22728
+ let dbFallback = false;
22729
+ try {
22730
+ store = new ContentStore({ persistDb: config3.persistDb, dbDir: config3.dbDir });
22731
+ } catch (e) {
22732
+ debug("Failed to create DB, falling back to in-memory:", e);
22733
+ store = new ContentStore(":memory:");
22734
+ dbFallback = true;
22735
+ }
22604
22736
  const tracker = new SessionTracker();
22737
+ function applyIntentFilter(output, intent, sourceLabel) {
22738
+ if (Buffer.byteLength(output) <= config3.intentSearchThreshold) return output;
22739
+ const indexed = store.index(output, sourceLabel);
22740
+ tracker.trackIndexed(Buffer.byteLength(output));
22741
+ const searchResults = store.search(intent, { limit: 3 });
22742
+ const terms = store.getDistinctiveTerms(indexed.sourceId);
22743
+ let filtered = `Indexed ${indexed.totalChunks} sections from ${sourceLabel}.
22744
+ `;
22745
+ filtered += `${searchResults.results.length} sections matched "${intent}":
22746
+
22747
+ `;
22748
+ for (const hit of searchResults.results) {
22749
+ filtered += ` - **${hit.title}**: ${hit.snippet.slice(0, 200)}
22750
+ `;
22751
+ }
22752
+ if (terms.length > 0 && config3.compressionLevel !== "ultra") {
22753
+ filtered += `
22754
+ Searchable terms: ${terms.join(", ")}
22755
+ `;
22756
+ }
22757
+ filtered += "\nUse search(queries: [...]) to retrieve full content of any section.";
22758
+ return compactLabel(filtered, config3.compressionLevel);
22759
+ }
22605
22760
  const shutdown = () => {
22606
22761
  try {
22607
22762
  store.close();
@@ -22618,7 +22773,7 @@ async function createServer(config3) {
22618
22773
  });
22619
22774
  server2.tool(
22620
22775
  "execute",
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(", ")}.
22776
+ `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(", ")}.
22622
22777
 
22623
22778
  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.`,
22624
22779
  {
@@ -22643,27 +22798,8 @@ PREFER THIS OVER BASH for: API calls (gh, curl, aws), test runners (npm test, py
22643
22798
  STDERR:
22644
22799
  ${result.stderr}`;
22645
22800
  }
22646
- if (intent && Buffer.byteLength(output) > config3.intentSearchThreshold) {
22647
- const indexed = store.index(output, `execute:${language}`);
22648
- tracker.trackIndexed(Buffer.byteLength(output));
22649
- const searchResults = store.search(intent, { limit: 3 });
22650
- const terms = store.getDistinctiveTerms(indexed.sourceId);
22651
- let filtered = `Indexed ${indexed.totalChunks} sections from execute output.
22652
- `;
22653
- filtered += `${searchResults.results.length} sections matched "${intent}":
22654
-
22655
- `;
22656
- for (const hit of searchResults.results) {
22657
- filtered += ` - **${hit.title}**: ${hit.snippet.slice(0, 200)}
22658
- `;
22659
- }
22660
- if (terms.length > 0 && config3.compressionLevel !== "ultra") {
22661
- filtered += `
22662
- Searchable terms: ${terms.join(", ")}
22663
- `;
22664
- }
22665
- filtered += "\nUse search(queries: [...]) to retrieve full content of any section.";
22666
- output = compactLabel(filtered, config3.compressionLevel);
22801
+ if (intent) {
22802
+ output = applyIntentFilter(output, intent, `execute:${language}`);
22667
22803
  }
22668
22804
  const responseBytes = Buffer.byteLength(output);
22669
22805
  tracker.trackCall("execute", responseBytes);
@@ -22707,27 +22843,8 @@ Searchable terms: ${terms.join(", ")}
22707
22843
  STDERR:
22708
22844
  ${result.stderr}`;
22709
22845
  }
22710
- if (intent && Buffer.byteLength(output) > config3.intentSearchThreshold) {
22711
- const indexed = store.index(output, `file:${filePath}`);
22712
- tracker.trackIndexed(Buffer.byteLength(output));
22713
- const searchResults = store.search(intent, { limit: 3 });
22714
- const terms = store.getDistinctiveTerms(indexed.sourceId);
22715
- let filtered = `Indexed ${indexed.totalChunks} sections from "${filePath}" into knowledge base.
22716
- `;
22717
- filtered += `${searchResults.results.length} sections matched "${intent}":
22718
-
22719
- `;
22720
- for (const hit of searchResults.results) {
22721
- filtered += ` - **${hit.title}**: ${hit.snippet.slice(0, 200)}
22722
- `;
22723
- }
22724
- if (terms.length > 0 && config3.compressionLevel !== "ultra") {
22725
- filtered += `
22726
- Searchable terms: ${terms.join(", ")}
22727
- `;
22728
- }
22729
- filtered += "\nUse search(queries: [...]) to retrieve full content of any section.";
22730
- output = compactLabel(filtered, config3.compressionLevel);
22846
+ if (intent) {
22847
+ output = applyIntentFilter(output, intent, `file:${filePath}`);
22731
22848
  }
22732
22849
  const responseBytes = Buffer.byteLength(output);
22733
22850
  tracker.trackCall("execute_file", responseBytes);
@@ -22757,20 +22874,38 @@ Searchable terms: ${terms.join(", ")}
22757
22874
  ]
22758
22875
  };
22759
22876
  }
22760
- const fileStat = statSync(absPath);
22761
- if (fileStat.size > 50 * 1024 * 1024) {
22877
+ try {
22878
+ const fileStat = statSync(absPath);
22879
+ if (fileStat.size > 50 * 1024 * 1024) {
22880
+ return {
22881
+ content: [
22882
+ {
22883
+ type: "text",
22884
+ text: `Error: file "${filePath}" is too large (${(fileStat.size / 1024 / 1024).toFixed(1)}MB). Max 50MB.`
22885
+ }
22886
+ ]
22887
+ };
22888
+ }
22889
+ text = readFileSync2(absPath, "utf-8");
22890
+ label = source ?? filePath;
22891
+ } catch (e) {
22892
+ const msg = e instanceof Error ? e.message : String(e);
22893
+ return {
22894
+ content: [{ type: "text", text: `Error reading "${filePath}": ${msg}` }]
22895
+ };
22896
+ }
22897
+ } else if (content) {
22898
+ const contentBytes = Buffer.byteLength(content);
22899
+ if (contentBytes > 50 * 1024 * 1024) {
22762
22900
  return {
22763
22901
  content: [
22764
22902
  {
22765
22903
  type: "text",
22766
- text: `Error: file "${filePath}" is too large (${(fileStat.size / 1024 / 1024).toFixed(1)}MB). Max 50MB.`
22904
+ text: `Error: content too large (${(contentBytes / 1024 / 1024).toFixed(1)}MB). Max 50MB.`
22767
22905
  }
22768
22906
  ]
22769
22907
  };
22770
22908
  }
22771
- text = readFileSync2(absPath, "utf-8");
22772
- label = source ?? filePath;
22773
- } else if (content) {
22774
22909
  text = content;
22775
22910
  } else {
22776
22911
  return {
@@ -22869,8 +23004,22 @@ ${hit.snippet}
22869
23004
  content: [{ type: "text", text: `Error: invalid URL "${url}"` }]
22870
23005
  };
22871
23006
  }
23007
+ let resolvedIp = null;
23008
+ try {
23009
+ const validated = await resolveAndValidate(url);
23010
+ resolvedIp = validated.resolvedIp;
23011
+ } catch (err) {
23012
+ return {
23013
+ content: [
23014
+ {
23015
+ type: "text",
23016
+ text: `Error: ${err instanceof Error ? err.message : "DNS validation failed"}`
23017
+ }
23018
+ ]
23019
+ };
23020
+ }
22872
23021
  const label = source ?? url;
22873
- const fetchCode = buildFetchCode(url);
23022
+ const fetchCode = buildFetchCode(url, resolvedIp);
22874
23023
  const result = await executor.execute({
22875
23024
  language: "javascript",
22876
23025
  code: fetchCode,
@@ -22883,6 +23032,7 @@ ${hit.snippet}
22883
23032
  }
22884
23033
  const markdown = result.stdout;
22885
23034
  tracker.trackSandboxed(result.networkBytes ?? 0);
23035
+ const injectionWarnings = detectInjectionPatterns(markdown);
22886
23036
  const indexed = store.index(markdown, label);
22887
23037
  tracker.trackIndexed(Buffer.byteLength(markdown));
22888
23038
  const preview = markdown.slice(0, 3072);
@@ -22899,6 +23049,11 @@ ${preview}`;
22899
23049
  Searchable terms: ${terms.join(", ")}`;
22900
23050
  }
22901
23051
  output += "\n\nUse search(queries: [...]) to retrieve full content of any section.";
23052
+ if (injectionWarnings.length > 0) {
23053
+ output += `
23054
+
23055
+ \u26A0 Content safety notice: detected patterns (${injectionWarnings.join(", ")}). Review indexed content before relying on it.`;
23056
+ }
22902
23057
  tracker.trackCall("fetch_and_index", Buffer.byteLength(output));
22903
23058
  return { content: [{ type: "text", text: output }] };
22904
23059
  }
@@ -22919,15 +23074,16 @@ Searchable terms: ${terms.join(", ")}`;
22919
23074
  timeout: external_exports.number().default(6e4).describe("Max execution time in ms (default: 60s)")
22920
23075
  },
22921
23076
  async ({ commands, queries, timeout }) => {
22922
- const commandResults = await Promise.allSettled(
22923
- commands.map(async (cmd) => {
23077
+ const commandResults = await limitConcurrency(
23078
+ commands.map((cmd) => async () => {
22924
23079
  const result = await executor.execute({
22925
23080
  language: "shell",
22926
23081
  code: cmd.command,
22927
23082
  timeout
22928
23083
  });
22929
23084
  return { label: cmd.label, result };
22930
- })
23085
+ }),
23086
+ 4
22931
23087
  );
22932
23088
  let combined = "";
22933
23089
  const inventory = [];
@@ -23072,6 +23228,11 @@ Searchable terms: ${terms.join(", ")}`;
23072
23228
  );
23073
23229
  }
23074
23230
  }
23231
+ if (dbFallback) {
23232
+ lines.push(
23233
+ "\n\u26A0 **Warning:** Persistent DB creation failed \u2014 using in-memory storage. Indexed data will not survive restarts."
23234
+ );
23235
+ }
23075
23236
  const output = lines.join("\n");
23076
23237
  tracker.trackCall("discover", Buffer.byteLength(output));
23077
23238
  return { content: [{ type: "text", text: output }] };
@@ -23085,11 +23246,21 @@ Searchable terms: ${terms.join(", ")}`;
23085
23246
  }
23086
23247
  };
23087
23248
  }
23088
- function buildFetchCode(url) {
23089
- const escaped = JSON.stringify(url);
23090
- return `
23091
- const url = ${escaped};
23092
- const resp = await fetch(url);
23249
+ function buildFetchCode(url, resolvedIp) {
23250
+ let fetchSetup;
23251
+ if (resolvedIp) {
23252
+ const pinnedUrl = new URL(url);
23253
+ const originalHost = pinnedUrl.host;
23254
+ pinnedUrl.hostname = resolvedIp;
23255
+ fetchSetup = `
23256
+ const url = ${JSON.stringify(pinnedUrl.toString())};
23257
+ const resp = await fetch(url, { headers: { 'Host': ${JSON.stringify(originalHost)} }, redirect: 'error' });`;
23258
+ } else {
23259
+ fetchSetup = `
23260
+ const url = ${JSON.stringify(url)};
23261
+ const resp = await fetch(url, { redirect: 'error' });`;
23262
+ }
23263
+ return `${fetchSetup}
23093
23264
  if (!resp.ok) { console.error("HTTP " + resp.status); process.exit(1); }
23094
23265
  const html = await resp.text();
23095
23266
 
@@ -23125,12 +23296,15 @@ md = md.replace(/<br\\s*\\/?>/gi, "\\n");
23125
23296
  md = md.replace(/<[^>]+>/g, "");
23126
23297
 
23127
23298
  // Decode entities
23128
- md = md.replace(/&amp;/g, "&")
23129
- .replace(/&lt;/g, "<")
23299
+ md = md.replace(/&lt;/g, "<")
23130
23300
  .replace(/&gt;/g, ">")
23131
23301
  .replace(/&quot;/g, '"')
23132
23302
  .replace(/&#39;/g, "'")
23133
- .replace(/&nbsp;/g, " ");
23303
+ .replace(/&apos;/g, "'")
23304
+ .replace(/&nbsp;/g, " ")
23305
+ .replace(/&#(\\d+);/g, (_, n) => { const c = parseInt(n, 10); return c > 0 && c <= 0x10FFFF ? String.fromCodePoint(c) : ''; })
23306
+ .replace(/&#x([0-9a-fA-F]+);/g, (_, h) => { const c = parseInt(h, 16); return c > 0 && c <= 0x10FFFF ? String.fromCodePoint(c) : ''; })
23307
+ .replace(/&amp;/g, "&");
23134
23308
 
23135
23309
  // Clean whitespace
23136
23310
  md = md.replace(/\\n{3,}/g, "\\n\\n").trim();