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.
- package/dist/config.d.ts +4 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +51 -0
- package/dist/config.js.map +1 -1
- package/dist/logger.d.ts +0 -2
- package/dist/logger.d.ts.map +1 -1
- package/dist/logger.js +0 -6
- package/dist/logger.js.map +1 -1
- package/dist/network.d.ts +9 -0
- package/dist/network.d.ts.map +1 -1
- package/dist/network.js +61 -0
- package/dist/network.js.map +1 -1
- package/dist/server.bundle.mjs +316 -142
- package/dist/server.bundle.mjs.map +3 -3
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +162 -55
- package/dist/server.js.map +1 -1
- package/dist/stats.d.ts.map +1 -1
- package/dist/stats.js +24 -11
- package/dist/stats.js.map +1 -1
- package/dist/store.d.ts +6 -1
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +37 -66
- package/dist/store.js.map +1 -1
- package/dist/types.d.ts +0 -19
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/server.bundle.mjs
CHANGED
|
@@ -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
|
|
21861
|
-
|
|
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
|
|
21896
|
-
const
|
|
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
|
|
22016
|
+
`| Est. tokens used | ~${estTokensLo.toLocaleString()}-${estTokensHi.toLocaleString()} tokens (${tokenCost(estTokensMid)}) |`
|
|
21909
22017
|
);
|
|
21910
22018
|
lines.push(
|
|
21911
|
-
`|
|
|
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
|
|
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)} (~${
|
|
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(
|
|
22152
|
-
|
|
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
|
-
|
|
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
|
-
|
|
22278
|
-
|
|
22279
|
-
|
|
22378
|
+
${table}.title,
|
|
22379
|
+
${table}.content,
|
|
22380
|
+
${table}.content_type,
|
|
22280
22381
|
sources.label,
|
|
22281
|
-
bm25(
|
|
22282
|
-
highlight(
|
|
22283
|
-
FROM
|
|
22284
|
-
JOIN sources ON sources.id =
|
|
22285
|
-
WHERE
|
|
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(
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
22571
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
22647
|
-
|
|
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
|
|
22711
|
-
|
|
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
|
-
|
|
22761
|
-
|
|
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:
|
|
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
|
|
22923
|
-
commands.map(async (
|
|
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
|
-
|
|
23090
|
-
|
|
23091
|
-
const
|
|
23092
|
-
const
|
|
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(/&
|
|
23129
|
-
.replace(/</g, "<")
|
|
23299
|
+
md = md.replace(/</g, "<")
|
|
23130
23300
|
.replace(/>/g, ">")
|
|
23131
23301
|
.replace(/"/g, '"')
|
|
23132
23302
|
.replace(/'/g, "'")
|
|
23133
|
-
.replace(/&
|
|
23303
|
+
.replace(/'/g, "'")
|
|
23304
|
+
.replace(/ /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(/&/g, "&");
|
|
23134
23308
|
|
|
23135
23309
|
// Clean whitespace
|
|
23136
23310
|
md = md.replace(/\\n{3,}/g, "\\n\\n").trim();
|