context-compress 2026.3.21 → 2026.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +258 -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 +7 -1
- package/dist/executor.d.ts.map +1 -1
- package/dist/executor.js +51 -4
- package/dist/executor.js.map +1 -1
- package/dist/filters.d.ts +52 -0
- package/dist/filters.d.ts.map +1 -0
- package/dist/filters.js +719 -0
- package/dist/filters.js.map +1 -0
- 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 +1333 -619
- package/dist/server.bundle.mjs.map +4 -4
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +44 -610
- package/dist/server.js.map +1 -1
- package/dist/stats.d.ts +7 -1
- package/dist/stats.d.ts.map +1 -1
- package/dist/stats.js +65 -0
- 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 +11 -0
- 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/token-reduction-report.md +164 -88
- package/hooks/pretooluse.mjs +38 -0
- package/package.json +5 -4
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
|
}
|
|
@@ -21217,6 +21215,510 @@ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
|
21217
21215
|
import { tmpdir } from "node:os";
|
|
21218
21216
|
import { join as join2 } from "node:path";
|
|
21219
21217
|
|
|
21218
|
+
// src/filters.ts
|
|
21219
|
+
var DEFAULT_MODE = "balanced";
|
|
21220
|
+
function applyCommandFilter(code, stdout, mode = DEFAULT_MODE) {
|
|
21221
|
+
if (mode === "conservative") return { output: stdout, filtered: false };
|
|
21222
|
+
const cmd = code.trim().split(/\s+/)[0];
|
|
21223
|
+
const fullCmd = code.trim();
|
|
21224
|
+
if (cmd === "git") return filterGit(fullCmd, stdout, mode);
|
|
21225
|
+
if (cmd === "npm" || cmd === "yarn" || cmd === "pnpm" || cmd === "bun")
|
|
21226
|
+
return filterPackageManager(fullCmd, stdout, mode);
|
|
21227
|
+
if (fullCmd.includes("test") || fullCmd.includes("jest") || fullCmd.includes("vitest") || fullCmd.includes("pytest") || fullCmd.includes("cargo test")) {
|
|
21228
|
+
return filterTestOutput(stdout);
|
|
21229
|
+
}
|
|
21230
|
+
if (cmd === "cargo" || cmd === "make" || cmd === "gradle")
|
|
21231
|
+
return filterBuildOutput(fullCmd, stdout);
|
|
21232
|
+
if (cmd === "docker" || cmd === "kubectl") return filterContainerOutput(fullCmd, stdout);
|
|
21233
|
+
if (cmd === "ls" || cmd === "find" || cmd === "tree")
|
|
21234
|
+
return filterFileList(fullCmd, stdout, mode);
|
|
21235
|
+
if (cmd === "grep" || cmd === "rg" || cmd === "ripgrep") {
|
|
21236
|
+
if (mode === "aggressive") return filterGrep(stdout);
|
|
21237
|
+
}
|
|
21238
|
+
if (mode === "aggressive") {
|
|
21239
|
+
if (cmd === "df") return filterDf(stdout);
|
|
21240
|
+
if (cmd === "du") return filterDu(stdout);
|
|
21241
|
+
if (cmd === "ps") return filterPs(stdout);
|
|
21242
|
+
}
|
|
21243
|
+
return { output: stdout, filtered: false };
|
|
21244
|
+
}
|
|
21245
|
+
function filterGit(cmd, stdout, mode = DEFAULT_MODE) {
|
|
21246
|
+
if (/git\s+(push|pull|fetch|clone)/.test(cmd)) {
|
|
21247
|
+
const lines = stdout.split("\n");
|
|
21248
|
+
const filtered = lines.filter(
|
|
21249
|
+
(l) => !l.startsWith("remote: Counting") && !l.startsWith("remote: Compressing") && !l.startsWith("remote: Total") && !l.includes("Unpacking objects:") && !l.includes("Receiving objects:") && !l.includes("Resolving deltas:") && !/^\s*\d+%/.test(l)
|
|
21250
|
+
);
|
|
21251
|
+
return { output: filtered.join("\n"), filtered: true };
|
|
21252
|
+
}
|
|
21253
|
+
if (/git\s+status/.test(cmd)) {
|
|
21254
|
+
return { output: filterGitStatus(stdout, mode), filtered: true };
|
|
21255
|
+
}
|
|
21256
|
+
if (/git\s+log/.test(cmd) && !cmd.includes("--oneline")) {
|
|
21257
|
+
if (mode === "aggressive") {
|
|
21258
|
+
return { output: aggressiveGitLog(stdout), filtered: true };
|
|
21259
|
+
}
|
|
21260
|
+
if (mode === "balanced") {
|
|
21261
|
+
const truncated = balancedGitLog(stdout);
|
|
21262
|
+
if (truncated.length < stdout.length) {
|
|
21263
|
+
return { output: truncated, filtered: true };
|
|
21264
|
+
}
|
|
21265
|
+
}
|
|
21266
|
+
}
|
|
21267
|
+
if (/git\s+diff/.test(cmd) && mode === "aggressive") {
|
|
21268
|
+
if (/--(stat|name-only|name-status|shortstat|numstat)\b/.test(cmd)) {
|
|
21269
|
+
return { output: stdout, filtered: false };
|
|
21270
|
+
}
|
|
21271
|
+
return { output: aggressiveGitDiff(stdout), filtered: true };
|
|
21272
|
+
}
|
|
21273
|
+
return { output: stdout, filtered: false };
|
|
21274
|
+
}
|
|
21275
|
+
function filterGitStatus(stdout, mode) {
|
|
21276
|
+
const lines = stdout.split("\n");
|
|
21277
|
+
const balanced = lines.filter((l) => !l.startsWith(" (use ") && l.trim() !== "");
|
|
21278
|
+
if (mode !== "aggressive") return balanced.join("\n");
|
|
21279
|
+
const out = [];
|
|
21280
|
+
for (const l of balanced) {
|
|
21281
|
+
if (/^On branch/.test(l)) {
|
|
21282
|
+
out.push(l.replace(/^On branch /, "* "));
|
|
21283
|
+
continue;
|
|
21284
|
+
}
|
|
21285
|
+
if (/^Your branch is/.test(l)) continue;
|
|
21286
|
+
if (/^Changes (to be committed|not staged for commit):/.test(l)) continue;
|
|
21287
|
+
if (/^Untracked files:/.test(l)) {
|
|
21288
|
+
out.push("? Untracked:");
|
|
21289
|
+
continue;
|
|
21290
|
+
}
|
|
21291
|
+
if (/^no changes added to commit/.test(l)) continue;
|
|
21292
|
+
if (/^nothing to commit/.test(l)) {
|
|
21293
|
+
out.push("(clean)");
|
|
21294
|
+
continue;
|
|
21295
|
+
}
|
|
21296
|
+
const m = l.match(/^\s*(modified|new file|deleted|renamed|typechange):\s*(.+)$/);
|
|
21297
|
+
if (m) {
|
|
21298
|
+
const code = { modified: "M", "new file": "A", deleted: "D", renamed: "R", typechange: "T" }[m[1]] ?? "?";
|
|
21299
|
+
out.push(`${code} ${m[2]}`);
|
|
21300
|
+
continue;
|
|
21301
|
+
}
|
|
21302
|
+
out.push(l);
|
|
21303
|
+
}
|
|
21304
|
+
return out.join("\n");
|
|
21305
|
+
}
|
|
21306
|
+
function aggressiveGitLog(stdout) {
|
|
21307
|
+
const lines = stdout.split("\n");
|
|
21308
|
+
const out = [];
|
|
21309
|
+
let sha = "";
|
|
21310
|
+
let author = "";
|
|
21311
|
+
let date3 = "";
|
|
21312
|
+
let subject = "";
|
|
21313
|
+
let inCommit = false;
|
|
21314
|
+
let blanksAfterDate = 0;
|
|
21315
|
+
const flush = () => {
|
|
21316
|
+
if (!sha) return;
|
|
21317
|
+
const reltime = date3 ? ` (${humanReltime(date3)})` : "";
|
|
21318
|
+
const auth = author ? ` <${author.replace(/\s*<.*?>/, "").trim()}>` : "";
|
|
21319
|
+
out.push(`${sha.slice(0, 7)} ${subject}${reltime}${auth}`);
|
|
21320
|
+
};
|
|
21321
|
+
for (const line of lines) {
|
|
21322
|
+
const m = line.match(/^commit\s+([0-9a-f]{7,40})/);
|
|
21323
|
+
if (m) {
|
|
21324
|
+
flush();
|
|
21325
|
+
sha = m[1];
|
|
21326
|
+
author = "";
|
|
21327
|
+
date3 = "";
|
|
21328
|
+
subject = "";
|
|
21329
|
+
inCommit = true;
|
|
21330
|
+
blanksAfterDate = 0;
|
|
21331
|
+
continue;
|
|
21332
|
+
}
|
|
21333
|
+
if (!inCommit) continue;
|
|
21334
|
+
if (/^Author:\s/.test(line)) {
|
|
21335
|
+
author = line.replace(/^Author:\s+/, "").trim();
|
|
21336
|
+
continue;
|
|
21337
|
+
}
|
|
21338
|
+
if (/^Date:\s/.test(line)) {
|
|
21339
|
+
date3 = line.replace(/^Date:\s+/, "").trim();
|
|
21340
|
+
continue;
|
|
21341
|
+
}
|
|
21342
|
+
if (line.trim() === "") {
|
|
21343
|
+
blanksAfterDate++;
|
|
21344
|
+
continue;
|
|
21345
|
+
}
|
|
21346
|
+
if (!subject && blanksAfterDate >= 1) {
|
|
21347
|
+
subject = line.trim();
|
|
21348
|
+
}
|
|
21349
|
+
}
|
|
21350
|
+
flush();
|
|
21351
|
+
return out.join("\n");
|
|
21352
|
+
}
|
|
21353
|
+
var BALANCED_GIT_LOG_BODY_LINES = 3;
|
|
21354
|
+
function balancedGitLog(stdout) {
|
|
21355
|
+
const lines = stdout.split("\n");
|
|
21356
|
+
const out = [];
|
|
21357
|
+
let bodyKept = 0;
|
|
21358
|
+
let bodyDropped = 0;
|
|
21359
|
+
let subjectSeen = false;
|
|
21360
|
+
let inCommit = false;
|
|
21361
|
+
let inBody = false;
|
|
21362
|
+
let blanksAfterDate = 0;
|
|
21363
|
+
const flushOmitted = () => {
|
|
21364
|
+
if (bodyDropped > 0) {
|
|
21365
|
+
out.push(` [+${bodyDropped} lines omitted]`);
|
|
21366
|
+
bodyDropped = 0;
|
|
21367
|
+
}
|
|
21368
|
+
};
|
|
21369
|
+
for (const line of lines) {
|
|
21370
|
+
if (/^commit\s+[0-9a-f]{7,40}/.test(line)) {
|
|
21371
|
+
flushOmitted();
|
|
21372
|
+
out.push(line);
|
|
21373
|
+
inCommit = true;
|
|
21374
|
+
inBody = false;
|
|
21375
|
+
subjectSeen = false;
|
|
21376
|
+
bodyKept = 0;
|
|
21377
|
+
blanksAfterDate = 0;
|
|
21378
|
+
continue;
|
|
21379
|
+
}
|
|
21380
|
+
if (!inCommit) {
|
|
21381
|
+
out.push(line);
|
|
21382
|
+
continue;
|
|
21383
|
+
}
|
|
21384
|
+
if (/^(Author|Date|Merge):\s/.test(line)) {
|
|
21385
|
+
out.push(line);
|
|
21386
|
+
continue;
|
|
21387
|
+
}
|
|
21388
|
+
if (line.trim() === "") {
|
|
21389
|
+
out.push(line);
|
|
21390
|
+
blanksAfterDate++;
|
|
21391
|
+
continue;
|
|
21392
|
+
}
|
|
21393
|
+
if (blanksAfterDate >= 1) inBody = true;
|
|
21394
|
+
if (inBody) {
|
|
21395
|
+
if (!subjectSeen) {
|
|
21396
|
+
subjectSeen = true;
|
|
21397
|
+
out.push(line);
|
|
21398
|
+
continue;
|
|
21399
|
+
}
|
|
21400
|
+
if (bodyKept >= BALANCED_GIT_LOG_BODY_LINES) {
|
|
21401
|
+
bodyDropped++;
|
|
21402
|
+
continue;
|
|
21403
|
+
}
|
|
21404
|
+
bodyKept++;
|
|
21405
|
+
}
|
|
21406
|
+
out.push(line);
|
|
21407
|
+
}
|
|
21408
|
+
flushOmitted();
|
|
21409
|
+
return out.join("\n");
|
|
21410
|
+
}
|
|
21411
|
+
function aggressiveGitDiff(stdout) {
|
|
21412
|
+
const lines = stdout.split("\n");
|
|
21413
|
+
const out = [];
|
|
21414
|
+
let currentFile = "";
|
|
21415
|
+
for (const line of lines) {
|
|
21416
|
+
const fm = line.match(/^diff --git a\/(.+?) b\//);
|
|
21417
|
+
if (fm) {
|
|
21418
|
+
currentFile = fm[1];
|
|
21419
|
+
out.push(`@@ ${currentFile}`);
|
|
21420
|
+
continue;
|
|
21421
|
+
}
|
|
21422
|
+
if (/^---\s|^\+\+\+\s|^index\s|^@@\s/.test(line)) continue;
|
|
21423
|
+
if (line.startsWith("+") || line.startsWith("-")) out.push(line);
|
|
21424
|
+
}
|
|
21425
|
+
return out.join("\n");
|
|
21426
|
+
}
|
|
21427
|
+
function humanReltime(dateStr) {
|
|
21428
|
+
const d = new Date(dateStr);
|
|
21429
|
+
if (Number.isNaN(d.getTime())) return dateStr;
|
|
21430
|
+
const ms = Date.now() - d.getTime();
|
|
21431
|
+
const hours = Math.round(ms / 36e5);
|
|
21432
|
+
if (hours < 1) return "just now";
|
|
21433
|
+
if (hours < 24) return `${hours}h ago`;
|
|
21434
|
+
const days = Math.round(hours / 24);
|
|
21435
|
+
if (days < 30) return `${days}d ago`;
|
|
21436
|
+
const months = Math.round(days / 30);
|
|
21437
|
+
if (months < 12) return `${months}mo ago`;
|
|
21438
|
+
return `${Math.round(months / 12)}y ago`;
|
|
21439
|
+
}
|
|
21440
|
+
function filterPackageManager(cmd, stdout, mode = DEFAULT_MODE) {
|
|
21441
|
+
if (/\b(install|add|i)\b/.test(cmd)) {
|
|
21442
|
+
const lines = stdout.split("\n");
|
|
21443
|
+
const filtered = lines.filter(
|
|
21444
|
+
(l) => !l.startsWith("npm warn") && !l.includes("packages are looking for funding") && !l.includes("run `npm fund`") && !l.startsWith("npm notice") && !/^[\s│├└─]+$/.test(l) && // tree-drawing characters
|
|
21445
|
+
!/^\s*$/.test(l)
|
|
21446
|
+
);
|
|
21447
|
+
if (mode === "aggressive") {
|
|
21448
|
+
const summaryOnly = filtered.filter(
|
|
21449
|
+
(l) => /^(added|removed|changed|audited)\s+\d+/.test(l) || /vulnerabilit(y|ies)/i.test(l) || /^npm\s+ERR/.test(l)
|
|
21450
|
+
);
|
|
21451
|
+
return { output: summaryOnly.join("\n"), filtered: true };
|
|
21452
|
+
}
|
|
21453
|
+
return { output: filtered.join("\n"), filtered: true };
|
|
21454
|
+
}
|
|
21455
|
+
if (/\btest\b/.test(cmd)) {
|
|
21456
|
+
return filterTestOutput(stdout);
|
|
21457
|
+
}
|
|
21458
|
+
if (mode === "aggressive" && /\b(ls|list|ll)\b/.test(cmd)) {
|
|
21459
|
+
return filterNpmLs(stdout);
|
|
21460
|
+
}
|
|
21461
|
+
return { output: stdout, filtered: false };
|
|
21462
|
+
}
|
|
21463
|
+
function filterNpmLs(stdout) {
|
|
21464
|
+
const lines = stdout.split("\n");
|
|
21465
|
+
const seen = /* @__PURE__ */ new Set();
|
|
21466
|
+
const out = [];
|
|
21467
|
+
for (const l of lines) {
|
|
21468
|
+
const stripped = l.replace(/^[\s│├└─┬]+/u, "").trimEnd();
|
|
21469
|
+
if (!stripped) continue;
|
|
21470
|
+
if (/\bdeduped\b/.test(stripped)) continue;
|
|
21471
|
+
const cleaned = stripped.replace(/^extraneous\s+/, "").replace(/\s+\bextraneous\b/g, "");
|
|
21472
|
+
if (seen.has(cleaned)) continue;
|
|
21473
|
+
seen.add(cleaned);
|
|
21474
|
+
out.push(cleaned);
|
|
21475
|
+
}
|
|
21476
|
+
return { output: out.join("\n"), filtered: true };
|
|
21477
|
+
}
|
|
21478
|
+
var FAIL_MARKER_RE = /^\s*[✗✘×]\s/;
|
|
21479
|
+
var FAIL_WORD_RE = /\bFAIL\b/;
|
|
21480
|
+
var FAILED_RE = /\bfailed?\b/i;
|
|
21481
|
+
var ERROR_RE = /\bERROR\b/;
|
|
21482
|
+
var SUMMARY_RE = /^\s*(Tests?|Suites?|Test Suites)\s*:|^\s*(pass|fail|skip|pending|todo)\s|\b\d+\s+(passing|failing|pending|skipped)\b|^(ok|not ok)\s|^ℹ\s|^(PASS|FAIL)\s/i;
|
|
21483
|
+
function isFailMarker(line) {
|
|
21484
|
+
return FAIL_MARKER_RE.test(line) || FAIL_WORD_RE.test(line) || FAILED_RE.test(line) || ERROR_RE.test(line);
|
|
21485
|
+
}
|
|
21486
|
+
function isSummaryLine(line) {
|
|
21487
|
+
return SUMMARY_RE.test(line);
|
|
21488
|
+
}
|
|
21489
|
+
function filterTestOutput(stdout) {
|
|
21490
|
+
const lines = stdout.split("\n");
|
|
21491
|
+
const failures = [];
|
|
21492
|
+
const summary = [];
|
|
21493
|
+
let inFailure = false;
|
|
21494
|
+
let failCount = 0;
|
|
21495
|
+
for (const line of lines) {
|
|
21496
|
+
if (isFailMarker(line)) {
|
|
21497
|
+
inFailure = true;
|
|
21498
|
+
failCount++;
|
|
21499
|
+
}
|
|
21500
|
+
if (inFailure) {
|
|
21501
|
+
failures.push(line);
|
|
21502
|
+
if (line.trim() === "" && failures.length > 3) inFailure = false;
|
|
21503
|
+
}
|
|
21504
|
+
if (isSummaryLine(line)) {
|
|
21505
|
+
summary.push(line);
|
|
21506
|
+
}
|
|
21507
|
+
}
|
|
21508
|
+
if (failCount === 0 && summary.length > 0) {
|
|
21509
|
+
return { output: summary.join("\n"), filtered: true };
|
|
21510
|
+
}
|
|
21511
|
+
if (failures.length > 0) {
|
|
21512
|
+
const rollup = summary.filter((l) => !/^PASS\s/i.test(l));
|
|
21513
|
+
const result = [...failures, "", ...rollup].join("\n");
|
|
21514
|
+
return { output: result, filtered: true };
|
|
21515
|
+
}
|
|
21516
|
+
return { output: stdout, filtered: false };
|
|
21517
|
+
}
|
|
21518
|
+
function filterBuildOutput(cmd, stdout) {
|
|
21519
|
+
const lines = stdout.split("\n");
|
|
21520
|
+
const filtered = lines.filter(
|
|
21521
|
+
(l) => !l.includes("Downloading") && !l.includes("Downloaded") && !/Compiling\s+\d+\s+of\s+\d+/.test(l) && !/^\s*Compiling\s+[\w-]+\s+v\d/.test(l) && !/^\s*Checking\s+[\w-]+\s+v\d/.test(l) && !l.includes("Blocking waiting for file lock") && !/^\s*$/.test(l)
|
|
21522
|
+
);
|
|
21523
|
+
return { output: filtered.join("\n"), filtered: filtered.length < lines.length };
|
|
21524
|
+
}
|
|
21525
|
+
function filterContainerOutput(cmd, stdout) {
|
|
21526
|
+
if (/docker\s+build/.test(cmd)) {
|
|
21527
|
+
const lines = stdout.split("\n");
|
|
21528
|
+
const filtered = lines.filter(
|
|
21529
|
+
(l) => !l.startsWith(" ---> ") && !l.startsWith("Sending build context") && !/^\s*$/.test(l)
|
|
21530
|
+
);
|
|
21531
|
+
return { output: filtered.join("\n"), filtered: true };
|
|
21532
|
+
}
|
|
21533
|
+
if (/^kubectl\s+(get|describe)\b/.test(cmd)) {
|
|
21534
|
+
const lines = stdout.split("\n").filter((l) => l.length > 0);
|
|
21535
|
+
if (lines.length <= 30) return { output: stdout, filtered: false };
|
|
21536
|
+
const header = lines[0];
|
|
21537
|
+
const rows = lines.slice(1);
|
|
21538
|
+
const headerCols = header.split(/\s{2,}/);
|
|
21539
|
+
const hasNamespace = headerCols[0]?.toUpperCase() === "NAMESPACE";
|
|
21540
|
+
const statusIdx = headerCols.findIndex((c) => /^STATUS$/i.test(c));
|
|
21541
|
+
const counts = /* @__PURE__ */ new Map();
|
|
21542
|
+
for (const row of rows) {
|
|
21543
|
+
const cols = row.split(/\s{2,}/);
|
|
21544
|
+
const ns = hasNamespace ? cols[0] : "(default)";
|
|
21545
|
+
const status = statusIdx >= 0 ? cols[statusIdx] : "\u2014";
|
|
21546
|
+
const key = `${ns} ${status}`;
|
|
21547
|
+
counts.set(key, (counts.get(key) ?? 0) + 1);
|
|
21548
|
+
}
|
|
21549
|
+
const summaryLines = [`${header}`, `(${rows.length} rows summarized by namespace/status)`];
|
|
21550
|
+
for (const [key, n] of [...counts.entries()].sort((a, b) => b[1] - a[1])) {
|
|
21551
|
+
const [ns, status] = key.split(" ");
|
|
21552
|
+
summaryLines.push(` ${ns} \u2014 ${status}: ${n}`);
|
|
21553
|
+
}
|
|
21554
|
+
return { output: summaryLines.join("\n"), filtered: true };
|
|
21555
|
+
}
|
|
21556
|
+
return { output: stdout, filtered: false };
|
|
21557
|
+
}
|
|
21558
|
+
function filterFileList(cmd, stdout, mode = DEFAULT_MODE) {
|
|
21559
|
+
const isLs = /^ls\b/.test(cmd);
|
|
21560
|
+
const isLong = /-l/.test(cmd);
|
|
21561
|
+
if (mode === "aggressive" && isLs && isLong) {
|
|
21562
|
+
return { output: aggressiveLsLong(stdout), filtered: true };
|
|
21563
|
+
}
|
|
21564
|
+
if (mode === "balanced" && isLs && isLong) {
|
|
21565
|
+
return { output: balancedLsLong(stdout), filtered: true };
|
|
21566
|
+
}
|
|
21567
|
+
const { summarizeAt, minDirs } = mode === "aggressive" ? { summarizeAt: 10, minDirs: 3 } : { summarizeAt: 20, minDirs: 4 };
|
|
21568
|
+
const lines = stdout.split("\n").filter((l) => l.trim() !== "");
|
|
21569
|
+
if (lines.length <= summarizeAt) return { output: stdout, filtered: false };
|
|
21570
|
+
if (cmd.includes("-R") || cmd.startsWith("find")) {
|
|
21571
|
+
const dirs = /* @__PURE__ */ new Map();
|
|
21572
|
+
for (const line of lines) {
|
|
21573
|
+
const parts = line.split("/");
|
|
21574
|
+
const dir = parts.length > 1 ? parts.slice(0, -1).join("/") : ".";
|
|
21575
|
+
dirs.set(dir, (dirs.get(dir) ?? 0) + 1);
|
|
21576
|
+
}
|
|
21577
|
+
if (dirs.size > minDirs) {
|
|
21578
|
+
const summary = Array.from(dirs.entries()).sort((a, b) => b[1] - a[1]).map(([dir, count]) => ` ${dir}/ (${count} files)`).join("\n");
|
|
21579
|
+
return {
|
|
21580
|
+
output: `${lines.length} files found:
|
|
21581
|
+
${summary}`,
|
|
21582
|
+
filtered: true
|
|
21583
|
+
};
|
|
21584
|
+
}
|
|
21585
|
+
}
|
|
21586
|
+
return { output: stdout, filtered: false };
|
|
21587
|
+
}
|
|
21588
|
+
function balancedLsLong(stdout) {
|
|
21589
|
+
const lines = stdout.split("\n");
|
|
21590
|
+
const out = [];
|
|
21591
|
+
for (const line of lines) {
|
|
21592
|
+
if (line.trim() === "") continue;
|
|
21593
|
+
if (/^total\s+\d+/.test(line)) continue;
|
|
21594
|
+
const m = line.match(
|
|
21595
|
+
/^([dlcb-])[rwxst@+\-]{9,}\s+\d+\s+\S+\s+\S+\s+\S+\s+\S+\s+\S+\s+\S+\s+(\.\.?)$/
|
|
21596
|
+
);
|
|
21597
|
+
if (m) continue;
|
|
21598
|
+
out.push(line);
|
|
21599
|
+
}
|
|
21600
|
+
return out.join("\n");
|
|
21601
|
+
}
|
|
21602
|
+
function aggressiveLsLong(stdout) {
|
|
21603
|
+
const lines = stdout.split("\n");
|
|
21604
|
+
const out = [];
|
|
21605
|
+
let inSection = false;
|
|
21606
|
+
for (const line of lines) {
|
|
21607
|
+
if (/^[^\s]+:$/.test(line.trim())) {
|
|
21608
|
+
out.push(line.trim());
|
|
21609
|
+
inSection = true;
|
|
21610
|
+
continue;
|
|
21611
|
+
}
|
|
21612
|
+
if (/^total\s+\d+/.test(line)) continue;
|
|
21613
|
+
if (line.trim() === "") continue;
|
|
21614
|
+
const m = line.match(
|
|
21615
|
+
/^([dlcb-])[rwxst@+\-]{9,}\s+\d+\s+\S+\s+\S+\s+(\S+)\s+\S+\s+\S+\s+\S+\s+(.+)$/
|
|
21616
|
+
);
|
|
21617
|
+
if (m) {
|
|
21618
|
+
const type = m[1];
|
|
21619
|
+
const sizeStr = m[2];
|
|
21620
|
+
const name = m[3];
|
|
21621
|
+
if (name === "." || name === "..") continue;
|
|
21622
|
+
if (type === "d" && inSection) continue;
|
|
21623
|
+
out.push(name + (type === "d" ? "/" : ` ${formatSize(sizeStr)}`));
|
|
21624
|
+
continue;
|
|
21625
|
+
}
|
|
21626
|
+
out.push(line);
|
|
21627
|
+
}
|
|
21628
|
+
return out.join("\n");
|
|
21629
|
+
}
|
|
21630
|
+
function formatSize(s) {
|
|
21631
|
+
const n = Number.parseInt(s, 10);
|
|
21632
|
+
if (Number.isNaN(n)) return s;
|
|
21633
|
+
if (n >= 1024 * 1024) return `${(n / 1024 / 1024).toFixed(1)}M`;
|
|
21634
|
+
if (n >= 1024) return `${(n / 1024).toFixed(1)}K`;
|
|
21635
|
+
return `${n}B`;
|
|
21636
|
+
}
|
|
21637
|
+
function filterGrep(stdout) {
|
|
21638
|
+
const lines = stdout.split("\n").filter((l) => l.length > 0);
|
|
21639
|
+
if (lines.length === 0) return { output: stdout, filtered: false };
|
|
21640
|
+
const byFile = /* @__PURE__ */ new Map();
|
|
21641
|
+
for (const line of lines) {
|
|
21642
|
+
const m = line.match(/^([^:]+):(\d+):(.*)$/);
|
|
21643
|
+
if (!m) {
|
|
21644
|
+
const arr2 = byFile.get("(no path)") ?? [];
|
|
21645
|
+
arr2.push(line.length > 100 ? `${line.slice(0, 100)}\u2026` : line);
|
|
21646
|
+
byFile.set("(no path)", arr2);
|
|
21647
|
+
continue;
|
|
21648
|
+
}
|
|
21649
|
+
const [, file, lineNo, content] = m;
|
|
21650
|
+
const truncated = content.length > 100 ? `${content.slice(0, 100)}\u2026` : content;
|
|
21651
|
+
const arr = byFile.get(file) ?? [];
|
|
21652
|
+
arr.push(` L${lineNo}: ${truncated.trim()}`);
|
|
21653
|
+
byFile.set(file, arr);
|
|
21654
|
+
}
|
|
21655
|
+
const out = [];
|
|
21656
|
+
for (const [file, hits] of byFile) {
|
|
21657
|
+
out.push(`${file} (${hits.length})`);
|
|
21658
|
+
for (const h of hits.slice(0, 8)) out.push(h);
|
|
21659
|
+
if (hits.length > 8) out.push(` ... +${hits.length - 8} more matches`);
|
|
21660
|
+
}
|
|
21661
|
+
return { output: out.join("\n"), filtered: true };
|
|
21662
|
+
}
|
|
21663
|
+
function filterDf(stdout) {
|
|
21664
|
+
const lines = stdout.split("\n");
|
|
21665
|
+
if (lines.length === 0) return { output: stdout, filtered: false };
|
|
21666
|
+
const header = lines[0];
|
|
21667
|
+
const out = [header.replace(/\s{2,}/g, " ")];
|
|
21668
|
+
for (const line of lines.slice(1)) {
|
|
21669
|
+
if (!line.trim()) continue;
|
|
21670
|
+
if (/^(tmpfs|devfs|devtmpfs|udev|overlay|map\s|none\s|\/dev\/loop)/.test(line)) continue;
|
|
21671
|
+
out.push(line.replace(/\s{2,}/g, " "));
|
|
21672
|
+
}
|
|
21673
|
+
return { output: out.join("\n"), filtered: true };
|
|
21674
|
+
}
|
|
21675
|
+
function filterDu(stdout) {
|
|
21676
|
+
const lines = stdout.split("\n").filter((l) => l.trim() !== "");
|
|
21677
|
+
if (lines.length <= 25) return { output: stdout, filtered: false };
|
|
21678
|
+
const parsed = lines.map((l) => {
|
|
21679
|
+
const m = l.match(/^([\d.]+[KMGT]?B?)?\s*(.*)$/);
|
|
21680
|
+
if (!m) return null;
|
|
21681
|
+
const sizeRaw = m[1] ?? "0";
|
|
21682
|
+
const path = m[2];
|
|
21683
|
+
return { sizeRaw, sizeBytes: parseDuSize(sizeRaw), path, line: l };
|
|
21684
|
+
}).filter((x) => x !== null);
|
|
21685
|
+
parsed.sort((a, b) => b.sizeBytes - a.sizeBytes);
|
|
21686
|
+
const top = parsed.slice(0, 20).map((p) => p.line);
|
|
21687
|
+
return {
|
|
21688
|
+
output: `(top 20 of ${parsed.length} entries by size)
|
|
21689
|
+
${top.join("\n")}`,
|
|
21690
|
+
filtered: true
|
|
21691
|
+
};
|
|
21692
|
+
}
|
|
21693
|
+
function parseDuSize(s) {
|
|
21694
|
+
const m = s.match(/^([\d.]+)([KMGT])?B?$/i);
|
|
21695
|
+
if (!m) return 0;
|
|
21696
|
+
const n = Number.parseFloat(m[1]);
|
|
21697
|
+
const unit = (m[2] ?? "").toUpperCase();
|
|
21698
|
+
const factor = unit === "T" ? 1024 ** 4 : unit === "G" ? 1024 ** 3 : unit === "M" ? 1024 ** 2 : unit === "K" ? 1024 : 1;
|
|
21699
|
+
return n * factor;
|
|
21700
|
+
}
|
|
21701
|
+
function filterPs(stdout) {
|
|
21702
|
+
const lines = stdout.split("\n");
|
|
21703
|
+
if (lines.length <= 2) return { output: stdout, filtered: false };
|
|
21704
|
+
const header = lines[0];
|
|
21705
|
+
const isAux = header.includes("USER") && header.includes("%CPU") && header.includes("%MEM");
|
|
21706
|
+
if (!isAux) return { output: stdout, filtered: false };
|
|
21707
|
+
const out = ["PID %CPU %MEM CMD"];
|
|
21708
|
+
for (const line of lines.slice(1)) {
|
|
21709
|
+
if (!line.trim()) continue;
|
|
21710
|
+
const parts = line.trim().split(/\s+/);
|
|
21711
|
+
if (parts.length < 11) continue;
|
|
21712
|
+
const pid = parts[1];
|
|
21713
|
+
const cpu = parts[2];
|
|
21714
|
+
const mem = parts[3];
|
|
21715
|
+
const cmd = parts.slice(10).join(" ");
|
|
21716
|
+
if (/^\[.*\]$/.test(cmd)) continue;
|
|
21717
|
+
out.push(`${pid.padEnd(5)} ${cpu.padStart(4)} ${mem.padStart(4)} ${cmd}`);
|
|
21718
|
+
}
|
|
21719
|
+
return { output: out.join("\n"), filtered: true };
|
|
21720
|
+
}
|
|
21721
|
+
|
|
21220
21722
|
// src/utils.ts
|
|
21221
21723
|
function detectInjectionPatterns(content) {
|
|
21222
21724
|
const warnings = [];
|
|
@@ -21265,6 +21767,11 @@ function formatBytes(bytes) {
|
|
|
21265
21767
|
|
|
21266
21768
|
// src/executor.ts
|
|
21267
21769
|
var DEFAULT_TIMEOUT = 3e4;
|
|
21770
|
+
var ANSI_RE = /\x1b\[[0-9;]*[a-zA-Z]/;
|
|
21771
|
+
var ANSI_RE_G = /\x1b\[[0-9;]*[a-zA-Z]/g;
|
|
21772
|
+
function stripAnsi(str) {
|
|
21773
|
+
return str.replace(ANSI_RE_G, "");
|
|
21774
|
+
}
|
|
21268
21775
|
var SAFE_ENV_KEYS = [
|
|
21269
21776
|
"PATH",
|
|
21270
21777
|
"HOME",
|
|
@@ -21317,6 +21824,20 @@ function killProcessTree(pid) {
|
|
|
21317
21824
|
}
|
|
21318
21825
|
}
|
|
21319
21826
|
}
|
|
21827
|
+
function stripProgressLines(output) {
|
|
21828
|
+
const lines = output.split("\n");
|
|
21829
|
+
const filtered = lines.filter((l) => {
|
|
21830
|
+
const trimmed = l.trim();
|
|
21831
|
+
if (ANSI_RE.test(l) && trimmed.replace(ANSI_RE_G, "").trim() === "") return false;
|
|
21832
|
+
if (/^[\s\[│├└─═━▓░█▒▏▎▍▌▋▊▉\]>=#\-.\d%]+$/.test(trimmed) && trimmed.length > 3) return false;
|
|
21833
|
+
if (/^[⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏\-\\|/]\s/.test(trimmed)) return false;
|
|
21834
|
+
if (/(?:downloading|uploading|fetching|resolving)\s+[\d.]+\s*[kmg]?b/i.test(trimmed))
|
|
21835
|
+
return false;
|
|
21836
|
+
if (/\d+\.?\d*\s*[kmg]?b\/s/i.test(trimmed) && /eta|remaining/i.test(trimmed)) return false;
|
|
21837
|
+
return true;
|
|
21838
|
+
});
|
|
21839
|
+
return filtered.join("\n");
|
|
21840
|
+
}
|
|
21320
21841
|
function deduplicateLines(output) {
|
|
21321
21842
|
const lines = output.split("\n");
|
|
21322
21843
|
if (lines.length < 3) return output;
|
|
@@ -21348,12 +21869,12 @@ function deduplicateLines(output) {
|
|
|
21348
21869
|
function groupErrorLines(output) {
|
|
21349
21870
|
const lines = output.split("\n");
|
|
21350
21871
|
if (lines.length < 5) return output;
|
|
21351
|
-
const
|
|
21872
|
+
const ERROR_RE2 = /^(.*?(?:error|warning|Error|Warning|ERR|WARN)[:\s])\s*(.+?)(?:\s+(?:at|in|on)\s+(?:line\s+)?(\d+))?$/i;
|
|
21352
21873
|
const errorGroups = /* @__PURE__ */ new Map();
|
|
21353
21874
|
const resultLines = [];
|
|
21354
21875
|
let groupedCount = 0;
|
|
21355
21876
|
for (const line of lines) {
|
|
21356
|
-
const match = line.match(
|
|
21877
|
+
const match = line.match(ERROR_RE2);
|
|
21357
21878
|
if (match) {
|
|
21358
21879
|
const prefix = match[1].trim();
|
|
21359
21880
|
const msg = match[2].trim();
|
|
@@ -21505,7 +22026,8 @@ var SubprocessExecutor = class {
|
|
|
21505
22026
|
tmpDir,
|
|
21506
22027
|
timeout,
|
|
21507
22028
|
maxOutput,
|
|
21508
|
-
plugin.needsShell
|
|
22029
|
+
plugin.needsShell,
|
|
22030
|
+
opts.language === "shell" ? opts.code : void 0
|
|
21509
22031
|
);
|
|
21510
22032
|
} finally {
|
|
21511
22033
|
setTimeout(() => this.cleanupTempDir(tmpDir), 100).unref();
|
|
@@ -21532,8 +22054,8 @@ var SubprocessExecutor = class {
|
|
|
21532
22054
|
}
|
|
21533
22055
|
return this.execute({ ...opts, code });
|
|
21534
22056
|
}
|
|
21535
|
-
spawnAndCapture(cmd, args, cwd, timeout, maxOutput, useShell) {
|
|
21536
|
-
return new Promise((
|
|
22057
|
+
spawnAndCapture(cmd, args, cwd, timeout, maxOutput, useShell, shellCode) {
|
|
22058
|
+
return new Promise((resolve5) => {
|
|
21537
22059
|
const hardCap = this.config.hardCapBytes;
|
|
21538
22060
|
const stdoutChunks = [];
|
|
21539
22061
|
const stderrChunks = [];
|
|
@@ -21573,7 +22095,7 @@ var SubprocessExecutor = class {
|
|
|
21573
22095
|
this.activeProcesses.delete(proc);
|
|
21574
22096
|
if (!resolved) {
|
|
21575
22097
|
resolved = true;
|
|
21576
|
-
|
|
22098
|
+
resolve5({
|
|
21577
22099
|
stdout: "",
|
|
21578
22100
|
stderr: err.message,
|
|
21579
22101
|
exitCode: 1,
|
|
@@ -21597,7 +22119,15 @@ var SubprocessExecutor = class {
|
|
|
21597
22119
|
stdout += `
|
|
21598
22120
|
[output capped at ${formatBytes(hardCap)} \u2014 process killed]`;
|
|
21599
22121
|
}
|
|
22122
|
+
stdout = stripAnsi(stdout);
|
|
22123
|
+
if (shellCode && stdout) {
|
|
22124
|
+
const filtered = applyCommandFilter(shellCode, stdout);
|
|
22125
|
+
if (filtered.filtered) {
|
|
22126
|
+
stdout = filtered.output;
|
|
22127
|
+
}
|
|
22128
|
+
}
|
|
21600
22129
|
if (stdout.length > 1e4) {
|
|
22130
|
+
stdout = stripProgressLines(stdout);
|
|
21601
22131
|
stdout = deduplicateLines(stdout);
|
|
21602
22132
|
stdout = groupErrorLines(stdout);
|
|
21603
22133
|
}
|
|
@@ -21605,7 +22135,7 @@ var SubprocessExecutor = class {
|
|
|
21605
22135
|
if (truncated) {
|
|
21606
22136
|
stdout = smartTruncate(stdout, maxOutput);
|
|
21607
22137
|
}
|
|
21608
|
-
|
|
22138
|
+
resolve5({
|
|
21609
22139
|
stdout,
|
|
21610
22140
|
stderr,
|
|
21611
22141
|
exitCode: code,
|
|
@@ -21638,65 +22168,6 @@ async function __cm_main(){${code}}
|
|
|
21638
22168
|
__cm_main().then(()=>{${epilogue}}).catch(e=>{console.error(e);${epilogue}process.exit(1)});`;
|
|
21639
22169
|
}
|
|
21640
22170
|
|
|
21641
|
-
// src/network.ts
|
|
21642
|
-
import dns from "node:dns";
|
|
21643
|
-
function isPrivateHost(hostname2) {
|
|
21644
|
-
const h = hostname2.startsWith("[") && hostname2.endsWith("]") ? hostname2.slice(1, -1) : hostname2;
|
|
21645
|
-
const lower = h.toLowerCase();
|
|
21646
|
-
if (lower === "localhost" || lower === "0.0.0.0") return true;
|
|
21647
|
-
if (/^0\./.test(h)) return true;
|
|
21648
|
-
if (/^127\./.test(h)) return true;
|
|
21649
|
-
if (/^10\./.test(h)) return true;
|
|
21650
|
-
if (/^172\.(1[6-9]|2\d|3[01])\./.test(h)) return true;
|
|
21651
|
-
if (/^192\.168\./.test(h)) return true;
|
|
21652
|
-
if (/^169\.254\./.test(h)) return true;
|
|
21653
|
-
if (/^100\.(6[4-9]|[7-9]\d|1[01]\d|12[0-7])\./.test(h)) return true;
|
|
21654
|
-
if (lower === "::1") return true;
|
|
21655
|
-
if (lower === "::" || lower === "0:0:0:0:0:0:0:0") return true;
|
|
21656
|
-
const mappedMatch = lower.match(/^::ffff:(\d+\.\d+\.\d+\.\d+)$/);
|
|
21657
|
-
if (mappedMatch) return isPrivateHost(mappedMatch[1]);
|
|
21658
|
-
if (/^fe[89ab]/i.test(h)) return true;
|
|
21659
|
-
if (/^f[cd]/i.test(h)) return true;
|
|
21660
|
-
return false;
|
|
21661
|
-
}
|
|
21662
|
-
async function resolveAndValidate(url) {
|
|
21663
|
-
const parsed = new URL(url);
|
|
21664
|
-
const hostname2 = parsed.hostname;
|
|
21665
|
-
if (/^\d+\.\d+\.\d+\.\d+$/.test(hostname2) || hostname2.includes(":")) {
|
|
21666
|
-
if (isPrivateHost(hostname2)) {
|
|
21667
|
-
throw new Error(`Blocked: resolved IP ${hostname2} is a private/internal address`);
|
|
21668
|
-
}
|
|
21669
|
-
return { url, resolvedIp: null };
|
|
21670
|
-
}
|
|
21671
|
-
let resolvedIp = null;
|
|
21672
|
-
let v4Error = false;
|
|
21673
|
-
let v6Error = false;
|
|
21674
|
-
const [v4Result, v6Result] = await Promise.allSettled([
|
|
21675
|
-
dns.promises.lookup(hostname2, { family: 4 }),
|
|
21676
|
-
dns.promises.lookup(hostname2, { family: 6 })
|
|
21677
|
-
]);
|
|
21678
|
-
if (v4Result.status === "fulfilled") {
|
|
21679
|
-
if (isPrivateHost(v4Result.value.address)) {
|
|
21680
|
-
throw new Error(`Blocked: ${hostname2} resolved to private IP ${v4Result.value.address}`);
|
|
21681
|
-
}
|
|
21682
|
-
resolvedIp = v4Result.value.address;
|
|
21683
|
-
} else {
|
|
21684
|
-
v4Error = true;
|
|
21685
|
-
}
|
|
21686
|
-
if (v6Result.status === "fulfilled") {
|
|
21687
|
-
if (isPrivateHost(v6Result.value.address)) {
|
|
21688
|
-
throw new Error(`Blocked: ${hostname2} resolved to private IPv6 ${v6Result.value.address}`);
|
|
21689
|
-
}
|
|
21690
|
-
if (!resolvedIp) resolvedIp = v6Result.value.address;
|
|
21691
|
-
} else {
|
|
21692
|
-
v6Error = true;
|
|
21693
|
-
}
|
|
21694
|
-
if (v4Error && v6Error) {
|
|
21695
|
-
throw new Error(`DNS resolution failed for ${hostname2}: unable to verify host safety`);
|
|
21696
|
-
}
|
|
21697
|
-
return { url, resolvedIp };
|
|
21698
|
-
}
|
|
21699
|
-
|
|
21700
22171
|
// src/runtime/index.ts
|
|
21701
22172
|
import { exec } from "node:child_process";
|
|
21702
22173
|
import { promisify } from "node:util";
|
|
@@ -22013,6 +22484,7 @@ function hasBun(runtimes) {
|
|
|
22013
22484
|
}
|
|
22014
22485
|
|
|
22015
22486
|
// src/stats.ts
|
|
22487
|
+
import { readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
|
22016
22488
|
var BAR_WIDTH = 20;
|
|
22017
22489
|
function asciiBar(ratio, width = BAR_WIDTH) {
|
|
22018
22490
|
const filled = Math.round(ratio * width);
|
|
@@ -22032,6 +22504,10 @@ var SessionTracker = class {
|
|
|
22032
22504
|
bytesSandboxed: 0,
|
|
22033
22505
|
sessionStart: Date.now()
|
|
22034
22506
|
};
|
|
22507
|
+
cumulativeFile;
|
|
22508
|
+
constructor(cumulativeFile) {
|
|
22509
|
+
this.cumulativeFile = cumulativeFile ?? null;
|
|
22510
|
+
}
|
|
22035
22511
|
trackCall(toolName, responseBytes) {
|
|
22036
22512
|
this.stats.calls[toolName] = (this.stats.calls[toolName] ?? 0) + 1;
|
|
22037
22513
|
this.stats.bytesReturned[toolName] = (this.stats.bytesReturned[toolName] ?? 0) + responseBytes;
|
|
@@ -22045,6 +22521,47 @@ var SessionTracker = class {
|
|
|
22045
22521
|
getSnapshot() {
|
|
22046
22522
|
return { ...this.stats };
|
|
22047
22523
|
}
|
|
22524
|
+
/** Load cumulative stats from disk */
|
|
22525
|
+
loadCumulative() {
|
|
22526
|
+
if (!this.cumulativeFile) return null;
|
|
22527
|
+
try {
|
|
22528
|
+
const data = readFileSync2(this.cumulativeFile, "utf-8");
|
|
22529
|
+
return JSON.parse(data);
|
|
22530
|
+
} catch {
|
|
22531
|
+
return null;
|
|
22532
|
+
}
|
|
22533
|
+
}
|
|
22534
|
+
/** Save current session stats to cumulative file */
|
|
22535
|
+
saveCumulative() {
|
|
22536
|
+
if (!this.cumulativeFile) return;
|
|
22537
|
+
const snap = this.stats;
|
|
22538
|
+
const keptOut = snap.bytesIndexed + snap.bytesSandboxed;
|
|
22539
|
+
const totalReturned = Object.values(snap.bytesReturned).reduce((a, b) => a + b, 0);
|
|
22540
|
+
const cumulative = this.loadCumulative() ?? {
|
|
22541
|
+
totalBytesSaved: 0,
|
|
22542
|
+
totalBytesProcessed: 0,
|
|
22543
|
+
totalCalls: 0,
|
|
22544
|
+
totalSessions: 0,
|
|
22545
|
+
firstSeen: (/* @__PURE__ */ new Date()).toISOString(),
|
|
22546
|
+
lastSeen: (/* @__PURE__ */ new Date()).toISOString(),
|
|
22547
|
+
perCommand: {}
|
|
22548
|
+
};
|
|
22549
|
+
cumulative.totalBytesSaved += keptOut;
|
|
22550
|
+
cumulative.totalBytesProcessed += keptOut + totalReturned;
|
|
22551
|
+
cumulative.totalCalls += Object.values(snap.calls).reduce((a, b) => a + b, 0);
|
|
22552
|
+
cumulative.totalSessions += 1;
|
|
22553
|
+
cumulative.lastSeen = (/* @__PURE__ */ new Date()).toISOString();
|
|
22554
|
+
for (const [name, calls] of Object.entries(snap.calls)) {
|
|
22555
|
+
if (!cumulative.perCommand[name]) {
|
|
22556
|
+
cumulative.perCommand[name] = { calls: 0 };
|
|
22557
|
+
}
|
|
22558
|
+
cumulative.perCommand[name].calls += calls;
|
|
22559
|
+
}
|
|
22560
|
+
try {
|
|
22561
|
+
writeFileSync2(this.cumulativeFile, JSON.stringify(cumulative, null, 2));
|
|
22562
|
+
} catch {
|
|
22563
|
+
}
|
|
22564
|
+
}
|
|
22048
22565
|
formatReport() {
|
|
22049
22566
|
const snap = this.stats;
|
|
22050
22567
|
const elapsed = Date.now() - snap.sessionStart;
|
|
@@ -22105,6 +22622,18 @@ var SessionTracker = class {
|
|
|
22105
22622
|
`
|
|
22106
22623
|
Context-compress kept ${formatBytes(keptOut)} out of context (${reductionPct}% savings).`
|
|
22107
22624
|
);
|
|
22625
|
+
const cumulative = this.loadCumulative();
|
|
22626
|
+
if (cumulative) {
|
|
22627
|
+
lines.push("\n## Cumulative Savings (All Sessions)\n");
|
|
22628
|
+
lines.push("| Metric | Value |");
|
|
22629
|
+
lines.push("|--------|-------|");
|
|
22630
|
+
lines.push(`| Sessions tracked | ${cumulative.totalSessions} |`);
|
|
22631
|
+
lines.push(`| Total data processed | ${formatBytes(cumulative.totalBytesProcessed)} |`);
|
|
22632
|
+
lines.push(`| Total kept out of context | ${formatBytes(cumulative.totalBytesSaved)} |`);
|
|
22633
|
+
const cumTokensMid = Math.round(cumulative.totalBytesSaved / 4);
|
|
22634
|
+
lines.push(`| Est. total tokens saved | ~${cumTokensMid.toLocaleString()} |`);
|
|
22635
|
+
lines.push(`| Tracking since | ${cumulative.firstSeen.split("T")[0]} |`);
|
|
22636
|
+
}
|
|
22108
22637
|
return lines.join("\n");
|
|
22109
22638
|
}
|
|
22110
22639
|
};
|
|
@@ -22301,6 +22830,9 @@ var ContentStore = class {
|
|
|
22301
22830
|
insertChunkStmt;
|
|
22302
22831
|
vocabCountStmt;
|
|
22303
22832
|
vocabInsertStmt;
|
|
22833
|
+
// Cache for getDistinctiveTerms — keyed by sourceId, with "_all" for the global query.
|
|
22834
|
+
// Invalidated whenever index() runs, since chunk counts/distributions change.
|
|
22835
|
+
distinctiveTermsCache = /* @__PURE__ */ new Map();
|
|
22304
22836
|
constructor(options) {
|
|
22305
22837
|
let path;
|
|
22306
22838
|
if (typeof options === "string") {
|
|
@@ -22393,6 +22925,7 @@ var ContentStore = class {
|
|
|
22393
22925
|
return sourceId2;
|
|
22394
22926
|
});
|
|
22395
22927
|
const sourceId = tx();
|
|
22928
|
+
this.distinctiveTermsCache.clear();
|
|
22396
22929
|
return {
|
|
22397
22930
|
sourceId,
|
|
22398
22931
|
label,
|
|
@@ -22516,10 +23049,16 @@ var ContentStore = class {
|
|
|
22516
23049
|
* Get distinctive terms for search hint.
|
|
22517
23050
|
*/
|
|
22518
23051
|
getDistinctiveTerms(sourceId) {
|
|
23052
|
+
const cacheKey = sourceId ?? "_all";
|
|
23053
|
+
const cached2 = this.distinctiveTermsCache.get(cacheKey);
|
|
23054
|
+
if (cached2) return cached2;
|
|
22519
23055
|
const totalChunks = this.db.prepare(
|
|
22520
23056
|
sourceId ? "SELECT COUNT(*) as cnt FROM chunks WHERE source_id = ?" : "SELECT COUNT(*) as cnt FROM chunks"
|
|
22521
23057
|
).get(...sourceId ? [sourceId] : []).cnt;
|
|
22522
|
-
if (totalChunks === 0)
|
|
23058
|
+
if (totalChunks === 0) {
|
|
23059
|
+
this.distinctiveTermsCache.set(cacheKey, []);
|
|
23060
|
+
return [];
|
|
23061
|
+
}
|
|
22523
23062
|
const filter = sourceId ? " WHERE source_id = ?" : "";
|
|
22524
23063
|
const stmt = this.db.prepare(`SELECT content FROM chunks${filter} LIMIT 500`);
|
|
22525
23064
|
const rows = sourceId ? stmt.all(sourceId) : stmt.all();
|
|
@@ -22544,7 +23083,9 @@ var ContentStore = class {
|
|
|
22544
23083
|
scored.push({ word, score });
|
|
22545
23084
|
}
|
|
22546
23085
|
scored.sort((a, b) => b.score - a.score);
|
|
22547
|
-
|
|
23086
|
+
const terms = scored.slice(0, 40).map((s) => s.word);
|
|
23087
|
+
this.distinctiveTermsCache.set(cacheKey, terms);
|
|
23088
|
+
return terms;
|
|
22548
23089
|
}
|
|
22549
23090
|
/**
|
|
22550
23091
|
* List all indexed sources with metadata.
|
|
@@ -22690,155 +23231,238 @@ function cleanupStaleDbs() {
|
|
|
22690
23231
|
return cleaned;
|
|
22691
23232
|
}
|
|
22692
23233
|
|
|
22693
|
-
// src/
|
|
22694
|
-
|
|
22695
|
-
|
|
22696
|
-
|
|
22697
|
-
|
|
22698
|
-
|
|
22699
|
-
|
|
22700
|
-
|
|
22701
|
-
|
|
22702
|
-
|
|
22703
|
-
|
|
22704
|
-
|
|
22705
|
-
|
|
22706
|
-
|
|
23234
|
+
// src/tools/batch-execute.ts
|
|
23235
|
+
function registerBatchExecuteTool(server2, ctx) {
|
|
23236
|
+
const { executor, store, tracker, config: config3, withExecutionLimit } = ctx;
|
|
23237
|
+
server2.tool(
|
|
23238
|
+
"batch_execute",
|
|
23239
|
+
"Execute multiple commands in ONE call, auto-index all output, and search with multiple queries. Returns search results directly \u2014 no follow-up calls needed.\n\nTHIS IS THE PRIMARY TOOL. Use this instead of multiple execute() calls.\n\nOne batch_execute call replaces 30+ execute calls + 10+ search calls.\nProvide all commands to run and all queries to search \u2014 everything happens in one round trip.",
|
|
23240
|
+
{
|
|
23241
|
+
commands: external_exports.array(
|
|
23242
|
+
external_exports.object({
|
|
23243
|
+
label: external_exports.string().describe("Section header for this command's output"),
|
|
23244
|
+
command: external_exports.string().describe("Shell command to execute")
|
|
23245
|
+
})
|
|
23246
|
+
).describe("Commands to execute as a batch."),
|
|
23247
|
+
queries: external_exports.array(external_exports.string()).describe(
|
|
23248
|
+
"Search queries to extract information from indexed output. Use 5-8 comprehensive queries."
|
|
23249
|
+
),
|
|
23250
|
+
timeout: external_exports.number().default(6e4).describe("Max execution time in ms (default: 60s)")
|
|
23251
|
+
},
|
|
23252
|
+
async ({ commands, queries, timeout }) => {
|
|
23253
|
+
const commandResults = await limitConcurrency(
|
|
23254
|
+
commands.map((cmd) => async () => {
|
|
23255
|
+
const result = await withExecutionLimit(
|
|
23256
|
+
() => executor.execute({
|
|
23257
|
+
language: "shell",
|
|
23258
|
+
code: cmd.command,
|
|
23259
|
+
timeout
|
|
23260
|
+
})
|
|
23261
|
+
);
|
|
23262
|
+
return { label: cmd.label, result };
|
|
23263
|
+
}),
|
|
23264
|
+
4
|
|
23265
|
+
);
|
|
23266
|
+
let combined = "";
|
|
23267
|
+
const inventory = [];
|
|
23268
|
+
for (let i = 0; i < commandResults.length; i++) {
|
|
23269
|
+
const settled = commandResults[i];
|
|
23270
|
+
const label = commands[i].label;
|
|
23271
|
+
if (settled.status === "fulfilled") {
|
|
23272
|
+
const { result } = settled.value;
|
|
23273
|
+
const output2 = result.stdout || "(no output)";
|
|
23274
|
+
combined += `## ${label}
|
|
23275
|
+
|
|
23276
|
+
${output2}
|
|
22707
23277
|
|
|
22708
|
-
// src/server.ts
|
|
22709
|
-
var LANGUAGE_ENUM = ALL_LANGUAGES;
|
|
22710
|
-
var projectDir = process.env.CLAUDE_PROJECT_DIR ?? process.cwd();
|
|
22711
|
-
function isWithinProject(absPath) {
|
|
22712
|
-
try {
|
|
22713
|
-
const normalized = realpathSync(resolve(absPath));
|
|
22714
|
-
const realProjectDir = realpathSync(projectDir);
|
|
22715
|
-
return normalized === realProjectDir || normalized.startsWith(`${realProjectDir}/`);
|
|
22716
|
-
} catch {
|
|
22717
|
-
const normalized = resolve(absPath);
|
|
22718
|
-
return normalized === projectDir || normalized.startsWith(`${projectDir}/`);
|
|
22719
|
-
}
|
|
22720
|
-
}
|
|
22721
|
-
function getVersion() {
|
|
22722
|
-
try {
|
|
22723
|
-
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
22724
|
-
const pkgPath = join4(__dirname, "..", "package.json");
|
|
22725
|
-
const pkg = JSON.parse(readFileSync2(pkgPath, "utf-8"));
|
|
22726
|
-
return pkg.version ?? "1.0.0";
|
|
22727
|
-
} catch {
|
|
22728
|
-
return "1.0.0";
|
|
22729
|
-
}
|
|
22730
|
-
}
|
|
22731
|
-
function compactLabel(normal, level) {
|
|
22732
|
-
if (level === "ultra") {
|
|
22733
|
-
return normal.replace(/\*\*/g, "").replace(/Use search\(queries: \[\.\.\.]\) to retrieve.*$/gm, "\u2192 search() for more").replace(/Searchable terms: .+$/gm, "");
|
|
22734
|
-
}
|
|
22735
|
-
if (level === "compact") {
|
|
22736
|
-
return normal.replace(
|
|
22737
|
-
/Use search\(queries: \[\.\.\.]\) to retrieve full content of any section\./,
|
|
22738
|
-
"\u2192 search() for details"
|
|
22739
|
-
);
|
|
22740
|
-
}
|
|
22741
|
-
return normal;
|
|
22742
|
-
}
|
|
22743
|
-
async function createServer(config3) {
|
|
22744
|
-
const version2 = getVersion();
|
|
22745
|
-
debug("Version:", version2);
|
|
22746
|
-
cleanupStaleDbs();
|
|
22747
|
-
const runtimes = await detectRuntimes();
|
|
22748
|
-
const bunDetected = hasBun(runtimes);
|
|
22749
|
-
debug("Runtimes detected:", runtimes.size);
|
|
22750
|
-
const executor = new SubprocessExecutor(runtimes, config3);
|
|
22751
|
-
let store;
|
|
22752
|
-
let dbFallback = false;
|
|
22753
|
-
try {
|
|
22754
|
-
store = new ContentStore({ persistDb: config3.persistDb, dbDir: config3.dbDir });
|
|
22755
|
-
} catch (e) {
|
|
22756
|
-
debug("Failed to create DB, falling back to in-memory:", e);
|
|
22757
|
-
store = new ContentStore(":memory:");
|
|
22758
|
-
dbFallback = true;
|
|
22759
|
-
}
|
|
22760
|
-
const tracker = new SessionTracker();
|
|
22761
|
-
let activeExecutions = 0;
|
|
22762
|
-
const MAX_CONCURRENT_EXECUTIONS = 8;
|
|
22763
|
-
const EXECUTION_LIMIT_ERROR = "Error: too many concurrent executions. Try again shortly.";
|
|
22764
|
-
async function withExecutionLimit(fn) {
|
|
22765
|
-
if (activeExecutions >= MAX_CONCURRENT_EXECUTIONS) {
|
|
22766
|
-
throw new Error(EXECUTION_LIMIT_ERROR);
|
|
22767
|
-
}
|
|
22768
|
-
activeExecutions++;
|
|
22769
|
-
try {
|
|
22770
|
-
return await fn();
|
|
22771
|
-
} finally {
|
|
22772
|
-
activeExecutions--;
|
|
22773
|
-
}
|
|
22774
|
-
}
|
|
22775
|
-
function applyIntentFilter(output, intent, sourceLabel) {
|
|
22776
|
-
if (Buffer.byteLength(output) <= config3.intentSearchThreshold) return output;
|
|
22777
|
-
const indexed = store.index(output, sourceLabel);
|
|
22778
|
-
tracker.trackIndexed(Buffer.byteLength(output));
|
|
22779
|
-
const searchResults = store.search(intent, { limit: 3 });
|
|
22780
|
-
const terms = store.getDistinctiveTerms(indexed.sourceId);
|
|
22781
|
-
let filtered = `Indexed ${indexed.totalChunks} sections from ${sourceLabel}.
|
|
22782
23278
|
`;
|
|
22783
|
-
|
|
23279
|
+
const lineCount = output2.split("\n").length;
|
|
23280
|
+
inventory.push(`- **${label}**: ${lineCount} lines`);
|
|
23281
|
+
} else {
|
|
23282
|
+
combined += `## ${label}
|
|
23283
|
+
|
|
23284
|
+
(error: ${settled.reason})
|
|
22784
23285
|
|
|
22785
23286
|
`;
|
|
22786
|
-
|
|
22787
|
-
|
|
23287
|
+
inventory.push(`- **${label}**: error`);
|
|
23288
|
+
}
|
|
23289
|
+
}
|
|
23290
|
+
const indexed = store.index(combined, "batch_execute");
|
|
23291
|
+
tracker.trackIndexed(Buffer.byteLength(combined));
|
|
23292
|
+
const searchResults = [];
|
|
23293
|
+
let totalBytes = 0;
|
|
23294
|
+
for (const query of queries) {
|
|
23295
|
+
if (totalBytes > config3.batchMaxBytes) break;
|
|
23296
|
+
let result = store.search(query, { source: "batch_execute", limit: 5 });
|
|
23297
|
+
if (result.results.length === 0) {
|
|
23298
|
+
result = store.search(query, { limit: 5 });
|
|
23299
|
+
}
|
|
23300
|
+
let block = `## ${query}
|
|
23301
|
+
|
|
22788
23302
|
`;
|
|
22789
|
-
|
|
22790
|
-
|
|
22791
|
-
|
|
22792
|
-
|
|
23303
|
+
if (result.results.length === 0) {
|
|
23304
|
+
block += "No results found.\n";
|
|
23305
|
+
} else {
|
|
23306
|
+
for (const hit of result.results) {
|
|
23307
|
+
block += `--- [${hit.source}] ---
|
|
23308
|
+
### ${hit.title}
|
|
23309
|
+
|
|
23310
|
+
${hit.snippet}
|
|
23311
|
+
|
|
22793
23312
|
`;
|
|
23313
|
+
}
|
|
23314
|
+
}
|
|
23315
|
+
searchResults.push(block);
|
|
23316
|
+
totalBytes += Buffer.byteLength(block);
|
|
23317
|
+
}
|
|
23318
|
+
const terms = store.getDistinctiveTerms(indexed.sourceId);
|
|
23319
|
+
let output = `**Inventory** (${commands.length} commands):
|
|
23320
|
+
${inventory.join("\n")}
|
|
23321
|
+
|
|
23322
|
+
`;
|
|
23323
|
+
output += searchResults.join("\n---\n\n");
|
|
23324
|
+
if (terms.length > 0) {
|
|
23325
|
+
output += `
|
|
23326
|
+
|
|
23327
|
+
Searchable terms: ${terms.join(", ")}`;
|
|
23328
|
+
}
|
|
23329
|
+
tracker.trackCall("batch_execute", Buffer.byteLength(output));
|
|
23330
|
+
return { content: [{ type: "text", text: output }] };
|
|
22794
23331
|
}
|
|
22795
|
-
|
|
22796
|
-
|
|
22797
|
-
|
|
22798
|
-
|
|
22799
|
-
|
|
22800
|
-
|
|
22801
|
-
} catch {
|
|
22802
|
-
}
|
|
22803
|
-
try {
|
|
22804
|
-
store.close();
|
|
22805
|
-
} catch {
|
|
22806
|
-
}
|
|
22807
|
-
};
|
|
22808
|
-
process.on("SIGINT", shutdown);
|
|
22809
|
-
process.on("SIGTERM", shutdown);
|
|
22810
|
-
process.on("beforeExit", shutdown);
|
|
22811
|
-
process.on("uncaughtException", (err) => {
|
|
22812
|
-
debug("Uncaught exception:", err);
|
|
22813
|
-
shutdown();
|
|
22814
|
-
process.exit(1);
|
|
22815
|
-
});
|
|
22816
|
-
process.on("unhandledRejection", (err) => {
|
|
22817
|
-
debug("Unhandled rejection:", err);
|
|
22818
|
-
shutdown();
|
|
22819
|
-
process.exit(1);
|
|
22820
|
-
});
|
|
22821
|
-
const searchCalls = [];
|
|
22822
|
-
const server2 = new McpServer({
|
|
22823
|
-
name: "context-compress",
|
|
22824
|
-
version: version2
|
|
22825
|
-
});
|
|
23332
|
+
);
|
|
23333
|
+
}
|
|
23334
|
+
|
|
23335
|
+
// src/tools/discover.ts
|
|
23336
|
+
function registerDiscoverTool(server2, ctx) {
|
|
23337
|
+
const { store, tracker, dbFallback } = ctx;
|
|
22826
23338
|
server2.tool(
|
|
22827
|
-
"
|
|
22828
|
-
|
|
23339
|
+
"discover",
|
|
23340
|
+
"Shows what's in the knowledge base and suggests optimization opportunities. Lists all indexed sources, chunk counts, searchable terms, and recommends next actions. Use this to understand what data is available for search.",
|
|
23341
|
+
{},
|
|
23342
|
+
async () => {
|
|
23343
|
+
const storeStats = store.getStats();
|
|
23344
|
+
const snap = tracker.getSnapshot();
|
|
23345
|
+
const lines = [];
|
|
23346
|
+
lines.push("## Knowledge Base Discovery\n");
|
|
23347
|
+
if (storeStats.totalSources === 0) {
|
|
23348
|
+
lines.push("No content indexed yet. Use these tools to build the knowledge base:\n");
|
|
23349
|
+
lines.push("- `batch_execute` \u2014 run commands and auto-index output");
|
|
23350
|
+
lines.push("- `execute` with `intent` \u2014 auto-indexes large output");
|
|
23351
|
+
lines.push("- `index` \u2014 index documentation or files");
|
|
23352
|
+
lines.push("- `fetch_and_index` \u2014 fetch and index web pages");
|
|
23353
|
+
} else {
|
|
23354
|
+
lines.push("| Metric | Value |");
|
|
23355
|
+
lines.push("|--------|-------|");
|
|
23356
|
+
lines.push(`| Indexed sources | ${storeStats.totalSources} |`);
|
|
23357
|
+
lines.push(`| Total chunks | ${storeStats.totalChunks} |`);
|
|
23358
|
+
lines.push(`| Vocabulary size | ${storeStats.vocabularySize} |`);
|
|
23359
|
+
lines.push(
|
|
23360
|
+
`| Trigram index | ${storeStats.hasTrigramTable ? "active" : "lazy (not yet needed)"} |`
|
|
23361
|
+
);
|
|
23362
|
+
const sources = store.listSources();
|
|
23363
|
+
if (sources.length > 0) {
|
|
23364
|
+
lines.push("\n### Indexed Sources\n");
|
|
23365
|
+
for (const src of sources) {
|
|
23366
|
+
lines.push(
|
|
23367
|
+
`- **${src.label}** \u2014 ${src.chunkCount} chunks${src.codeChunks > 0 ? ` (${src.codeChunks} with code)` : ""}`
|
|
23368
|
+
);
|
|
23369
|
+
}
|
|
23370
|
+
}
|
|
23371
|
+
const terms = store.getDistinctiveTerms();
|
|
23372
|
+
if (terms.length > 0) {
|
|
23373
|
+
lines.push("\n### Top Searchable Terms\n");
|
|
23374
|
+
lines.push(terms.slice(0, 20).join(", "));
|
|
23375
|
+
}
|
|
23376
|
+
}
|
|
23377
|
+
lines.push("\n### Optimization Suggestions\n");
|
|
23378
|
+
const totalCalls = Object.values(snap.calls).reduce((a, b) => a + b, 0);
|
|
23379
|
+
if (totalCalls === 0) {
|
|
23380
|
+
lines.push("- Start by using `batch_execute` to run multiple commands at once");
|
|
23381
|
+
} else {
|
|
23382
|
+
const searchCalls = snap.calls.search ?? 0;
|
|
23383
|
+
const executeCalls = snap.calls.execute ?? 0;
|
|
23384
|
+
const batchCalls = snap.calls.batch_execute ?? 0;
|
|
23385
|
+
if (executeCalls > 3 && batchCalls === 0) {
|
|
23386
|
+
lines.push(
|
|
23387
|
+
"- **Use batch_execute** \u2014 you've made multiple execute calls that could be batched into one"
|
|
23388
|
+
);
|
|
23389
|
+
}
|
|
23390
|
+
if (searchCalls > 5) {
|
|
23391
|
+
lines.push("- **Batch your searches** \u2014 pass multiple queries in a single search() call");
|
|
23392
|
+
}
|
|
23393
|
+
if (storeStats.totalChunks > 50) {
|
|
23394
|
+
lines.push(
|
|
23395
|
+
"- **Use source filtering** \u2014 scope searches with `source` parameter for faster, targeted results"
|
|
23396
|
+
);
|
|
23397
|
+
}
|
|
23398
|
+
if (storeStats.totalSources === 0 && totalCalls > 2) {
|
|
23399
|
+
lines.push(
|
|
23400
|
+
"- **Index more content** \u2014 use `intent` parameter in execute calls to auto-index large output"
|
|
23401
|
+
);
|
|
23402
|
+
}
|
|
23403
|
+
}
|
|
23404
|
+
if (dbFallback) {
|
|
23405
|
+
lines.push(
|
|
23406
|
+
"\n\u26A0 **Warning:** Persistent DB creation failed \u2014 using in-memory storage. Indexed data will not survive restarts."
|
|
23407
|
+
);
|
|
23408
|
+
}
|
|
23409
|
+
const output = lines.join("\n");
|
|
23410
|
+
tracker.trackCall("discover", Buffer.byteLength(output));
|
|
23411
|
+
return { content: [{ type: "text", text: output }] };
|
|
23412
|
+
}
|
|
23413
|
+
);
|
|
23414
|
+
}
|
|
22829
23415
|
|
|
22830
|
-
|
|
23416
|
+
// src/tools/execute-file.ts
|
|
23417
|
+
import { resolve as resolve2 } from "node:path";
|
|
23418
|
+
|
|
23419
|
+
// src/types.ts
|
|
23420
|
+
var ALL_LANGUAGES = [
|
|
23421
|
+
"javascript",
|
|
23422
|
+
"typescript",
|
|
23423
|
+
"python",
|
|
23424
|
+
"shell",
|
|
23425
|
+
"ruby",
|
|
23426
|
+
"go",
|
|
23427
|
+
"rust",
|
|
23428
|
+
"php",
|
|
23429
|
+
"perl",
|
|
23430
|
+
"r",
|
|
23431
|
+
"elixir"
|
|
23432
|
+
];
|
|
23433
|
+
|
|
23434
|
+
// src/util/path.ts
|
|
23435
|
+
import { realpathSync } from "node:fs";
|
|
23436
|
+
import { resolve } from "node:path";
|
|
23437
|
+
function isWithinProject(absPath, projectDir2) {
|
|
23438
|
+
try {
|
|
23439
|
+
const normalized = realpathSync(resolve(absPath));
|
|
23440
|
+
const realProjectDir = realpathSync(projectDir2);
|
|
23441
|
+
return normalized === realProjectDir || normalized.startsWith(`${realProjectDir}/`);
|
|
23442
|
+
} catch {
|
|
23443
|
+
const normalized = resolve(absPath);
|
|
23444
|
+
const normalizedProject = resolve(projectDir2);
|
|
23445
|
+
return normalized === normalizedProject || normalized.startsWith(`${normalizedProject}/`);
|
|
23446
|
+
}
|
|
23447
|
+
}
|
|
23448
|
+
|
|
23449
|
+
// src/tools/execute-file.ts
|
|
23450
|
+
var LANGUAGE_ENUM = ALL_LANGUAGES;
|
|
23451
|
+
function registerExecuteFileTool(server2, ctx) {
|
|
23452
|
+
const { executor, tracker, projectDir: projectDir2, withExecutionLimit, applyIntentFilter } = ctx;
|
|
23453
|
+
server2.tool(
|
|
23454
|
+
"execute_file",
|
|
23455
|
+
"Read a file and process it without loading contents into context. The file is read into a FILE_CONTENT variable inside the sandbox. Only your printed summary enters context.\n\nPREFER THIS OVER Read/cat for: log files, data files (CSV, JSON, XML), large source files for analysis, and any file where you need to extract specific information rather than read the entire content.",
|
|
22831
23456
|
{
|
|
23457
|
+
path: external_exports.string().describe("Absolute file path or relative to project root"),
|
|
22832
23458
|
language: external_exports.enum(LANGUAGE_ENUM).describe("Runtime language"),
|
|
22833
23459
|
code: external_exports.string().describe(
|
|
22834
|
-
"
|
|
22835
|
-
),
|
|
22836
|
-
intent: external_exports.string().optional().describe(
|
|
22837
|
-
"What you're looking for in the output. When provided and output is large (>5KB), indexes output into knowledge base and returns section titles + previews \u2014 not full content. Use search(queries: [...]) to retrieve specific sections."
|
|
23460
|
+
"Code to process FILE_CONTENT. Print summary via console.log/print/echo/IO.puts."
|
|
22838
23461
|
),
|
|
23462
|
+
intent: external_exports.string().optional().describe("What you're looking for in the output."),
|
|
22839
23463
|
timeout: external_exports.number().default(3e4).describe("Max execution time in ms")
|
|
22840
23464
|
},
|
|
22841
|
-
async ({ language, code, intent, timeout }) => {
|
|
23465
|
+
async ({ path: filePath, language, code, intent, timeout }) => {
|
|
22842
23466
|
const codeBytes = Buffer.byteLength(code);
|
|
22843
23467
|
if (codeBytes > 1024e3) {
|
|
22844
23468
|
return {
|
|
@@ -22847,18 +23471,35 @@ PREFER THIS OVER BASH for: API calls (gh, curl, aws), test runners (npm test, py
|
|
|
22847
23471
|
type: "text",
|
|
22848
23472
|
text: `Error: code too large (${(codeBytes / 1024).toFixed(0)}KB). Max 1MB.`
|
|
22849
23473
|
}
|
|
22850
|
-
]
|
|
23474
|
+
],
|
|
23475
|
+
isError: true
|
|
23476
|
+
};
|
|
23477
|
+
}
|
|
23478
|
+
const absPath = resolve2(projectDir2, filePath);
|
|
23479
|
+
if (!isWithinProject(absPath, projectDir2)) {
|
|
23480
|
+
return {
|
|
23481
|
+
content: [
|
|
23482
|
+
{
|
|
23483
|
+
type: "text",
|
|
23484
|
+
text: `Error: path "${filePath}" is outside the project directory`
|
|
23485
|
+
}
|
|
23486
|
+
],
|
|
23487
|
+
isError: true
|
|
22851
23488
|
};
|
|
22852
23489
|
}
|
|
22853
23490
|
let result;
|
|
22854
23491
|
try {
|
|
22855
|
-
result = await withExecutionLimit(
|
|
22856
|
-
|
|
22857
|
-
|
|
22858
|
-
|
|
22859
|
-
|
|
22860
|
-
|
|
22861
|
-
|
|
23492
|
+
result = await withExecutionLimit(
|
|
23493
|
+
() => executor.executeFile({
|
|
23494
|
+
language,
|
|
23495
|
+
code,
|
|
23496
|
+
filePath: absPath,
|
|
23497
|
+
timeout
|
|
23498
|
+
})
|
|
23499
|
+
);
|
|
23500
|
+
} catch (e) {
|
|
23501
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
23502
|
+
return { content: [{ type: "text", text: msg }], isError: true };
|
|
22862
23503
|
}
|
|
22863
23504
|
let output = result.stdout;
|
|
22864
23505
|
if (result.stderr && result.exitCode !== 0) {
|
|
@@ -22868,26 +23509,35 @@ STDERR:
|
|
|
22868
23509
|
${result.stderr}`;
|
|
22869
23510
|
}
|
|
22870
23511
|
if (intent) {
|
|
22871
|
-
output = applyIntentFilter(output, intent, `
|
|
23512
|
+
output = applyIntentFilter(output, intent, `file:${filePath}`);
|
|
22872
23513
|
}
|
|
22873
23514
|
const responseBytes = Buffer.byteLength(output);
|
|
22874
|
-
tracker.trackCall("
|
|
23515
|
+
tracker.trackCall("execute_file", responseBytes);
|
|
22875
23516
|
return { content: [{ type: "text", text: output }] };
|
|
22876
23517
|
}
|
|
22877
23518
|
);
|
|
23519
|
+
}
|
|
23520
|
+
|
|
23521
|
+
// src/tools/execute.ts
|
|
23522
|
+
var LANGUAGE_ENUM2 = ALL_LANGUAGES;
|
|
23523
|
+
function registerExecuteTool(server2, ctx) {
|
|
23524
|
+
const { executor, tracker, withExecutionLimit, applyIntentFilter, bunDetected } = ctx;
|
|
22878
23525
|
server2.tool(
|
|
22879
|
-
"
|
|
22880
|
-
|
|
23526
|
+
"execute",
|
|
23527
|
+
`Execute code in a sandboxed subprocess. Only stdout enters context \u2014 raw data stays in the subprocess. Use instead of bash/cat when output would exceed ~5KB. ${bunDetected ? "(Bun detected \u2014 JS/TS runs 3-5x faster) " : ""}Available: ${ALL_LANGUAGES.join(", ")}.
|
|
23528
|
+
|
|
23529
|
+
PREFER THIS OVER BASH for: API calls (gh, curl, aws), test runners (npm test, pytest), git queries (git log, git diff), data processing, and ANY CLI command that may produce large output. Bash should only be used for file mutations, git writes, and navigation.`,
|
|
22881
23530
|
{
|
|
22882
|
-
|
|
22883
|
-
language: external_exports.enum(LANGUAGE_ENUM).describe("Runtime language"),
|
|
23531
|
+
language: external_exports.enum(LANGUAGE_ENUM2).describe("Runtime language"),
|
|
22884
23532
|
code: external_exports.string().describe(
|
|
22885
|
-
"
|
|
23533
|
+
"Source code to execute. Use console.log (JS/TS), print (Python/Ruby/Perl/R), echo (Shell), echo (PHP), fmt.Println (Go), or IO.puts (Elixir) to output a summary to context."
|
|
23534
|
+
),
|
|
23535
|
+
intent: external_exports.string().optional().describe(
|
|
23536
|
+
"What you're looking for in the output. When provided and output is large (>5KB), indexes output into knowledge base and returns section titles + previews \u2014 not full content. Use search(queries: [...]) to retrieve specific sections."
|
|
22886
23537
|
),
|
|
22887
|
-
intent: external_exports.string().optional().describe("What you're looking for in the output."),
|
|
22888
23538
|
timeout: external_exports.number().default(3e4).describe("Max execution time in ms")
|
|
22889
23539
|
},
|
|
22890
|
-
async ({
|
|
23540
|
+
async ({ language, code, intent, timeout }) => {
|
|
22891
23541
|
const codeBytes = Buffer.byteLength(code);
|
|
22892
23542
|
if (codeBytes > 1024e3) {
|
|
22893
23543
|
return {
|
|
@@ -22896,33 +23546,19 @@ ${result.stderr}`;
|
|
|
22896
23546
|
type: "text",
|
|
22897
23547
|
text: `Error: code too large (${(codeBytes / 1024).toFixed(0)}KB). Max 1MB.`
|
|
22898
23548
|
}
|
|
22899
|
-
]
|
|
22900
|
-
|
|
22901
|
-
}
|
|
22902
|
-
const absPath = resolve(projectDir, filePath);
|
|
22903
|
-
if (!isWithinProject(absPath)) {
|
|
22904
|
-
return {
|
|
22905
|
-
content: [
|
|
22906
|
-
{
|
|
22907
|
-
type: "text",
|
|
22908
|
-
text: `Error: path "${filePath}" is outside the project directory`
|
|
22909
|
-
}
|
|
22910
|
-
]
|
|
23549
|
+
],
|
|
23550
|
+
isError: true
|
|
22911
23551
|
};
|
|
22912
23552
|
}
|
|
22913
23553
|
let result;
|
|
22914
23554
|
try {
|
|
22915
|
-
result = await withExecutionLimit(
|
|
22916
|
-
() => executor.executeFile({
|
|
22917
|
-
language,
|
|
22918
|
-
code,
|
|
22919
|
-
filePath: absPath,
|
|
22920
|
-
timeout
|
|
22921
|
-
})
|
|
22922
|
-
);
|
|
23555
|
+
result = await withExecutionLimit(() => executor.execute({ language, code, timeout }));
|
|
22923
23556
|
} catch (e) {
|
|
22924
23557
|
const msg = e instanceof Error ? e.message : String(e);
|
|
22925
|
-
return { content: [{ type: "text", text: msg }] };
|
|
23558
|
+
return { content: [{ type: "text", text: msg }], isError: true };
|
|
23559
|
+
}
|
|
23560
|
+
if (result.networkBytes) {
|
|
23561
|
+
tracker.trackSandboxed(result.networkBytes);
|
|
22926
23562
|
}
|
|
22927
23563
|
let output = result.stdout;
|
|
22928
23564
|
if (result.stderr && result.exitCode !== 0) {
|
|
@@ -22932,164 +23568,191 @@ STDERR:
|
|
|
22932
23568
|
${result.stderr}`;
|
|
22933
23569
|
}
|
|
22934
23570
|
if (intent) {
|
|
22935
|
-
output = applyIntentFilter(output, intent, `
|
|
23571
|
+
output = applyIntentFilter(output, intent, `execute:${language}`);
|
|
22936
23572
|
}
|
|
22937
23573
|
const responseBytes = Buffer.byteLength(output);
|
|
22938
|
-
tracker.trackCall("
|
|
23574
|
+
tracker.trackCall("execute", responseBytes);
|
|
22939
23575
|
return { content: [{ type: "text", text: output }] };
|
|
22940
23576
|
}
|
|
22941
23577
|
);
|
|
23578
|
+
}
|
|
23579
|
+
|
|
23580
|
+
// src/network.ts
|
|
23581
|
+
import dns from "node:dns";
|
|
23582
|
+
function isPrivateHost(hostname2) {
|
|
23583
|
+
const h = hostname2.startsWith("[") && hostname2.endsWith("]") ? hostname2.slice(1, -1) : hostname2;
|
|
23584
|
+
const lower = h.toLowerCase();
|
|
23585
|
+
if (lower === "localhost" || lower === "0.0.0.0") return true;
|
|
23586
|
+
if (/^0\./.test(h)) return true;
|
|
23587
|
+
if (/^127\./.test(h)) return true;
|
|
23588
|
+
if (/^10\./.test(h)) return true;
|
|
23589
|
+
if (/^172\.(1[6-9]|2\d|3[01])\./.test(h)) return true;
|
|
23590
|
+
if (/^192\.168\./.test(h)) return true;
|
|
23591
|
+
if (/^169\.254\./.test(h)) return true;
|
|
23592
|
+
if (/^100\.(6[4-9]|[7-9]\d|1[01]\d|12[0-7])\./.test(h)) return true;
|
|
23593
|
+
if (lower === "::1") return true;
|
|
23594
|
+
if (lower === "::" || lower === "0:0:0:0:0:0:0:0") return true;
|
|
23595
|
+
const mappedMatch = lower.match(/^::ffff:(\d+\.\d+\.\d+\.\d+)$/);
|
|
23596
|
+
if (mappedMatch) return isPrivateHost(mappedMatch[1]);
|
|
23597
|
+
const hexMappedMatch = lower.match(/^::ffff:([0-9a-f]{1,4}):([0-9a-f]{1,4})$/);
|
|
23598
|
+
if (hexMappedMatch) {
|
|
23599
|
+
const g1 = hexMappedMatch[1].padStart(4, "0");
|
|
23600
|
+
const g2 = hexMappedMatch[2].padStart(4, "0");
|
|
23601
|
+
const b1 = Number.parseInt(g1.slice(0, 2), 16);
|
|
23602
|
+
const b2 = Number.parseInt(g1.slice(2, 4), 16);
|
|
23603
|
+
const b3 = Number.parseInt(g2.slice(0, 2), 16);
|
|
23604
|
+
const b4 = Number.parseInt(g2.slice(2, 4), 16);
|
|
23605
|
+
return isPrivateHost(`${b1}.${b2}.${b3}.${b4}`);
|
|
23606
|
+
}
|
|
23607
|
+
if (/^fe[89ab]/i.test(h)) return true;
|
|
23608
|
+
if (/^f[cd]/i.test(h)) return true;
|
|
23609
|
+
return false;
|
|
23610
|
+
}
|
|
23611
|
+
async function resolveAndValidate(url) {
|
|
23612
|
+
const parsed = new URL(url);
|
|
23613
|
+
const hostname2 = parsed.hostname;
|
|
23614
|
+
if (/^\d+\.\d+\.\d+\.\d+$/.test(hostname2) || hostname2.includes(":")) {
|
|
23615
|
+
if (isPrivateHost(hostname2)) {
|
|
23616
|
+
throw new Error(`Blocked: resolved IP ${hostname2} is a private/internal address`);
|
|
23617
|
+
}
|
|
23618
|
+
return { url, resolvedIp: null };
|
|
23619
|
+
}
|
|
23620
|
+
let resolvedIp = null;
|
|
23621
|
+
let v4Error = false;
|
|
23622
|
+
let v6Error = false;
|
|
23623
|
+
const [v4Result, v6Result] = await Promise.allSettled([
|
|
23624
|
+
dns.promises.lookup(hostname2, { family: 4 }),
|
|
23625
|
+
dns.promises.lookup(hostname2, { family: 6 })
|
|
23626
|
+
]);
|
|
23627
|
+
if (v4Result.status === "fulfilled") {
|
|
23628
|
+
if (isPrivateHost(v4Result.value.address)) {
|
|
23629
|
+
throw new Error(`Blocked: ${hostname2} resolved to private IP ${v4Result.value.address}`);
|
|
23630
|
+
}
|
|
23631
|
+
resolvedIp = v4Result.value.address;
|
|
23632
|
+
} else {
|
|
23633
|
+
v4Error = true;
|
|
23634
|
+
}
|
|
23635
|
+
if (v6Result.status === "fulfilled") {
|
|
23636
|
+
if (isPrivateHost(v6Result.value.address)) {
|
|
23637
|
+
throw new Error(`Blocked: ${hostname2} resolved to private IPv6 ${v6Result.value.address}`);
|
|
23638
|
+
}
|
|
23639
|
+
if (!resolvedIp) resolvedIp = v6Result.value.address;
|
|
23640
|
+
} else {
|
|
23641
|
+
v6Error = true;
|
|
23642
|
+
}
|
|
23643
|
+
if (v4Error && v6Error) {
|
|
23644
|
+
throw new Error(`DNS resolution failed for ${hostname2}: unable to verify host safety`);
|
|
23645
|
+
}
|
|
23646
|
+
return { url, resolvedIp };
|
|
23647
|
+
}
|
|
23648
|
+
|
|
23649
|
+
// src/util/fetch-code.ts
|
|
23650
|
+
function buildFetchCode(url, resolvedIp) {
|
|
23651
|
+
let fetchSetup;
|
|
23652
|
+
if (resolvedIp) {
|
|
23653
|
+
const pinnedUrl = new URL(url);
|
|
23654
|
+
const originalHost = pinnedUrl.host;
|
|
23655
|
+
const hostnameValue = resolvedIp.includes(":") && !resolvedIp.startsWith("[") ? `[${resolvedIp}]` : resolvedIp;
|
|
23656
|
+
pinnedUrl.hostname = hostnameValue;
|
|
23657
|
+
fetchSetup = `
|
|
23658
|
+
const url = ${JSON.stringify(pinnedUrl.toString())};
|
|
23659
|
+
const resp = await fetch(url, { headers: { 'Host': ${JSON.stringify(originalHost)} }, redirect: 'error' });`;
|
|
23660
|
+
} else {
|
|
23661
|
+
fetchSetup = `
|
|
23662
|
+
const url = ${JSON.stringify(url)};
|
|
23663
|
+
const resp = await fetch(url, { redirect: 'error' });`;
|
|
23664
|
+
}
|
|
23665
|
+
return `${fetchSetup}
|
|
23666
|
+
if (!resp.ok) { console.error("HTTP " + resp.status); process.exit(1); }
|
|
23667
|
+
const cl = resp.headers.get('content-length');
|
|
23668
|
+
if (cl && parseInt(cl, 10) > 10 * 1024 * 1024) {
|
|
23669
|
+
console.error("Response too large: " + cl + " bytes"); process.exit(1);
|
|
23670
|
+
}
|
|
23671
|
+
const html = await resp.text();
|
|
23672
|
+
if (html.length > 10 * 1024 * 1024) {
|
|
23673
|
+
console.error("Response body too large: " + html.length + " chars"); process.exit(1);
|
|
23674
|
+
}
|
|
23675
|
+
|
|
23676
|
+
// Strip unwanted tags
|
|
23677
|
+
let md = html
|
|
23678
|
+
.replace(/<script[^>]*>[\\s\\S]*?<\\/script>/gi, "")
|
|
23679
|
+
.replace(/<style[^>]*>[\\s\\S]*?<\\/style>/gi, "")
|
|
23680
|
+
.replace(/<nav[^>]*>[\\s\\S]*?<\\/nav>/gi, "")
|
|
23681
|
+
.replace(/<header[^>]*>[\\s\\S]*?<\\/header>/gi, "")
|
|
23682
|
+
.replace(/<footer[^>]*>[\\s\\S]*?<\\/footer>/gi, "");
|
|
23683
|
+
|
|
23684
|
+
// Convert headings
|
|
23685
|
+
md = md.replace(/<h1[^>]*>(.*?)<\\/h1>/gi, "# $1\\n");
|
|
23686
|
+
md = md.replace(/<h2[^>]*>(.*?)<\\/h2>/gi, "## $1\\n");
|
|
23687
|
+
md = md.replace(/<h3[^>]*>(.*?)<\\/h3>/gi, "### $1\\n");
|
|
23688
|
+
md = md.replace(/<h4[^>]*>(.*?)<\\/h4>/gi, "#### $1\\n");
|
|
23689
|
+
|
|
23690
|
+
// Convert code blocks
|
|
23691
|
+
md = md.replace(/<pre[^>]*><code[^>]*>(.*?)<\\/code><\\/pre>/gis, "\`\`\`\\n$1\\n\`\`\`\\n");
|
|
23692
|
+
md = md.replace(/<code[^>]*>(.*?)<\\/code>/gi, "\`$1\`");
|
|
23693
|
+
|
|
23694
|
+
// Convert links
|
|
23695
|
+
md = md.replace(/<a[^>]*href="([^"]*)"[^>]*>(.*?)<\\/a>/gi, "[$2]($1)");
|
|
23696
|
+
|
|
23697
|
+
// Convert lists
|
|
23698
|
+
md = md.replace(/<li[^>]*>(.*?)<\\/li>/gi, "- $1\\n");
|
|
23699
|
+
|
|
23700
|
+
// Convert paragraphs
|
|
23701
|
+
md = md.replace(/<p[^>]*>(.*?)<\\/p>/gis, "$1\\n\\n");
|
|
23702
|
+
md = md.replace(/<br\\s*\\/?>/gi, "\\n");
|
|
23703
|
+
|
|
23704
|
+
// Strip remaining tags
|
|
23705
|
+
md = md.replace(/<[^>]+>/g, "");
|
|
23706
|
+
|
|
23707
|
+
// Decode entities
|
|
23708
|
+
md = md.replace(/</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;
|
|
22942
23728
|
server2.tool(
|
|
22943
|
-
"
|
|
22944
|
-
"
|
|
23729
|
+
"fetch_and_index",
|
|
23730
|
+
"Fetches URL content, converts HTML to markdown, indexes into searchable knowledge base, and returns a ~3KB preview. Full content stays in sandbox \u2014 use search() for deeper lookups.\n\nBetter than WebFetch: preview is immediate, full content is searchable, raw HTML never enters context.",
|
|
22945
23731
|
{
|
|
22946
|
-
|
|
22947
|
-
path: external_exports.string().optional().describe("File path to read and index (content never enters context)."),
|
|
23732
|
+
url: external_exports.string().describe("The URL to fetch and index"),
|
|
22948
23733
|
source: external_exports.string().optional().describe("Label for the indexed content")
|
|
22949
23734
|
},
|
|
22950
|
-
async ({
|
|
22951
|
-
|
|
22952
|
-
|
|
22953
|
-
|
|
22954
|
-
const absPath = resolve(projectDir, filePath);
|
|
22955
|
-
if (!isWithinProject(absPath)) {
|
|
22956
|
-
return {
|
|
22957
|
-
content: [
|
|
22958
|
-
{
|
|
22959
|
-
type: "text",
|
|
22960
|
-
text: `Error: path "${filePath}" is outside the project directory`
|
|
22961
|
-
}
|
|
22962
|
-
]
|
|
22963
|
-
};
|
|
22964
|
-
}
|
|
22965
|
-
try {
|
|
22966
|
-
const fileStat = statSync(absPath);
|
|
22967
|
-
if (fileStat.size > 50 * 1024 * 1024) {
|
|
22968
|
-
return {
|
|
22969
|
-
content: [
|
|
22970
|
-
{
|
|
22971
|
-
type: "text",
|
|
22972
|
-
text: `Error: file "${filePath}" is too large (${(fileStat.size / 1024 / 1024).toFixed(1)}MB). Max 50MB.`
|
|
22973
|
-
}
|
|
22974
|
-
]
|
|
22975
|
-
};
|
|
22976
|
-
}
|
|
22977
|
-
text = readFileSync2(absPath, "utf-8");
|
|
22978
|
-
label = source ?? filePath;
|
|
22979
|
-
} catch (e) {
|
|
22980
|
-
const msg = e instanceof Error ? e.message : String(e);
|
|
23735
|
+
async ({ url, source }) => {
|
|
23736
|
+
try {
|
|
23737
|
+
const parsed = new URL(url);
|
|
23738
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
22981
23739
|
return {
|
|
22982
|
-
content: [{ type: "text", text:
|
|
23740
|
+
content: [{ type: "text", text: "Error: only http/https URLs are allowed" }],
|
|
23741
|
+
isError: true
|
|
22983
23742
|
};
|
|
22984
23743
|
}
|
|
22985
|
-
|
|
22986
|
-
const contentBytes = Buffer.byteLength(content);
|
|
22987
|
-
if (contentBytes > 50 * 1024 * 1024) {
|
|
23744
|
+
if (isPrivateHost(parsed.hostname)) {
|
|
22988
23745
|
return {
|
|
22989
23746
|
content: [
|
|
22990
|
-
{
|
|
22991
|
-
|
|
22992
|
-
|
|
22993
|
-
}
|
|
22994
|
-
]
|
|
23747
|
+
{ type: "text", text: "Error: internal/private URLs are not allowed" }
|
|
23748
|
+
],
|
|
23749
|
+
isError: true
|
|
22995
23750
|
};
|
|
22996
23751
|
}
|
|
22997
|
-
|
|
22998
|
-
} else {
|
|
23752
|
+
} catch {
|
|
22999
23753
|
return {
|
|
23000
|
-
content: [{ type: "text", text:
|
|
23001
|
-
|
|
23002
|
-
}
|
|
23003
|
-
const result = store.index(text, label);
|
|
23004
|
-
tracker.trackIndexed(Buffer.byteLength(text));
|
|
23005
|
-
const summary = `Indexed "${label}": ${result.totalChunks} chunks (${result.codeChunks} with code). Use search(queries: [...]) to retrieve sections.`;
|
|
23006
|
-
tracker.trackCall("index", Buffer.byteLength(summary));
|
|
23007
|
-
return { content: [{ type: "text", text: summary }] };
|
|
23008
|
-
}
|
|
23009
|
-
);
|
|
23010
|
-
server2.tool(
|
|
23011
|
-
"search",
|
|
23012
|
-
"Search indexed content. Pass ALL search questions as queries array in ONE call.\n\nTIPS: 2-4 specific terms per query. Use 'source' to scope results.",
|
|
23013
|
-
{
|
|
23014
|
-
queries: external_exports.array(external_exports.string()).describe("Array of search queries. Batch ALL questions in one call."),
|
|
23015
|
-
source: external_exports.string().optional().describe("Filter to a specific indexed source (partial match)."),
|
|
23016
|
-
limit: external_exports.number().default(3).describe("Results per query (default: 3)")
|
|
23017
|
-
},
|
|
23018
|
-
async ({ queries, source, limit }) => {
|
|
23019
|
-
const now = Date.now();
|
|
23020
|
-
searchCalls.push(now);
|
|
23021
|
-
while (searchCalls.length > 0 && searchCalls[0] < now - config3.searchWindowMs) {
|
|
23022
|
-
searchCalls.shift();
|
|
23023
|
-
}
|
|
23024
|
-
const callCount = searchCalls.length;
|
|
23025
|
-
if (callCount > config3.searchBlockAfter) {
|
|
23026
|
-
const msg = "Too many search calls in quick succession. Use batch_execute instead to run commands and search in one call.";
|
|
23027
|
-
tracker.trackCall("search", Buffer.byteLength(msg));
|
|
23028
|
-
return { content: [{ type: "text", text: msg }] };
|
|
23029
|
-
}
|
|
23030
|
-
const effectiveLimit = callCount > config3.searchReduceAfter ? 1 : Math.min(limit, config3.searchLimit);
|
|
23031
|
-
const allResults = [];
|
|
23032
|
-
let totalBytes = 0;
|
|
23033
|
-
for (const query of queries) {
|
|
23034
|
-
if (totalBytes > config3.searchMaxBytes) break;
|
|
23035
|
-
const result = store.search(query, { source, limit: effectiveLimit });
|
|
23036
|
-
let block = `## ${query}
|
|
23037
|
-
`;
|
|
23038
|
-
if (result.corrected) {
|
|
23039
|
-
block += `(corrected to: "${result.corrected}")
|
|
23040
|
-
`;
|
|
23041
|
-
}
|
|
23042
|
-
if (result.results.length === 0) {
|
|
23043
|
-
block += "No results found.\n";
|
|
23044
|
-
} else {
|
|
23045
|
-
for (const hit of result.results) {
|
|
23046
|
-
block += `
|
|
23047
|
-
--- [${hit.source}] ---
|
|
23048
|
-
### ${hit.title}
|
|
23049
|
-
|
|
23050
|
-
${hit.snippet}
|
|
23051
|
-
`;
|
|
23052
|
-
}
|
|
23053
|
-
}
|
|
23054
|
-
allResults.push(block);
|
|
23055
|
-
totalBytes += Buffer.byteLength(block);
|
|
23056
|
-
}
|
|
23057
|
-
if (callCount > config3.searchReduceAfter) {
|
|
23058
|
-
allResults.push(
|
|
23059
|
-
`
|
|
23060
|
-
\u26A0 Search rate limited (${callCount} calls in ${config3.searchWindowMs / 1e3}s). Results reduced to 1 per query.`
|
|
23061
|
-
);
|
|
23062
|
-
}
|
|
23063
|
-
const output = allResults.join("\n---\n\n");
|
|
23064
|
-
tracker.trackCall("search", Buffer.byteLength(output));
|
|
23065
|
-
return { content: [{ type: "text", text: output }] };
|
|
23066
|
-
}
|
|
23067
|
-
);
|
|
23068
|
-
server2.tool(
|
|
23069
|
-
"fetch_and_index",
|
|
23070
|
-
"Fetches URL content, converts HTML to markdown, indexes into searchable knowledge base, and returns a ~3KB preview. Full content stays in sandbox \u2014 use search() for deeper lookups.\n\nBetter than WebFetch: preview is immediate, full content is searchable, raw HTML never enters context.",
|
|
23071
|
-
{
|
|
23072
|
-
url: external_exports.string().describe("The URL to fetch and index"),
|
|
23073
|
-
source: external_exports.string().optional().describe("Label for the indexed content")
|
|
23074
|
-
},
|
|
23075
|
-
async ({ url, source }) => {
|
|
23076
|
-
try {
|
|
23077
|
-
const parsed = new URL(url);
|
|
23078
|
-
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
23079
|
-
return {
|
|
23080
|
-
content: [{ type: "text", text: "Error: only http/https URLs are allowed" }]
|
|
23081
|
-
};
|
|
23082
|
-
}
|
|
23083
|
-
if (isPrivateHost(parsed.hostname)) {
|
|
23084
|
-
return {
|
|
23085
|
-
content: [
|
|
23086
|
-
{ type: "text", text: "Error: internal/private URLs are not allowed" }
|
|
23087
|
-
]
|
|
23088
|
-
};
|
|
23089
|
-
}
|
|
23090
|
-
} catch {
|
|
23091
|
-
return {
|
|
23092
|
-
content: [{ type: "text", text: `Error: invalid URL "${url}"` }]
|
|
23754
|
+
content: [{ type: "text", text: `Error: invalid URL "${url}"` }],
|
|
23755
|
+
isError: true
|
|
23093
23756
|
};
|
|
23094
23757
|
}
|
|
23095
23758
|
let resolvedIp = null;
|
|
@@ -23103,7 +23766,8 @@ ${hit.snippet}
|
|
|
23103
23766
|
type: "text",
|
|
23104
23767
|
text: `Error: ${err instanceof Error ? err.message : "DNS validation failed"}`
|
|
23105
23768
|
}
|
|
23106
|
-
]
|
|
23769
|
+
],
|
|
23770
|
+
isError: true
|
|
23107
23771
|
};
|
|
23108
23772
|
}
|
|
23109
23773
|
const label = source ?? url;
|
|
@@ -23119,12 +23783,12 @@ ${hit.snippet}
|
|
|
23119
23783
|
);
|
|
23120
23784
|
} catch (e) {
|
|
23121
23785
|
const msg = e instanceof Error ? e.message : String(e);
|
|
23122
|
-
return { content: [{ type: "text", text: msg }] };
|
|
23786
|
+
return { content: [{ type: "text", text: msg }], isError: true };
|
|
23123
23787
|
}
|
|
23124
23788
|
if (result.exitCode !== 0 || !result.stdout.trim()) {
|
|
23125
23789
|
const errMsg = `Failed to fetch ${url}: ${result.stderr || "empty response"}`;
|
|
23126
23790
|
tracker.trackCall("fetch_and_index", Buffer.byteLength(errMsg));
|
|
23127
|
-
return { content: [{ type: "text", text: errMsg }] };
|
|
23791
|
+
return { content: [{ type: "text", text: errMsg }], isError: true };
|
|
23128
23792
|
}
|
|
23129
23793
|
const markdown = result.stdout;
|
|
23130
23794
|
tracker.trackSandboxed(result.networkBytes ?? 0);
|
|
@@ -23154,188 +23818,311 @@ Searchable terms: ${terms.join(", ")}`;
|
|
|
23154
23818
|
return { content: [{ type: "text", text: output }] };
|
|
23155
23819
|
}
|
|
23156
23820
|
);
|
|
23821
|
+
}
|
|
23822
|
+
|
|
23823
|
+
// src/tools/index-content.ts
|
|
23824
|
+
import { readFileSync as readFileSync3, statSync } from "node:fs";
|
|
23825
|
+
import { resolve as resolve3 } from "node:path";
|
|
23826
|
+
function registerIndexTool(server2, ctx) {
|
|
23827
|
+
const { store, tracker, projectDir: projectDir2 } = ctx;
|
|
23157
23828
|
server2.tool(
|
|
23158
|
-
"
|
|
23159
|
-
"
|
|
23829
|
+
"index",
|
|
23830
|
+
"Index documentation or knowledge content into a searchable BM25 knowledge base. Chunks markdown by headings (keeping code blocks intact) and stores in ephemeral FTS5 database. The full content does NOT stay in context \u2014 only a brief summary is returned.\n\nWHEN TO USE:\n- Documentation (API docs, framework guides, code examples)\n- README files, migration guides, changelog entries\n- Any content with code examples you may need to reference precisely\n\nAfter indexing, use 'search' to retrieve specific sections on-demand.",
|
|
23160
23831
|
{
|
|
23161
|
-
|
|
23162
|
-
|
|
23163
|
-
|
|
23164
|
-
command: external_exports.string().describe("Shell command to execute")
|
|
23165
|
-
})
|
|
23166
|
-
).describe("Commands to execute as a batch."),
|
|
23167
|
-
queries: external_exports.array(external_exports.string()).describe(
|
|
23168
|
-
"Search queries to extract information from indexed output. Use 5-8 comprehensive queries."
|
|
23169
|
-
),
|
|
23170
|
-
timeout: external_exports.number().default(6e4).describe("Max execution time in ms (default: 60s)")
|
|
23832
|
+
content: external_exports.string().optional().describe("Raw text/markdown to index. Provide this OR path, not both."),
|
|
23833
|
+
path: external_exports.string().optional().describe("File path to read and index (content never enters context)."),
|
|
23834
|
+
source: external_exports.string().optional().describe("Label for the indexed content")
|
|
23171
23835
|
},
|
|
23172
|
-
async ({
|
|
23173
|
-
|
|
23174
|
-
|
|
23175
|
-
|
|
23176
|
-
|
|
23177
|
-
|
|
23178
|
-
|
|
23179
|
-
|
|
23180
|
-
|
|
23181
|
-
|
|
23182
|
-
|
|
23183
|
-
|
|
23184
|
-
|
|
23185
|
-
|
|
23186
|
-
|
|
23187
|
-
const inventory = [];
|
|
23188
|
-
for (let i = 0; i < commandResults.length; i++) {
|
|
23189
|
-
const settled = commandResults[i];
|
|
23190
|
-
const label = commands[i].label;
|
|
23191
|
-
if (settled.status === "fulfilled") {
|
|
23192
|
-
const { result } = settled.value;
|
|
23193
|
-
const output2 = result.stdout || "(no output)";
|
|
23194
|
-
combined += `## ${label}
|
|
23195
|
-
|
|
23196
|
-
${output2}
|
|
23197
|
-
|
|
23198
|
-
`;
|
|
23199
|
-
const lineCount = output2.split("\n").length;
|
|
23200
|
-
inventory.push(`- **${label}**: ${lineCount} lines`);
|
|
23201
|
-
} else {
|
|
23202
|
-
combined += `## ${label}
|
|
23203
|
-
|
|
23204
|
-
(error: ${settled.reason})
|
|
23205
|
-
|
|
23206
|
-
`;
|
|
23207
|
-
inventory.push(`- **${label}**: error`);
|
|
23836
|
+
async ({ content, path: filePath, source }) => {
|
|
23837
|
+
let text;
|
|
23838
|
+
let label = source ?? "indexed content";
|
|
23839
|
+
if (filePath) {
|
|
23840
|
+
const absPath = resolve3(projectDir2, filePath);
|
|
23841
|
+
if (!isWithinProject(absPath, projectDir2)) {
|
|
23842
|
+
return {
|
|
23843
|
+
content: [
|
|
23844
|
+
{
|
|
23845
|
+
type: "text",
|
|
23846
|
+
text: `Error: path "${filePath}" is outside the project directory`
|
|
23847
|
+
}
|
|
23848
|
+
],
|
|
23849
|
+
isError: true
|
|
23850
|
+
};
|
|
23208
23851
|
}
|
|
23852
|
+
try {
|
|
23853
|
+
const fileStat = statSync(absPath);
|
|
23854
|
+
if (fileStat.size > 50 * 1024 * 1024) {
|
|
23855
|
+
return {
|
|
23856
|
+
content: [
|
|
23857
|
+
{
|
|
23858
|
+
type: "text",
|
|
23859
|
+
text: `Error: file "${filePath}" is too large (${(fileStat.size / 1024 / 1024).toFixed(1)}MB). Max 50MB.`
|
|
23860
|
+
}
|
|
23861
|
+
],
|
|
23862
|
+
isError: true
|
|
23863
|
+
};
|
|
23864
|
+
}
|
|
23865
|
+
text = readFileSync3(absPath, "utf-8");
|
|
23866
|
+
label = source ?? filePath;
|
|
23867
|
+
} catch (e) {
|
|
23868
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
23869
|
+
return {
|
|
23870
|
+
content: [{ type: "text", text: `Error reading "${filePath}": ${msg}` }],
|
|
23871
|
+
isError: true
|
|
23872
|
+
};
|
|
23873
|
+
}
|
|
23874
|
+
} else if (content) {
|
|
23875
|
+
const contentBytes = Buffer.byteLength(content);
|
|
23876
|
+
if (contentBytes > 50 * 1024 * 1024) {
|
|
23877
|
+
return {
|
|
23878
|
+
content: [
|
|
23879
|
+
{
|
|
23880
|
+
type: "text",
|
|
23881
|
+
text: `Error: content too large (${(contentBytes / 1024 / 1024).toFixed(1)}MB). Max 50MB.`
|
|
23882
|
+
}
|
|
23883
|
+
],
|
|
23884
|
+
isError: true
|
|
23885
|
+
};
|
|
23886
|
+
}
|
|
23887
|
+
text = content;
|
|
23888
|
+
} else {
|
|
23889
|
+
return {
|
|
23890
|
+
content: [{ type: "text", text: "Error: provide either 'content' or 'path'" }],
|
|
23891
|
+
isError: true
|
|
23892
|
+
};
|
|
23209
23893
|
}
|
|
23210
|
-
const
|
|
23211
|
-
tracker.trackIndexed(Buffer.byteLength(
|
|
23212
|
-
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 = [];
|
|
23213
23929
|
let totalBytes = 0;
|
|
23214
23930
|
for (const query of queries) {
|
|
23215
|
-
if (totalBytes > config3.
|
|
23216
|
-
|
|
23217
|
-
if (result.results.length === 0) {
|
|
23218
|
-
result = store.search(query, { limit: 5 });
|
|
23219
|
-
}
|
|
23931
|
+
if (totalBytes > config3.searchMaxBytes) break;
|
|
23932
|
+
const result = store.search(query, { source, limit: effectiveLimit });
|
|
23220
23933
|
let block = `## ${query}
|
|
23221
|
-
|
|
23222
23934
|
`;
|
|
23935
|
+
if (result.corrected) {
|
|
23936
|
+
block += `(corrected to: "${result.corrected}")
|
|
23937
|
+
`;
|
|
23938
|
+
}
|
|
23223
23939
|
if (result.results.length === 0) {
|
|
23224
23940
|
block += "No results found.\n";
|
|
23225
23941
|
} else {
|
|
23226
23942
|
for (const hit of result.results) {
|
|
23227
|
-
block +=
|
|
23943
|
+
block += `
|
|
23944
|
+
--- [${hit.source}] ---
|
|
23228
23945
|
### ${hit.title}
|
|
23229
23946
|
|
|
23230
23947
|
${hit.snippet}
|
|
23231
|
-
|
|
23232
23948
|
`;
|
|
23233
23949
|
}
|
|
23234
23950
|
}
|
|
23235
|
-
|
|
23951
|
+
allResults.push(block);
|
|
23236
23952
|
totalBytes += Buffer.byteLength(block);
|
|
23237
23953
|
}
|
|
23238
|
-
|
|
23239
|
-
|
|
23240
|
-
|
|
23241
|
-
|
|
23242
|
-
|
|
23243
|
-
output += searchResults.join("\n---\n\n");
|
|
23244
|
-
if (terms.length > 0) {
|
|
23245
|
-
output += `
|
|
23246
|
-
|
|
23247
|
-
Searchable terms: ${terms.join(", ")}`;
|
|
23954
|
+
if (callCount > config3.searchReduceAfter) {
|
|
23955
|
+
allResults.push(
|
|
23956
|
+
`
|
|
23957
|
+
\u26A0 Search rate limited (${callCount} calls in ${config3.searchWindowMs / 1e3}s). Results reduced to 1 per query.`
|
|
23958
|
+
);
|
|
23248
23959
|
}
|
|
23249
|
-
|
|
23960
|
+
const output = allResults.join("\n---\n\n");
|
|
23961
|
+
tracker.trackCall("search", Buffer.byteLength(output));
|
|
23250
23962
|
return { content: [{ type: "text", text: output }] };
|
|
23251
23963
|
}
|
|
23252
23964
|
);
|
|
23965
|
+
}
|
|
23966
|
+
|
|
23967
|
+
// src/tools/stats.ts
|
|
23968
|
+
function registerStatsTool(server2, ctx) {
|
|
23969
|
+
const { tracker } = ctx;
|
|
23253
23970
|
server2.tool(
|
|
23254
23971
|
"stats",
|
|
23255
23972
|
"Returns context consumption statistics for the current session. Shows total bytes returned to context, breakdown by tool, call counts, estimated token usage, context savings ratio, and visual charts.",
|
|
23256
23973
|
{},
|
|
23257
23974
|
async () => {
|
|
23975
|
+
tracker.saveCumulative();
|
|
23258
23976
|
const report = tracker.formatReport();
|
|
23259
23977
|
tracker.trackCall("stats", Buffer.byteLength(report));
|
|
23260
23978
|
return { content: [{ type: "text", text: report }] };
|
|
23261
23979
|
}
|
|
23262
23980
|
);
|
|
23263
|
-
|
|
23264
|
-
|
|
23265
|
-
|
|
23266
|
-
|
|
23267
|
-
|
|
23268
|
-
|
|
23269
|
-
|
|
23270
|
-
|
|
23271
|
-
|
|
23272
|
-
|
|
23273
|
-
|
|
23274
|
-
|
|
23275
|
-
|
|
23276
|
-
|
|
23277
|
-
|
|
23278
|
-
|
|
23279
|
-
|
|
23280
|
-
|
|
23281
|
-
|
|
23282
|
-
|
|
23283
|
-
|
|
23284
|
-
|
|
23285
|
-
|
|
23286
|
-
|
|
23287
|
-
|
|
23288
|
-
|
|
23289
|
-
|
|
23290
|
-
|
|
23291
|
-
|
|
23292
|
-
|
|
23293
|
-
|
|
23294
|
-
|
|
23295
|
-
|
|
23296
|
-
const terms = store.getDistinctiveTerms();
|
|
23297
|
-
if (terms.length > 0) {
|
|
23298
|
-
lines.push("\n### Top Searchable Terms\n");
|
|
23299
|
-
lines.push(terms.slice(0, 20).join(", "));
|
|
23300
|
-
}
|
|
23301
|
-
}
|
|
23302
|
-
lines.push("\n### Optimization Suggestions\n");
|
|
23303
|
-
const totalCalls = Object.values(snap.calls).reduce((a, b) => a + b, 0);
|
|
23304
|
-
if (totalCalls === 0) {
|
|
23305
|
-
lines.push("- Start by using `batch_execute` to run multiple commands at once");
|
|
23306
|
-
} else {
|
|
23307
|
-
const searchCalls2 = snap.calls.search ?? 0;
|
|
23308
|
-
const executeCalls = snap.calls.execute ?? 0;
|
|
23309
|
-
const batchCalls = snap.calls.batch_execute ?? 0;
|
|
23310
|
-
if (executeCalls > 3 && batchCalls === 0) {
|
|
23311
|
-
lines.push(
|
|
23312
|
-
"- **Use batch_execute** \u2014 you've made multiple execute calls that could be batched into one"
|
|
23313
|
-
);
|
|
23314
|
-
}
|
|
23315
|
-
if (searchCalls2 > 5) {
|
|
23316
|
-
lines.push("- **Batch your searches** \u2014 pass multiple queries in a single search() call");
|
|
23317
|
-
}
|
|
23318
|
-
if (storeStats.totalChunks > 50) {
|
|
23319
|
-
lines.push(
|
|
23320
|
-
"- **Use source filtering** \u2014 scope searches with `source` parameter for faster, targeted results"
|
|
23321
|
-
);
|
|
23322
|
-
}
|
|
23323
|
-
if (storeStats.totalSources === 0 && totalCalls > 2) {
|
|
23324
|
-
lines.push(
|
|
23325
|
-
"- **Index more content** \u2014 use `intent` parameter in execute calls to auto-index large output"
|
|
23326
|
-
);
|
|
23327
|
-
}
|
|
23328
|
-
}
|
|
23329
|
-
if (dbFallback) {
|
|
23330
|
-
lines.push(
|
|
23331
|
-
"\n\u26A0 **Warning:** Persistent DB creation failed \u2014 using in-memory storage. Indexed data will not survive restarts."
|
|
23332
|
-
);
|
|
23333
|
-
}
|
|
23334
|
-
const output = lines.join("\n");
|
|
23335
|
-
tracker.trackCall("discover", Buffer.byteLength(output));
|
|
23336
|
-
return { content: [{ type: "text", text: output }] };
|
|
23981
|
+
}
|
|
23982
|
+
|
|
23983
|
+
// src/util/label.ts
|
|
23984
|
+
function compactLabel(normal, level) {
|
|
23985
|
+
if (level === "ultra") {
|
|
23986
|
+
return normal.replace(/\*\*/g, "").replace(/Use search\(queries: \[\.\.\.]\) to retrieve.*$/gm, "\u2192 search() for more").replace(/Searchable terms: .+$/gm, "");
|
|
23987
|
+
}
|
|
23988
|
+
if (level === "compact") {
|
|
23989
|
+
return normal.replace(
|
|
23990
|
+
/Use search\(queries: \[\.\.\.]\) to retrieve full content of any section\./,
|
|
23991
|
+
"\u2192 search() for details"
|
|
23992
|
+
);
|
|
23993
|
+
}
|
|
23994
|
+
return normal;
|
|
23995
|
+
}
|
|
23996
|
+
|
|
23997
|
+
// src/util/intent-filter.ts
|
|
23998
|
+
function createIntentFilter(deps) {
|
|
23999
|
+
const { config: config3, store, tracker } = deps;
|
|
24000
|
+
return function applyIntentFilter(output, intent, sourceLabel) {
|
|
24001
|
+
if (Buffer.byteLength(output) <= config3.intentSearchThreshold) return output;
|
|
24002
|
+
const indexed = store.index(output, sourceLabel);
|
|
24003
|
+
tracker.trackIndexed(Buffer.byteLength(output));
|
|
24004
|
+
const searchResults = store.search(intent, { limit: 3 });
|
|
24005
|
+
const terms = store.getDistinctiveTerms(indexed.sourceId);
|
|
24006
|
+
let filtered = `Indexed ${indexed.totalChunks} sections from ${sourceLabel}.
|
|
24007
|
+
`;
|
|
24008
|
+
filtered += `${searchResults.results.length} sections matched "${intent}":
|
|
24009
|
+
|
|
24010
|
+
`;
|
|
24011
|
+
for (const hit of searchResults.results) {
|
|
24012
|
+
filtered += ` - **${hit.title}**: ${hit.snippet.slice(0, 200)}
|
|
24013
|
+
`;
|
|
23337
24014
|
}
|
|
23338
|
-
|
|
24015
|
+
if (terms.length > 0 && config3.compressionLevel !== "ultra") {
|
|
24016
|
+
filtered += `
|
|
24017
|
+
Searchable terms: ${terms.join(", ")}
|
|
24018
|
+
`;
|
|
24019
|
+
}
|
|
24020
|
+
filtered += "\nUse search(queries: [...]) to retrieve full content of any section.";
|
|
24021
|
+
return compactLabel(filtered, config3.compressionLevel);
|
|
24022
|
+
};
|
|
24023
|
+
}
|
|
24024
|
+
|
|
24025
|
+
// src/util/version.ts
|
|
24026
|
+
import { readFileSync as readFileSync4 } from "node:fs";
|
|
24027
|
+
import { dirname, resolve as resolve4 } from "node:path";
|
|
24028
|
+
import { fileURLToPath } from "node:url";
|
|
24029
|
+
var __dirname = dirname(fileURLToPath(import.meta.url));
|
|
24030
|
+
function getVersion(fallback = "1.0.0") {
|
|
24031
|
+
try {
|
|
24032
|
+
const pkgPath = resolve4(__dirname, "..", "..", "package.json");
|
|
24033
|
+
const pkg = JSON.parse(readFileSync4(pkgPath, "utf-8"));
|
|
24034
|
+
return pkg.version ?? fallback;
|
|
24035
|
+
} catch {
|
|
24036
|
+
return fallback;
|
|
24037
|
+
}
|
|
24038
|
+
}
|
|
24039
|
+
|
|
24040
|
+
// src/server.ts
|
|
24041
|
+
var projectDir = process.env.CLAUDE_PROJECT_DIR ?? process.cwd();
|
|
24042
|
+
var MAX_CONCURRENT_EXECUTIONS = 8;
|
|
24043
|
+
var EXECUTION_LIMIT_ERROR = "Error: too many concurrent executions. Try again shortly.";
|
|
24044
|
+
async function createServer(config3) {
|
|
24045
|
+
const version2 = getVersion();
|
|
24046
|
+
debug("Version:", version2);
|
|
24047
|
+
cleanupStaleDbs();
|
|
24048
|
+
const runtimes = await detectRuntimes();
|
|
24049
|
+
const bunDetected = hasBun(runtimes);
|
|
24050
|
+
debug("Runtimes detected:", runtimes.size);
|
|
24051
|
+
const executor = new SubprocessExecutor(runtimes, config3);
|
|
24052
|
+
let store;
|
|
24053
|
+
let dbFallback = false;
|
|
24054
|
+
try {
|
|
24055
|
+
store = new ContentStore({ persistDb: config3.persistDb, dbDir: config3.dbDir });
|
|
24056
|
+
} catch (e) {
|
|
24057
|
+
debug("Failed to create DB, falling back to in-memory:", e);
|
|
24058
|
+
store = new ContentStore(":memory:");
|
|
24059
|
+
dbFallback = true;
|
|
24060
|
+
}
|
|
24061
|
+
const cumulativeFile = config3.persistDb ? join4(config3.dbDir ?? join4(projectDir, ".context-compress"), "stats.json") : void 0;
|
|
24062
|
+
const tracker = new SessionTracker(cumulativeFile);
|
|
24063
|
+
let activeExecutions = 0;
|
|
24064
|
+
async function withExecutionLimit(fn) {
|
|
24065
|
+
if (activeExecutions >= MAX_CONCURRENT_EXECUTIONS) {
|
|
24066
|
+
throw new Error(EXECUTION_LIMIT_ERROR);
|
|
24067
|
+
}
|
|
24068
|
+
activeExecutions++;
|
|
24069
|
+
try {
|
|
24070
|
+
return await fn();
|
|
24071
|
+
} finally {
|
|
24072
|
+
activeExecutions--;
|
|
24073
|
+
}
|
|
24074
|
+
}
|
|
24075
|
+
const applyIntentFilter = createIntentFilter({ config: config3, store, tracker });
|
|
24076
|
+
const shutdown = () => {
|
|
24077
|
+
try {
|
|
24078
|
+
tracker.saveCumulative();
|
|
24079
|
+
} catch {
|
|
24080
|
+
}
|
|
24081
|
+
try {
|
|
24082
|
+
executor.shutdown();
|
|
24083
|
+
} catch {
|
|
24084
|
+
}
|
|
24085
|
+
try {
|
|
24086
|
+
store.close();
|
|
24087
|
+
} catch {
|
|
24088
|
+
}
|
|
24089
|
+
};
|
|
24090
|
+
process.on("SIGINT", shutdown);
|
|
24091
|
+
process.on("SIGTERM", shutdown);
|
|
24092
|
+
process.on("beforeExit", shutdown);
|
|
24093
|
+
process.on("uncaughtException", (err) => {
|
|
24094
|
+
debug("Uncaught exception:", err);
|
|
24095
|
+
shutdown();
|
|
24096
|
+
process.exit(1);
|
|
24097
|
+
});
|
|
24098
|
+
process.on("unhandledRejection", (err) => {
|
|
24099
|
+
debug("Unhandled rejection:", err);
|
|
24100
|
+
shutdown();
|
|
24101
|
+
process.exit(1);
|
|
24102
|
+
});
|
|
24103
|
+
const server2 = new McpServer({
|
|
24104
|
+
name: "context-compress",
|
|
24105
|
+
version: version2
|
|
24106
|
+
});
|
|
24107
|
+
const ctx = {
|
|
24108
|
+
config: config3,
|
|
24109
|
+
store,
|
|
24110
|
+
tracker,
|
|
24111
|
+
executor,
|
|
24112
|
+
projectDir,
|
|
24113
|
+
bunDetected,
|
|
24114
|
+
dbFallback,
|
|
24115
|
+
withExecutionLimit,
|
|
24116
|
+
applyIntentFilter
|
|
24117
|
+
};
|
|
24118
|
+
registerExecuteTool(server2, ctx);
|
|
24119
|
+
registerExecuteFileTool(server2, ctx);
|
|
24120
|
+
registerIndexTool(server2, ctx);
|
|
24121
|
+
registerSearchTool(server2, ctx);
|
|
24122
|
+
registerFetchAndIndexTool(server2, ctx);
|
|
24123
|
+
registerBatchExecuteTool(server2, ctx);
|
|
24124
|
+
registerStatsTool(server2, ctx);
|
|
24125
|
+
registerDiscoverTool(server2, ctx);
|
|
23339
24126
|
return {
|
|
23340
24127
|
async start() {
|
|
23341
24128
|
const transport = new StdioServerTransport();
|
|
@@ -23344,79 +24131,6 @@ Searchable terms: ${terms.join(", ")}`;
|
|
|
23344
24131
|
}
|
|
23345
24132
|
};
|
|
23346
24133
|
}
|
|
23347
|
-
function buildFetchCode(url, resolvedIp) {
|
|
23348
|
-
let fetchSetup;
|
|
23349
|
-
if (resolvedIp) {
|
|
23350
|
-
const pinnedUrl = new URL(url);
|
|
23351
|
-
const originalHost = pinnedUrl.host;
|
|
23352
|
-
pinnedUrl.hostname = resolvedIp;
|
|
23353
|
-
fetchSetup = `
|
|
23354
|
-
const url = ${JSON.stringify(pinnedUrl.toString())};
|
|
23355
|
-
const resp = await fetch(url, { headers: { 'Host': ${JSON.stringify(originalHost)} }, redirect: 'error' });`;
|
|
23356
|
-
} else {
|
|
23357
|
-
fetchSetup = `
|
|
23358
|
-
const url = ${JSON.stringify(url)};
|
|
23359
|
-
const resp = await fetch(url, { redirect: 'error' });`;
|
|
23360
|
-
}
|
|
23361
|
-
return `${fetchSetup}
|
|
23362
|
-
if (!resp.ok) { console.error("HTTP " + resp.status); process.exit(1); }
|
|
23363
|
-
const cl = resp.headers.get('content-length');
|
|
23364
|
-
if (cl && parseInt(cl, 10) > 10 * 1024 * 1024) {
|
|
23365
|
-
console.error("Response too large: " + cl + " bytes"); process.exit(1);
|
|
23366
|
-
}
|
|
23367
|
-
const html = await resp.text();
|
|
23368
|
-
if (html.length > 10 * 1024 * 1024) {
|
|
23369
|
-
console.error("Response body too large: " + html.length + " chars"); process.exit(1);
|
|
23370
|
-
}
|
|
23371
|
-
|
|
23372
|
-
// Strip unwanted tags
|
|
23373
|
-
let md = html
|
|
23374
|
-
.replace(/<script[^>]*>[\\s\\S]*?<\\/script>/gi, "")
|
|
23375
|
-
.replace(/<style[^>]*>[\\s\\S]*?<\\/style>/gi, "")
|
|
23376
|
-
.replace(/<nav[^>]*>[\\s\\S]*?<\\/nav>/gi, "")
|
|
23377
|
-
.replace(/<header[^>]*>[\\s\\S]*?<\\/header>/gi, "")
|
|
23378
|
-
.replace(/<footer[^>]*>[\\s\\S]*?<\\/footer>/gi, "");
|
|
23379
|
-
|
|
23380
|
-
// Convert headings
|
|
23381
|
-
md = md.replace(/<h1[^>]*>(.*?)<\\/h1>/gi, "# $1\\n");
|
|
23382
|
-
md = md.replace(/<h2[^>]*>(.*?)<\\/h2>/gi, "## $1\\n");
|
|
23383
|
-
md = md.replace(/<h3[^>]*>(.*?)<\\/h3>/gi, "### $1\\n");
|
|
23384
|
-
md = md.replace(/<h4[^>]*>(.*?)<\\/h4>/gi, "#### $1\\n");
|
|
23385
|
-
|
|
23386
|
-
// Convert code blocks
|
|
23387
|
-
md = md.replace(/<pre[^>]*><code[^>]*>(.*?)<\\/code><\\/pre>/gis, "\`\`\`\\n$1\\n\`\`\`\\n");
|
|
23388
|
-
md = md.replace(/<code[^>]*>(.*?)<\\/code>/gi, "\`$1\`");
|
|
23389
|
-
|
|
23390
|
-
// Convert links
|
|
23391
|
-
md = md.replace(/<a[^>]*href="([^"]*)"[^>]*>(.*?)<\\/a>/gi, "[$2]($1)");
|
|
23392
|
-
|
|
23393
|
-
// Convert lists
|
|
23394
|
-
md = md.replace(/<li[^>]*>(.*?)<\\/li>/gi, "- $1\\n");
|
|
23395
|
-
|
|
23396
|
-
// Convert paragraphs
|
|
23397
|
-
md = md.replace(/<p[^>]*>(.*?)<\\/p>/gis, "$1\\n\\n");
|
|
23398
|
-
md = md.replace(/<br\\s*\\/?>/gi, "\\n");
|
|
23399
|
-
|
|
23400
|
-
// Strip remaining tags
|
|
23401
|
-
md = md.replace(/<[^>]+>/g, "");
|
|
23402
|
-
|
|
23403
|
-
// Decode entities
|
|
23404
|
-
md = md.replace(/</g, "<")
|
|
23405
|
-
.replace(/>/g, ">")
|
|
23406
|
-
.replace(/"/g, '"')
|
|
23407
|
-
.replace(/'/g, "'")
|
|
23408
|
-
.replace(/'/g, "'")
|
|
23409
|
-
.replace(/ /g, " ")
|
|
23410
|
-
.replace(/&#(\\d+);/g, (_, n) => { const c = parseInt(n, 10); return c > 0 && c <= 0x10FFFF ? String.fromCodePoint(c) : ''; })
|
|
23411
|
-
.replace(/&#x([0-9a-fA-F]+);/g, (_, h) => { const c = parseInt(h, 16); return c > 0 && c <= 0x10FFFF ? String.fromCodePoint(c) : ''; })
|
|
23412
|
-
.replace(/&/g, "&");
|
|
23413
|
-
|
|
23414
|
-
// Clean whitespace
|
|
23415
|
-
md = md.replace(/\\n{3,}/g, "\\n\\n").trim();
|
|
23416
|
-
|
|
23417
|
-
console.log(md);
|
|
23418
|
-
`;
|
|
23419
|
-
}
|
|
23420
24134
|
|
|
23421
24135
|
// src/index.ts
|
|
23422
24136
|
var config2 = loadConfig(process.env.CLAUDE_PROJECT_DIR);
|