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