@ajdev0/token-shrink 2.0.1 → 2.0.3
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 +52 -14
- package/dist/chunk-FYCLLG7F.js +847 -0
- package/dist/chunk-FYCLLG7F.js.map +1 -0
- package/dist/{chunk-7H6PGILN.js → chunk-RI64PBQ5.js} +24 -8
- package/dist/chunk-RI64PBQ5.js.map +1 -0
- package/dist/chunk-RLHEIKPR.js +894 -0
- package/dist/chunk-RLHEIKPR.js.map +1 -0
- package/dist/cli-DzY7l7Rr.d.cts +182 -0
- package/dist/cli-DzY7l7Rr.d.ts +182 -0
- package/dist/cli.cjs +396 -118
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.d.cts +2 -1
- package/dist/cli.d.ts +2 -1
- package/dist/cli.js +2 -2
- package/dist/index.cjs +1200 -151
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +160 -20
- package/dist/index.d.ts +160 -20
- package/dist/index.js +37 -3
- package/dist/index.js.map +1 -1
- package/dist/mcp.cjs +1132 -136
- package/dist/mcp.cjs.map +1 -1
- package/dist/mcp.d.cts +21 -6
- package/dist/mcp.d.ts +21 -6
- package/dist/mcp.js +6 -2
- package/hooks/claude/README.md +29 -0
- package/hooks/claude/git-impact.mjs +47 -0
- package/hooks/claude/settings.example.json +24 -0
- package/package.json +3 -1
- package/dist/chunk-7H6PGILN.js.map +0 -1
- package/dist/chunk-ERJ3LBJ4.js +0 -198
- package/dist/chunk-ERJ3LBJ4.js.map +0 -1
- package/dist/chunk-HRF3BIOV.js +0 -518
- package/dist/chunk-HRF3BIOV.js.map +0 -1
- package/dist/cli-EpVqinpB.d.cts +0 -96
- package/dist/cli-EpVqinpB.d.ts +0 -96
package/dist/mcp.cjs
CHANGED
|
@@ -184,6 +184,8 @@ __export(mcp_exports, {
|
|
|
184
184
|
CLINE_RULE_SENTINEL: () => CLINE_RULE_SENTINEL,
|
|
185
185
|
CURSOR_RULE_PATH: () => CURSOR_RULE_PATH,
|
|
186
186
|
RULE_TARGET_PATH: () => RULE_TARGET_PATH,
|
|
187
|
+
RULE_VERSION: () => RULE_VERSION,
|
|
188
|
+
RULE_VERSION_MARKER: () => RULE_VERSION_MARKER,
|
|
187
189
|
createAutoRule: () => createAutoRule,
|
|
188
190
|
createCursorRule: () => createCursorRule,
|
|
189
191
|
startMcpServer: () => startMcpServer
|
|
@@ -192,104 +194,180 @@ module.exports = __toCommonJS(mcp_exports);
|
|
|
192
194
|
var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
193
195
|
var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
194
196
|
var import_zod = require("zod");
|
|
195
|
-
var
|
|
196
|
-
var
|
|
197
|
+
var import_node_fs7 = __toESM(require("fs"), 1);
|
|
198
|
+
var import_node_path9 = __toESM(require("path"), 1);
|
|
197
199
|
|
|
198
200
|
// src/server/assembler.ts
|
|
199
201
|
var import_node_fs = __toESM(require("fs"), 1);
|
|
200
202
|
var import_node_path = __toESM(require("path"), 1);
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
const
|
|
204
|
-
|
|
205
|
-
|
|
203
|
+
var MARKDOWN_OVERHEAD_TOKENS = 40;
|
|
204
|
+
function assembleMany(activeFiles, cache, opts = {}) {
|
|
205
|
+
const {
|
|
206
|
+
includeStats = false,
|
|
207
|
+
maxSkeletons = 50,
|
|
208
|
+
maxTokens,
|
|
209
|
+
pruneActiveFile = false
|
|
210
|
+
} = opts;
|
|
211
|
+
const absList = activeFiles.map((p) => import_node_path.default.resolve(p));
|
|
212
|
+
const sources = absList.map((abs) => {
|
|
213
|
+
try {
|
|
214
|
+
return import_node_fs.default.readFileSync(abs, "utf8");
|
|
215
|
+
} catch {
|
|
216
|
+
return "";
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
const activeSet = new Set(absList);
|
|
220
|
+
const ring0Texts = absList.map((abs, i) => {
|
|
221
|
+
const entry = cache.get(abs);
|
|
222
|
+
return pruneActiveFile && entry ? entry.skeleton : sources[i];
|
|
223
|
+
});
|
|
206
224
|
const included = [];
|
|
207
225
|
const unresolved = [];
|
|
208
226
|
const seen = /* @__PURE__ */ new Set();
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
included.push({ filePath: imp, language: entry.language });
|
|
219
|
-
if (included.length >= maxSkeletons) break;
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
if (!activeEntry && activeSource) {
|
|
223
|
-
const specifiers = extractSpecifiers(activeSource);
|
|
224
|
-
for (const spec of specifiers) {
|
|
225
|
-
const resolved = resolveLocal(abs, spec);
|
|
226
|
-
if (!resolved) {
|
|
227
|
-
unresolved.push(spec);
|
|
228
|
-
continue;
|
|
229
|
-
}
|
|
230
|
-
if (seen.has(resolved)) continue;
|
|
231
|
-
seen.add(resolved);
|
|
232
|
-
const cached = cache.get(resolved);
|
|
233
|
-
included.push({ filePath: resolved, language: cached?.language ?? null });
|
|
234
|
-
if (included.length >= maxSkeletons) break;
|
|
227
|
+
const addUnresolved = (p) => {
|
|
228
|
+
if (!unresolved.includes(p)) unresolved.push(p);
|
|
229
|
+
};
|
|
230
|
+
const addIncluded = (p, allowUncached) => {
|
|
231
|
+
if (seen.has(p)) return;
|
|
232
|
+
const entry = cache.get(p);
|
|
233
|
+
if (!entry && !allowUncached) {
|
|
234
|
+
addUnresolved(p);
|
|
235
|
+
return;
|
|
235
236
|
}
|
|
237
|
+
seen.add(p);
|
|
238
|
+
included.push({ filePath: p, language: entry?.language ?? null });
|
|
239
|
+
};
|
|
240
|
+
for (let i = 0; i < absList.length; i++) {
|
|
241
|
+
const abs = absList[i];
|
|
242
|
+
const entry = cache.get(abs);
|
|
243
|
+
if (entry) {
|
|
244
|
+
for (const imp of entry.imports) {
|
|
245
|
+
if (activeSet.has(imp)) continue;
|
|
246
|
+
addIncluded(imp, false);
|
|
247
|
+
}
|
|
248
|
+
} else if (sources[i]) {
|
|
249
|
+
for (const spec of extractSpecifiers(sources[i])) {
|
|
250
|
+
const resolved = resolveLocal(abs, spec);
|
|
251
|
+
if (!resolved) {
|
|
252
|
+
addUnresolved(spec);
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
if (activeSet.has(resolved)) continue;
|
|
256
|
+
addIncluded(resolved, true);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
const ordered = maxTokens === void 0 ? included : rankByRelevance(included, cache);
|
|
261
|
+
const budget = maxTokens === void 0 ? void 0 : Math.max(0, maxTokens - MARKDOWN_OVERHEAD_TOKENS);
|
|
262
|
+
const selected = [];
|
|
263
|
+
let trimmed = 0;
|
|
264
|
+
let dependencyTokens = 0;
|
|
265
|
+
for (const cand of ordered) {
|
|
266
|
+
if (selected.length >= maxSkeletons) break;
|
|
267
|
+
const entry = cache.get(cand.filePath);
|
|
268
|
+
const cost = entry ? approximateTokens(entry.skeleton) : 8;
|
|
269
|
+
if (budget !== void 0 && dependencyTokens + cost > budget) {
|
|
270
|
+
trimmed++;
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
selected.push(cand);
|
|
274
|
+
dependencyTokens += cost;
|
|
236
275
|
}
|
|
276
|
+
const activeTokens = ring0Texts.reduce(
|
|
277
|
+
(acc, t) => acc + approximateTokens(t),
|
|
278
|
+
0
|
|
279
|
+
);
|
|
237
280
|
const lines = [];
|
|
238
281
|
lines.push("# Compressed Code Context", "");
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
282
|
+
if (absList.length === 1) {
|
|
283
|
+
lines.push(`Active file: \`${rel(absList[0])}\``, "");
|
|
284
|
+
} else {
|
|
285
|
+
lines.push("Active files:", "");
|
|
286
|
+
for (const p of absList) lines.push(`- \`${rel(p)}\``);
|
|
287
|
+
lines.push("");
|
|
288
|
+
}
|
|
245
289
|
lines.push(
|
|
246
|
-
|
|
290
|
+
absList.length === 1 ? "## Ring 0 \u2014 Active file (full text)" : "## Ring 0 \u2014 Active files (full text)",
|
|
291
|
+
""
|
|
292
|
+
);
|
|
293
|
+
for (let i = 0; i < absList.length; i++) {
|
|
294
|
+
if (absList.length > 1) lines.push(`### \`${rel(absList[i])}\``, "");
|
|
295
|
+
const body = ring0Texts[i].trim() || "(empty or unreadable file)";
|
|
296
|
+
lines.push(`\`\`\`${ext(absList[i])}`, body, "```", "");
|
|
297
|
+
}
|
|
298
|
+
lines.push(
|
|
299
|
+
`## Ring 1 \u2014 Pruned dependencies (${selected.length})`,
|
|
247
300
|
"",
|
|
248
301
|
"Implementation bodies removed; type signatures, interfaces and exports retained.",
|
|
249
302
|
""
|
|
250
303
|
);
|
|
251
|
-
if (
|
|
304
|
+
if (selected.length === 0) {
|
|
252
305
|
lines.push("_No local dependency skeletons available._", "");
|
|
253
306
|
}
|
|
254
|
-
for (const inc of
|
|
307
|
+
for (const inc of selected) {
|
|
255
308
|
const entry = cache.get(inc.filePath);
|
|
256
309
|
const label = rel(inc.filePath);
|
|
257
310
|
lines.push(`### \`${label}\``, "");
|
|
258
311
|
if (entry) {
|
|
259
|
-
lines.push(`\`\`\`${ext(inc.filePath)}
|
|
260
|
-
lines.push(entry.skeleton.trim());
|
|
261
|
-
lines.push("```", "");
|
|
312
|
+
lines.push(`\`\`\`${ext(inc.filePath)}`, entry.skeleton.trim(), "```", "");
|
|
262
313
|
} else {
|
|
263
314
|
lines.push("_Unindexed file._", "");
|
|
264
315
|
}
|
|
265
316
|
}
|
|
317
|
+
if (maxTokens !== void 0 && trimmed > 0) {
|
|
318
|
+
lines.push(
|
|
319
|
+
`_Note: token budget of ${maxTokens} excluded ${trimmed} lower-priority dependenc${trimmed === 1 ? "y" : "ies"}._`,
|
|
320
|
+
""
|
|
321
|
+
);
|
|
322
|
+
}
|
|
266
323
|
if (unresolved.length > 0) {
|
|
267
324
|
lines.push("## Unresolved imports", "");
|
|
268
325
|
for (const u of unresolved) lines.push(`- \`${u}\``);
|
|
269
326
|
lines.push("");
|
|
270
327
|
}
|
|
271
328
|
if (includeStats) {
|
|
272
|
-
const
|
|
273
|
-
(acc, inc) => {
|
|
274
|
-
const e = cache.get(inc.filePath);
|
|
275
|
-
return e ? acc + approximateTokens(e.skeleton) : acc;
|
|
276
|
-
},
|
|
277
|
-
0
|
|
278
|
-
);
|
|
279
|
-
lines.push("---", "");
|
|
329
|
+
const budgetNote = maxTokens !== void 0 ? ` \xB7 budget ${maxTokens} (${trimmed} deps trimmed)` : "";
|
|
280
330
|
lines.push(
|
|
281
|
-
|
|
331
|
+
"---",
|
|
332
|
+
"",
|
|
333
|
+
`_Token estimate \u2014 active: ${activeTokens} \xB7 pruned deps: ${dependencyTokens}${budgetNote}._`,
|
|
282
334
|
""
|
|
283
335
|
);
|
|
284
336
|
}
|
|
285
337
|
return {
|
|
286
338
|
markdown: lines.join("\n"),
|
|
287
|
-
activeFilePath:
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
339
|
+
activeFilePath: absList[0] ?? "",
|
|
340
|
+
activeFilePaths: absList,
|
|
341
|
+
activeSource: sources.join("\n"),
|
|
342
|
+
included: selected,
|
|
343
|
+
unresolved,
|
|
344
|
+
tokenStats: {
|
|
345
|
+
activeTokens,
|
|
346
|
+
dependencyTokens,
|
|
347
|
+
totalTokens: activeTokens + dependencyTokens,
|
|
348
|
+
...maxTokens !== void 0 ? { budget: maxTokens } : {},
|
|
349
|
+
trimmed
|
|
350
|
+
}
|
|
291
351
|
};
|
|
292
352
|
}
|
|
353
|
+
function rankByRelevance(candidates, cache) {
|
|
354
|
+
const fanIn = /* @__PURE__ */ new Map();
|
|
355
|
+
for (const entry of cache.values()) {
|
|
356
|
+
for (const imp of entry.imports) fanIn.set(imp, (fanIn.get(imp) ?? 0) + 1);
|
|
357
|
+
}
|
|
358
|
+
return candidates.map((cand, idx) => ({ cand, idx })).sort((a, b) => {
|
|
359
|
+
const fa = fanIn.get(a.cand.filePath) ?? 0;
|
|
360
|
+
const fb = fanIn.get(b.cand.filePath) ?? 0;
|
|
361
|
+
if (fa !== fb) return fb - fa;
|
|
362
|
+
const da = pathDepth(a.cand.filePath);
|
|
363
|
+
const db = pathDepth(b.cand.filePath);
|
|
364
|
+
if (da !== db) return da - db;
|
|
365
|
+
return a.idx - b.idx;
|
|
366
|
+
}).map((x) => x.cand);
|
|
367
|
+
}
|
|
368
|
+
function pathDepth(p) {
|
|
369
|
+
return p.split(/[/\\]+/).filter(Boolean).length;
|
|
370
|
+
}
|
|
293
371
|
function extractSpecifiers(source) {
|
|
294
372
|
const found = [];
|
|
295
373
|
const re = /(?:from\s+['"`]|import\s+['"`]|require\s*\(\s*['"`])([^'"`]+)['"`]/g;
|
|
@@ -346,12 +424,12 @@ function approximateTokens(text) {
|
|
|
346
424
|
}
|
|
347
425
|
|
|
348
426
|
// src/watcher/sync.ts
|
|
349
|
-
var
|
|
350
|
-
var
|
|
427
|
+
var import_node_fs4 = __toESM(require("fs"), 1);
|
|
428
|
+
var import_node_path6 = __toESM(require("path"), 1);
|
|
351
429
|
var import_node_crypto = require("crypto");
|
|
352
430
|
var import_chokidar = __toESM(require("chokidar"), 1);
|
|
353
431
|
|
|
354
|
-
// src/parser/
|
|
432
|
+
// src/parser/analyze.ts
|
|
355
433
|
var import_web_tree_sitter = __toESM(require("web-tree-sitter"), 1);
|
|
356
434
|
var import_node_module = require("module");
|
|
357
435
|
var import_node_path4 = __toESM(require("path"), 1);
|
|
@@ -426,7 +504,97 @@ async function getGrammar(spec, force = false) {
|
|
|
426
504
|
return download(spec);
|
|
427
505
|
}
|
|
428
506
|
|
|
429
|
-
// src/parser/
|
|
507
|
+
// src/parser/symbols.ts
|
|
508
|
+
var KIND_BY_TYPE = {
|
|
509
|
+
// TypeScript / JavaScript / TSX / JSX
|
|
510
|
+
function_declaration: "function",
|
|
511
|
+
generator_function_declaration: "function",
|
|
512
|
+
function_expression: "function",
|
|
513
|
+
function_signature_item: "function",
|
|
514
|
+
method_definition: "method",
|
|
515
|
+
method_signature: "method",
|
|
516
|
+
class_declaration: "class",
|
|
517
|
+
abstract_class_declaration: "class",
|
|
518
|
+
interface_declaration: "interface",
|
|
519
|
+
enum_declaration: "enum",
|
|
520
|
+
type_alias_declaration: "type",
|
|
521
|
+
// Python
|
|
522
|
+
function_definition: "function",
|
|
523
|
+
class_definition: "class",
|
|
524
|
+
// Go
|
|
525
|
+
method_declaration: "method",
|
|
526
|
+
type_spec: "type",
|
|
527
|
+
// Rust
|
|
528
|
+
function_item: "function",
|
|
529
|
+
struct_item: "class",
|
|
530
|
+
enum_item: "enum",
|
|
531
|
+
trait_item: "interface",
|
|
532
|
+
type_item: "type",
|
|
533
|
+
// Java / Kotlin / PHP / Dart / Swift (best-effort generic names)
|
|
534
|
+
constructor_declaration: "method",
|
|
535
|
+
module_declaration: "class",
|
|
536
|
+
protocol_declaration: "interface"
|
|
537
|
+
};
|
|
538
|
+
var SIGNATURE_MAX = 200;
|
|
539
|
+
function collectSymbols(root, source) {
|
|
540
|
+
const out = [];
|
|
541
|
+
const walk2 = (node) => {
|
|
542
|
+
const kind = KIND_BY_TYPE[node.type];
|
|
543
|
+
if (kind) {
|
|
544
|
+
const nameNode = node.childForFieldName("name");
|
|
545
|
+
if (nameNode && nameNode.text.trim()) {
|
|
546
|
+
out.push(symbolFrom(nameNode.text.trim(), kind, node.startIndex, node.endIndex, source));
|
|
547
|
+
}
|
|
548
|
+
} else if (node.type === "variable_declarator") {
|
|
549
|
+
const value = node.childForFieldName("value");
|
|
550
|
+
const valueType = value?.type;
|
|
551
|
+
if (valueType === "arrow_function" || valueType === "function_expression") {
|
|
552
|
+
const nameNode = node.childForFieldName("name");
|
|
553
|
+
if (nameNode && nameNode.text.trim()) {
|
|
554
|
+
out.push(
|
|
555
|
+
symbolFrom(nameNode.text.trim(), "arrow", node.startIndex, node.endIndex, source)
|
|
556
|
+
);
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
561
|
+
const child = node.child(i);
|
|
562
|
+
if (child) walk2(child);
|
|
563
|
+
}
|
|
564
|
+
};
|
|
565
|
+
walk2(root);
|
|
566
|
+
return out;
|
|
567
|
+
}
|
|
568
|
+
function matchSymbols(symbols, query, kind) {
|
|
569
|
+
const q = query.trim();
|
|
570
|
+
if (!q) return [];
|
|
571
|
+
const pool = kind ? symbols.filter((s) => s.kind === kind) : symbols;
|
|
572
|
+
const byName = pool.filter((s) => s.name === q);
|
|
573
|
+
if (byName.length > 0) return byName;
|
|
574
|
+
const byNameCi = pool.filter((s) => s.name.toLowerCase() === q.toLowerCase());
|
|
575
|
+
if (byNameCi.length > 0) return byNameCi;
|
|
576
|
+
const lower = q.toLowerCase();
|
|
577
|
+
const bySubstring = pool.filter((s) => s.name.toLowerCase().includes(lower));
|
|
578
|
+
return bySubstring.length > 0 ? bySubstring : pool.filter((s) => (s.signature ?? "").toLowerCase().includes(lower));
|
|
579
|
+
}
|
|
580
|
+
function symbolFrom(name, kind, start, end, source) {
|
|
581
|
+
return {
|
|
582
|
+
name,
|
|
583
|
+
kind,
|
|
584
|
+
line: source.slice(0, start).split("\n").length,
|
|
585
|
+
start,
|
|
586
|
+
end,
|
|
587
|
+
signature: signaturePreview(source, start, end)
|
|
588
|
+
};
|
|
589
|
+
}
|
|
590
|
+
function signaturePreview(source, start, end) {
|
|
591
|
+
const nl = source.indexOf("\n", start);
|
|
592
|
+
const endOfLine = nl === -1 ? end : nl;
|
|
593
|
+
const first = source.slice(start, Math.min(endOfLine, end)).trim();
|
|
594
|
+
return first.length > SIGNATURE_MAX ? `${first.slice(0, SIGNATURE_MAX)}\u2026` : first;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
// src/parser/analyze.ts
|
|
430
598
|
var languageCache = /* @__PURE__ */ new Map();
|
|
431
599
|
var initPromise = null;
|
|
432
600
|
function ensureParserInit() {
|
|
@@ -459,35 +627,6 @@ function loadLanguage(spec, force = false) {
|
|
|
459
627
|
return language;
|
|
460
628
|
})();
|
|
461
629
|
}
|
|
462
|
-
async function prune(filePath, source, opts = {}) {
|
|
463
|
-
const spec = languageForFile(filePath);
|
|
464
|
-
if (!spec || spec.rules.length === 0) {
|
|
465
|
-
return { code: source, language: null, removed: 0 };
|
|
466
|
-
}
|
|
467
|
-
const language = await loadLanguage(spec, opts.forceDownload);
|
|
468
|
-
const parser = new import_web_tree_sitter.default();
|
|
469
|
-
parser.setLanguage(language);
|
|
470
|
-
const tree = parser.parse(source);
|
|
471
|
-
const ranges = [];
|
|
472
|
-
for (const rule of spec.rules) {
|
|
473
|
-
const query = language.query(rule.query);
|
|
474
|
-
const captures = query.captures(tree.rootNode);
|
|
475
|
-
for (const cap of captures) {
|
|
476
|
-
if (cap.node.startIndex === cap.node.endIndex) continue;
|
|
477
|
-
if (rule.replacement.keepIf?.test(cap.node.text)) continue;
|
|
478
|
-
ranges.push({
|
|
479
|
-
start: cap.node.startIndex,
|
|
480
|
-
end: cap.node.endIndex,
|
|
481
|
-
token: rule.replacement.token
|
|
482
|
-
});
|
|
483
|
-
}
|
|
484
|
-
query.delete();
|
|
485
|
-
}
|
|
486
|
-
tree.delete();
|
|
487
|
-
parser.delete();
|
|
488
|
-
const { code, removed } = spliceRanges(source, ranges);
|
|
489
|
-
return { code, language: spec.name, removed };
|
|
490
|
-
}
|
|
491
630
|
function spliceRanges(source, ranges) {
|
|
492
631
|
let removed = 0;
|
|
493
632
|
const sorted = [...ranges].sort((a, b) => b.start - a.start);
|
|
@@ -499,6 +638,163 @@ function spliceRanges(source, ranges) {
|
|
|
499
638
|
}
|
|
500
639
|
return { code: out, removed };
|
|
501
640
|
}
|
|
641
|
+
async function analyze(filePath, source, opts = {}) {
|
|
642
|
+
const spec = languageForFile(filePath);
|
|
643
|
+
if (!spec || spec.rules.length === 0) {
|
|
644
|
+
return { code: source, language: null, removed: 0, symbols: [] };
|
|
645
|
+
}
|
|
646
|
+
let language;
|
|
647
|
+
try {
|
|
648
|
+
language = await loadLanguage(spec, opts.forceDownload);
|
|
649
|
+
} catch {
|
|
650
|
+
return { code: source, language: null, removed: 0, symbols: [] };
|
|
651
|
+
}
|
|
652
|
+
const parser = new import_web_tree_sitter.default();
|
|
653
|
+
parser.setLanguage(language);
|
|
654
|
+
const tree = parser.parse(source);
|
|
655
|
+
try {
|
|
656
|
+
const symbols = collectSymbols(tree.rootNode, source);
|
|
657
|
+
if (opts.skipPrune) {
|
|
658
|
+
return { code: source, language: spec.name, removed: 0, symbols };
|
|
659
|
+
}
|
|
660
|
+
const keepRanges = [
|
|
661
|
+
...opts.keepBlocksInside ?? [],
|
|
662
|
+
...opts.preserveAnnotations?.length ? annotatedKeepRanges(symbols, source, opts.preserveAnnotations) : []
|
|
663
|
+
];
|
|
664
|
+
const isProtected = (start, end) => keepRanges.some((k) => k.start <= start && end <= k.end);
|
|
665
|
+
const ranges = [];
|
|
666
|
+
for (const rule of spec.rules) {
|
|
667
|
+
const query = language.query(rule.query);
|
|
668
|
+
const captures = query.captures(tree.rootNode);
|
|
669
|
+
for (const cap of captures) {
|
|
670
|
+
if (cap.node.startIndex === cap.node.endIndex) continue;
|
|
671
|
+
if (rule.replacement.keepIf?.test(cap.node.text)) continue;
|
|
672
|
+
if (isProtected(cap.node.startIndex, cap.node.endIndex)) continue;
|
|
673
|
+
ranges.push({
|
|
674
|
+
start: cap.node.startIndex,
|
|
675
|
+
end: cap.node.endIndex,
|
|
676
|
+
token: rule.replacement.token
|
|
677
|
+
});
|
|
678
|
+
}
|
|
679
|
+
query.delete();
|
|
680
|
+
}
|
|
681
|
+
const { code, removed } = spliceRanges(source, ranges);
|
|
682
|
+
return { code, language: spec.name, removed, symbols };
|
|
683
|
+
} finally {
|
|
684
|
+
tree.delete();
|
|
685
|
+
parser.delete();
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
var ANNOTATION_WINDOW = 240;
|
|
689
|
+
function annotatedKeepRanges(symbols, source, annotations) {
|
|
690
|
+
if (annotations.length === 0) return [];
|
|
691
|
+
const markers = annotations.filter(Boolean).map(escapeRegExp);
|
|
692
|
+
if (markers.length === 0) return [];
|
|
693
|
+
const re = new RegExp(`@(${markers.join("|")})\\b`, "i");
|
|
694
|
+
return symbols.filter((s) => {
|
|
695
|
+
const windowStart = Math.max(0, s.start - ANNOTATION_WINDOW);
|
|
696
|
+
const tail = source.slice(windowStart, s.start);
|
|
697
|
+
const cut = Math.max(tail.lastIndexOf("}\n"), tail.lastIndexOf(";\n"));
|
|
698
|
+
const relevant = cut === -1 ? tail : tail.slice(cut + 1);
|
|
699
|
+
return re.test(relevant);
|
|
700
|
+
}).map((s) => ({ start: s.start, end: s.end }));
|
|
701
|
+
}
|
|
702
|
+
function escapeRegExp(text) {
|
|
703
|
+
return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
// src/config.ts
|
|
707
|
+
var import_node_fs3 = __toESM(require("fs"), 1);
|
|
708
|
+
var import_node_path5 = __toESM(require("path"), 1);
|
|
709
|
+
var CONFIG_FILE_NAME = ".tokenshrinkrc.json";
|
|
710
|
+
var EMPTY_CONFIG = {
|
|
711
|
+
ignorePatterns: [],
|
|
712
|
+
keepUnpruned: [],
|
|
713
|
+
preserveAnnotations: []
|
|
714
|
+
};
|
|
715
|
+
function configPathFor(root) {
|
|
716
|
+
return import_node_path5.default.join(root, CONFIG_FILE_NAME);
|
|
717
|
+
}
|
|
718
|
+
function loadConfig(root, warn) {
|
|
719
|
+
const file = configPathFor(root);
|
|
720
|
+
let raw;
|
|
721
|
+
try {
|
|
722
|
+
if (!import_node_fs3.default.existsSync(file)) return { ...EMPTY_CONFIG };
|
|
723
|
+
raw = JSON.parse(import_node_fs3.default.readFileSync(file, "utf8"));
|
|
724
|
+
} catch (err) {
|
|
725
|
+
warn?.(`Invalid ${CONFIG_FILE_NAME} at ${file}: ${err.message}`);
|
|
726
|
+
return { ...EMPTY_CONFIG };
|
|
727
|
+
}
|
|
728
|
+
return normalizeConfig(raw, warn, file);
|
|
729
|
+
}
|
|
730
|
+
function normalizeConfig(raw, warn, file) {
|
|
731
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
732
|
+
warn?.(`Invalid ${CONFIG_FILE_NAME}${file ? ` at ${file}` : ""}: expected a JSON object.`);
|
|
733
|
+
return { ...EMPTY_CONFIG };
|
|
734
|
+
}
|
|
735
|
+
const obj = raw;
|
|
736
|
+
const asStrings = (v) => Array.isArray(v) ? v.filter((x) => typeof x === "string") : [];
|
|
737
|
+
return {
|
|
738
|
+
ignorePatterns: asStrings(obj.ignorePatterns),
|
|
739
|
+
keepUnpruned: asStrings(obj.keepUnpruned),
|
|
740
|
+
preserveAnnotations: asStrings(obj.preserveAnnotations),
|
|
741
|
+
...typeof obj.autoWorkflow === "boolean" ? { autoWorkflow: obj.autoWorkflow } : {}
|
|
742
|
+
};
|
|
743
|
+
}
|
|
744
|
+
function matchesAny(file, patterns) {
|
|
745
|
+
const rel2 = normalizeSlashes(file);
|
|
746
|
+
for (const raw of patterns) {
|
|
747
|
+
if (!raw) continue;
|
|
748
|
+
const pattern = normalizeSlashes(raw).replace(/^\.\//, "");
|
|
749
|
+
if (matchesGlob(rel2, pattern)) return true;
|
|
750
|
+
}
|
|
751
|
+
return false;
|
|
752
|
+
}
|
|
753
|
+
function matchesGlob(file, pattern) {
|
|
754
|
+
if (file === pattern) return true;
|
|
755
|
+
if (!/[?*[]/.test(pattern)) {
|
|
756
|
+
return file.endsWith(`/${pattern}`);
|
|
757
|
+
}
|
|
758
|
+
const re = globToRegExp(pattern);
|
|
759
|
+
return re.test(file);
|
|
760
|
+
}
|
|
761
|
+
function globToRegExp(glob) {
|
|
762
|
+
let re = "^";
|
|
763
|
+
for (let i = 0; i < glob.length; i++) {
|
|
764
|
+
const c = glob[i];
|
|
765
|
+
if (c === "*") {
|
|
766
|
+
if (glob[i + 1] === "*") {
|
|
767
|
+
i++;
|
|
768
|
+
if (glob[i + 1] === "/") {
|
|
769
|
+
i++;
|
|
770
|
+
re += "(?:.*/)?";
|
|
771
|
+
} else {
|
|
772
|
+
re += ".*";
|
|
773
|
+
}
|
|
774
|
+
} else {
|
|
775
|
+
re += "[^/]*";
|
|
776
|
+
}
|
|
777
|
+
} else if (c === "?") {
|
|
778
|
+
re += "[^/]";
|
|
779
|
+
} else if (c === "[") {
|
|
780
|
+
const close = glob.indexOf("]", i);
|
|
781
|
+
if (close === -1) {
|
|
782
|
+
re += "\\[";
|
|
783
|
+
} else {
|
|
784
|
+
const inner = glob.slice(i + 1, close).replace(/\\/g, "\\\\");
|
|
785
|
+
re += `[${inner}]`;
|
|
786
|
+
i = close;
|
|
787
|
+
}
|
|
788
|
+
} else {
|
|
789
|
+
re += c.replace(/[.+^${}()|\\]/g, "\\$&");
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
re += "$";
|
|
793
|
+
return new RegExp(re);
|
|
794
|
+
}
|
|
795
|
+
function normalizeSlashes(p) {
|
|
796
|
+
return p.split(import_node_path5.default.sep).join("/").replace(/^\.\//, "");
|
|
797
|
+
}
|
|
502
798
|
|
|
503
799
|
// src/watcher/sync.ts
|
|
504
800
|
var DEFAULT_IGNORED = [
|
|
@@ -518,7 +814,7 @@ var ContextCache = class {
|
|
|
518
814
|
}
|
|
519
815
|
/** Returns the cached skeleton for a file, if present. */
|
|
520
816
|
getSkeleton(filePath) {
|
|
521
|
-
return this.entries.get(
|
|
817
|
+
return this.entries.get(import_node_path6.default.resolve(filePath)) ?? null;
|
|
522
818
|
}
|
|
523
819
|
get imports() {
|
|
524
820
|
return this.entries;
|
|
@@ -540,18 +836,18 @@ function extractImports(_filePath, source) {
|
|
|
540
836
|
function resolveImport(importer, specifier, root) {
|
|
541
837
|
if (!/^[.~@]/.test(specifier)) {
|
|
542
838
|
if (specifier.startsWith("@/")) {
|
|
543
|
-
return resolveCandidate(
|
|
839
|
+
return resolveCandidate(import_node_path6.default.join(root, specifier.slice(2)));
|
|
544
840
|
}
|
|
545
841
|
if (specifier.startsWith("@")) {
|
|
546
842
|
return null;
|
|
547
843
|
}
|
|
548
844
|
if (specifier.startsWith("~")) {
|
|
549
|
-
return resolveCandidate(
|
|
845
|
+
return resolveCandidate(import_node_path6.default.join(root, specifier.slice(1)));
|
|
550
846
|
}
|
|
551
847
|
return null;
|
|
552
848
|
}
|
|
553
|
-
const base =
|
|
554
|
-
return resolveCandidate(
|
|
849
|
+
const base = import_node_path6.default.dirname(import_node_path6.default.resolve(importer));
|
|
850
|
+
return resolveCandidate(import_node_path6.default.resolve(base, specifier));
|
|
555
851
|
}
|
|
556
852
|
function resolveCandidate(p) {
|
|
557
853
|
const candidates = [
|
|
@@ -574,29 +870,34 @@ function resolveCandidate(p) {
|
|
|
574
870
|
`${p}.h`,
|
|
575
871
|
`${p}.hpp`,
|
|
576
872
|
`${p}.php`,
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
873
|
+
import_node_path6.default.join(p, "index.ts"),
|
|
874
|
+
import_node_path6.default.join(p, "index.js"),
|
|
875
|
+
import_node_path6.default.join(p, "index.tsx"),
|
|
876
|
+
import_node_path6.default.join(p, "index.jsx"),
|
|
877
|
+
import_node_path6.default.join(p, "index.py")
|
|
582
878
|
];
|
|
583
879
|
for (const c of candidates) {
|
|
584
|
-
if (
|
|
880
|
+
if (import_node_fs4.default.existsSync(c) && import_node_fs4.default.statSync(c).isFile()) return import_node_path6.default.resolve(c);
|
|
585
881
|
}
|
|
586
882
|
return null;
|
|
587
883
|
}
|
|
588
884
|
function createWatcher(opts) {
|
|
589
885
|
const cache = new ContextCache();
|
|
590
|
-
const { root, ignored = DEFAULT_IGNORED, onIndexed } = opts;
|
|
886
|
+
const { root, ignored = DEFAULT_IGNORED, config, onIndexed, onRemoved } = opts;
|
|
887
|
+
const rel2 = (abs) => import_node_path6.default.relative(root, abs);
|
|
888
|
+
const effectiveIgnored = [
|
|
889
|
+
...ignored,
|
|
890
|
+
...config?.ignorePatterns?.length ? [(_abs) => matchesAny(rel2(String(_abs)), config.ignorePatterns)] : []
|
|
891
|
+
];
|
|
591
892
|
const pending = /* @__PURE__ */ new Map();
|
|
592
893
|
const DEBOUNCE_MS = 100;
|
|
593
894
|
async function handle(filePath) {
|
|
594
|
-
const abs =
|
|
895
|
+
const abs = import_node_path6.default.resolve(filePath);
|
|
595
896
|
let source;
|
|
596
897
|
try {
|
|
597
|
-
const st =
|
|
898
|
+
const st = import_node_fs4.default.statSync(abs);
|
|
598
899
|
if (!st.isFile()) return;
|
|
599
|
-
source =
|
|
900
|
+
source = import_node_fs4.default.readFileSync(abs, "utf8");
|
|
600
901
|
} catch {
|
|
601
902
|
return;
|
|
602
903
|
}
|
|
@@ -604,13 +905,18 @@ function createWatcher(opts) {
|
|
|
604
905
|
const cached = cache.entries.get(abs);
|
|
605
906
|
if (cached && cached.hash === hash) return;
|
|
606
907
|
await cache.ensureInit();
|
|
607
|
-
const
|
|
608
|
-
const
|
|
908
|
+
const keepFull = config?.keepUnpruned?.length ? matchesAny(import_node_path6.default.relative(root, abs), config.keepUnpruned) : false;
|
|
909
|
+
const { code, language, symbols } = await analyze(abs, source, {
|
|
910
|
+
skipPrune: keepFull,
|
|
911
|
+
preserveAnnotations: config?.preserveAnnotations
|
|
912
|
+
});
|
|
913
|
+
const imports = extractImports(abs, source);
|
|
609
914
|
const entry = {
|
|
610
915
|
hash,
|
|
611
916
|
skeleton: code,
|
|
612
917
|
language,
|
|
613
|
-
imports: imports.map((spec) => resolveImport(abs, spec, root)).filter((p) => p !== null)
|
|
918
|
+
imports: imports.map((spec) => resolveImport(abs, spec, root)).filter((p) => p !== null),
|
|
919
|
+
...symbols.length > 0 ? { symbols } : {}
|
|
614
920
|
};
|
|
615
921
|
cache.entries.set(abs, entry);
|
|
616
922
|
onIndexed?.(abs, entry);
|
|
@@ -620,7 +926,7 @@ function createWatcher(opts) {
|
|
|
620
926
|
// sockets) are never opened with fs.watch (which raises UVException).
|
|
621
927
|
ignored(_p, stats) {
|
|
622
928
|
if (stats && !stats.isFile() && !stats.isDirectory()) return true;
|
|
623
|
-
return isIgnored(
|
|
929
|
+
return isIgnored(import_node_path6.default.resolve(String(_p)), effectiveIgnored);
|
|
624
930
|
},
|
|
625
931
|
alwaysStat: true,
|
|
626
932
|
ignoreInitial: true,
|
|
@@ -628,7 +934,7 @@ function createWatcher(opts) {
|
|
|
628
934
|
awaitWriteFinish: { stabilityThreshold: 50, pollInterval: 10 }
|
|
629
935
|
});
|
|
630
936
|
function debounce(filePath) {
|
|
631
|
-
const abs =
|
|
937
|
+
const abs = import_node_path6.default.resolve(filePath);
|
|
632
938
|
const existing = pending.get(abs);
|
|
633
939
|
if (existing) clearTimeout(existing);
|
|
634
940
|
if (!existing) void handle(abs);
|
|
@@ -640,8 +946,9 @@ function createWatcher(opts) {
|
|
|
640
946
|
watcher.on("add", debounce);
|
|
641
947
|
watcher.on("change", debounce);
|
|
642
948
|
watcher.on("unlink", (filePath) => {
|
|
643
|
-
const abs =
|
|
949
|
+
const abs = import_node_path6.default.resolve(filePath);
|
|
644
950
|
cache.entries.delete(abs);
|
|
951
|
+
onRemoved?.(abs);
|
|
645
952
|
});
|
|
646
953
|
return {
|
|
647
954
|
cache,
|
|
@@ -654,7 +961,7 @@ function createWatcher(opts) {
|
|
|
654
961
|
*/
|
|
655
962
|
async indexAll() {
|
|
656
963
|
const files = [];
|
|
657
|
-
await walk(root, (f) => files.push(f),
|
|
964
|
+
await walk(root, (f) => files.push(f), effectiveIgnored);
|
|
658
965
|
let ok = 0;
|
|
659
966
|
for (const f of files) {
|
|
660
967
|
try {
|
|
@@ -669,9 +976,9 @@ function createWatcher(opts) {
|
|
|
669
976
|
};
|
|
670
977
|
}
|
|
671
978
|
async function walk(root, push, ignored) {
|
|
672
|
-
const entries = await
|
|
979
|
+
const entries = await import_node_fs4.default.promises.readdir(root, { withFileTypes: true });
|
|
673
980
|
for (const e of entries) {
|
|
674
|
-
const abs =
|
|
981
|
+
const abs = import_node_path6.default.join(root, e.name);
|
|
675
982
|
if (isIgnored(abs, ignored)) continue;
|
|
676
983
|
if (e.isDirectory()) {
|
|
677
984
|
await walk(abs, push, ignored);
|
|
@@ -682,18 +989,298 @@ async function walk(root, push, ignored) {
|
|
|
682
989
|
}
|
|
683
990
|
function isIgnored(abs, ignored) {
|
|
684
991
|
for (const m of ignored) {
|
|
992
|
+
if (typeof m === "function") {
|
|
993
|
+
if (m(abs)) return true;
|
|
994
|
+
continue;
|
|
995
|
+
}
|
|
685
996
|
if (typeof m === "string" && abs.includes(m)) return true;
|
|
686
997
|
if (m instanceof RegExp && m.test(abs)) return true;
|
|
687
998
|
}
|
|
688
999
|
return false;
|
|
689
1000
|
}
|
|
690
1001
|
|
|
1002
|
+
// src/detect.ts
|
|
1003
|
+
var import_node_fs5 = __toESM(require("fs"), 1);
|
|
1004
|
+
var import_node_path7 = __toESM(require("path"), 1);
|
|
1005
|
+
var VCS_MARKER_DIRS = [".git", ".hg", ".svn"];
|
|
1006
|
+
var PROJECT_MANIFEST_FILES = [
|
|
1007
|
+
// JavaScript / TypeScript
|
|
1008
|
+
"package.json",
|
|
1009
|
+
"tsconfig.json",
|
|
1010
|
+
// Python
|
|
1011
|
+
"pyproject.toml",
|
|
1012
|
+
"setup.py",
|
|
1013
|
+
"setup.cfg",
|
|
1014
|
+
"requirements.txt",
|
|
1015
|
+
// Go / Rust / Dart / Swift
|
|
1016
|
+
"go.mod",
|
|
1017
|
+
"Cargo.toml",
|
|
1018
|
+
"pubspec.yaml",
|
|
1019
|
+
"Package.swift",
|
|
1020
|
+
// Java / Kotlin
|
|
1021
|
+
"pom.xml",
|
|
1022
|
+
"build.gradle",
|
|
1023
|
+
"build.gradle.kts",
|
|
1024
|
+
"settings.gradle",
|
|
1025
|
+
"settings.gradle.kts",
|
|
1026
|
+
// PHP / Ruby / Elixir
|
|
1027
|
+
"composer.json",
|
|
1028
|
+
"Gemfile",
|
|
1029
|
+
"mix.exs"
|
|
1030
|
+
];
|
|
1031
|
+
var MANIFEST_FILE_SET = new Set(PROJECT_MANIFEST_FILES);
|
|
1032
|
+
var MAX_WALK_DEPTH = 64;
|
|
1033
|
+
function detectProjectRoot(fromPath) {
|
|
1034
|
+
const resolved = import_node_path7.default.resolve(fromPath);
|
|
1035
|
+
let dir = isDirectory(resolved) ? resolved : import_node_path7.default.dirname(resolved);
|
|
1036
|
+
let nearestManifest = null;
|
|
1037
|
+
for (let depth = 0; depth < MAX_WALK_DEPTH; depth++) {
|
|
1038
|
+
const names = readDirNames(dir);
|
|
1039
|
+
if (names) {
|
|
1040
|
+
if (VCS_MARKER_DIRS.some((vcs) => names.has(vcs))) return dir;
|
|
1041
|
+
if (nearestManifest === null && hasManifest(names)) nearestManifest = dir;
|
|
1042
|
+
}
|
|
1043
|
+
const parent = import_node_path7.default.dirname(dir);
|
|
1044
|
+
if (parent === dir) break;
|
|
1045
|
+
dir = parent;
|
|
1046
|
+
}
|
|
1047
|
+
return nearestManifest;
|
|
1048
|
+
}
|
|
1049
|
+
function isDirectory(p) {
|
|
1050
|
+
try {
|
|
1051
|
+
return import_node_fs5.default.statSync(p).isDirectory();
|
|
1052
|
+
} catch {
|
|
1053
|
+
return false;
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
function readDirNames(dir) {
|
|
1057
|
+
try {
|
|
1058
|
+
return new Set(import_node_fs5.default.readdirSync(dir));
|
|
1059
|
+
} catch {
|
|
1060
|
+
return null;
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
function hasManifest(names) {
|
|
1064
|
+
for (const name of names) {
|
|
1065
|
+
if (MANIFEST_FILE_SET.has(name)) return true;
|
|
1066
|
+
}
|
|
1067
|
+
return false;
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
// src/mcp.ts
|
|
1071
|
+
init_registry();
|
|
1072
|
+
|
|
1073
|
+
// src/search/index.ts
|
|
1074
|
+
function words(text) {
|
|
1075
|
+
const out = /* @__PURE__ */ new Set();
|
|
1076
|
+
for (const part of text.split(/[^A-Za-z0-9_$]+/)) {
|
|
1077
|
+
if (!part) continue;
|
|
1078
|
+
for (const seg of part.split(/(?<=[a-z0-9])(?=[A-Z])/)) {
|
|
1079
|
+
const w = seg.toLowerCase();
|
|
1080
|
+
if (w) out.add(w);
|
|
1081
|
+
}
|
|
1082
|
+
for (const seg of part.split("_")) {
|
|
1083
|
+
const w = seg.toLowerCase();
|
|
1084
|
+
if (w) out.add(w);
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
return [...out];
|
|
1088
|
+
}
|
|
1089
|
+
var SymbolSearch = class {
|
|
1090
|
+
docs = [];
|
|
1091
|
+
byFile = /* @__PURE__ */ new Map();
|
|
1092
|
+
inverted = /* @__PURE__ */ new Map();
|
|
1093
|
+
get size() {
|
|
1094
|
+
return this.docs.length;
|
|
1095
|
+
}
|
|
1096
|
+
/** Replace the docs for one file (or remove when `symbols` is empty). */
|
|
1097
|
+
setFile(filePath, symbols) {
|
|
1098
|
+
this.removeFile(filePath);
|
|
1099
|
+
if (symbols.length === 0) return;
|
|
1100
|
+
const docs = symbols.map((s) => ({ ...s, filePath }));
|
|
1101
|
+
const ids = docs.map((d) => {
|
|
1102
|
+
const id = this.docs.length;
|
|
1103
|
+
this.docs.push(d);
|
|
1104
|
+
for (const w of words(`${d.name}`)) {
|
|
1105
|
+
const list = this.inverted.get(w) ?? [];
|
|
1106
|
+
list.push(id);
|
|
1107
|
+
this.inverted.set(w, list);
|
|
1108
|
+
}
|
|
1109
|
+
return id;
|
|
1110
|
+
});
|
|
1111
|
+
this.byFile.set(filePath, docs);
|
|
1112
|
+
void ids;
|
|
1113
|
+
}
|
|
1114
|
+
removeFile(filePath) {
|
|
1115
|
+
const docs = this.byFile.get(filePath);
|
|
1116
|
+
if (!docs) return;
|
|
1117
|
+
const removed = new Set(docs);
|
|
1118
|
+
this.docs = this.docs.filter((d) => !removed.has(d));
|
|
1119
|
+
this.byFile.delete(filePath);
|
|
1120
|
+
this.rebuildInverted();
|
|
1121
|
+
}
|
|
1122
|
+
/** Bulk-load a whole cache (used at attach time and cold lazy builds). */
|
|
1123
|
+
loadCache(entries) {
|
|
1124
|
+
this.docs = [];
|
|
1125
|
+
this.byFile.clear();
|
|
1126
|
+
for (const [abs, entry] of entries) {
|
|
1127
|
+
if (!entry.symbols || entry.symbols.length === 0) continue;
|
|
1128
|
+
const docs = entry.symbols.map((s) => ({ ...s, filePath: abs }));
|
|
1129
|
+
this.byFile.set(abs, docs);
|
|
1130
|
+
this.docs.push(...docs);
|
|
1131
|
+
}
|
|
1132
|
+
this.rebuildInverted();
|
|
1133
|
+
}
|
|
1134
|
+
/** Ranked symbol hits for `query`. */
|
|
1135
|
+
search(query, opts = {}) {
|
|
1136
|
+
const maxResults = opts.maxResults ?? 10;
|
|
1137
|
+
const q = query.trim().toLowerCase();
|
|
1138
|
+
if (!q) return [];
|
|
1139
|
+
const queryWords = words(q);
|
|
1140
|
+
let candidates;
|
|
1141
|
+
if (queryWords.length > 0) {
|
|
1142
|
+
const ids = /* @__PURE__ */ new Set();
|
|
1143
|
+
for (const w of queryWords) {
|
|
1144
|
+
const post = this.inverted.get(w);
|
|
1145
|
+
if (post) for (const id of post) ids.add(id);
|
|
1146
|
+
}
|
|
1147
|
+
candidates = [...ids].map((id) => this.docs[id]);
|
|
1148
|
+
} else {
|
|
1149
|
+
candidates = this.docs;
|
|
1150
|
+
}
|
|
1151
|
+
if (opts.kind) candidates = candidates.filter((d) => d.kind === opts.kind);
|
|
1152
|
+
const scored = candidates.map((d) => this.score(d, q, queryWords)).filter((s) => s !== null).sort((a, b) => b.score - a.score || a.line - b.line).slice(0, maxResults);
|
|
1153
|
+
return scored.map((s) => ({ ...s, label: this.label(s) }));
|
|
1154
|
+
}
|
|
1155
|
+
score(d, q, queryWords) {
|
|
1156
|
+
const name = d.name.toLowerCase();
|
|
1157
|
+
let score = 0;
|
|
1158
|
+
if (name === q) score += 100;
|
|
1159
|
+
if (name.startsWith(q)) score += 60;
|
|
1160
|
+
if (name.includes(q)) score += 30;
|
|
1161
|
+
if ((d.signature ?? "").toLowerCase().includes(q)) score += 8;
|
|
1162
|
+
for (const w of queryWords) {
|
|
1163
|
+
if (name.includes(w)) score += 4;
|
|
1164
|
+
if ((d.signature ?? "").toLowerCase().includes(w)) score += 1;
|
|
1165
|
+
}
|
|
1166
|
+
if (score <= 0) return null;
|
|
1167
|
+
return { ...d, score };
|
|
1168
|
+
}
|
|
1169
|
+
label(d) {
|
|
1170
|
+
const sig = d.signature && d.signature.length > 0 ? ` \u2014 ${d.signature}` : "";
|
|
1171
|
+
return `${d.filePath}:${d.line}${sig}`;
|
|
1172
|
+
}
|
|
1173
|
+
rebuildInverted() {
|
|
1174
|
+
this.inverted.clear();
|
|
1175
|
+
this.docs.forEach((d, id) => {
|
|
1176
|
+
for (const w of words(d.name)) {
|
|
1177
|
+
const list = this.inverted.get(w) ?? [];
|
|
1178
|
+
list.push(id);
|
|
1179
|
+
this.inverted.set(w, list);
|
|
1180
|
+
}
|
|
1181
|
+
});
|
|
1182
|
+
}
|
|
1183
|
+
};
|
|
1184
|
+
|
|
1185
|
+
// src/git.ts
|
|
1186
|
+
var import_node_child_process = require("child_process");
|
|
1187
|
+
var import_node_path8 = __toESM(require("path"), 1);
|
|
1188
|
+
var import_node_fs6 = __toESM(require("fs"), 1);
|
|
1189
|
+
function runGit(root, args) {
|
|
1190
|
+
return new Promise((resolve) => {
|
|
1191
|
+
const child = (0, import_node_child_process.spawn)("git", ["--no-pager", ...args], {
|
|
1192
|
+
cwd: root,
|
|
1193
|
+
env: { ...process.env, LC_ALL: "C" },
|
|
1194
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
1195
|
+
});
|
|
1196
|
+
let out = "";
|
|
1197
|
+
let err = "";
|
|
1198
|
+
child.stdout.on("data", (d) => {
|
|
1199
|
+
out += d;
|
|
1200
|
+
});
|
|
1201
|
+
child.stderr.on("data", (d) => {
|
|
1202
|
+
err += d;
|
|
1203
|
+
});
|
|
1204
|
+
child.on("error", (e) => resolve({ ok: false, out: "", err: e.message }));
|
|
1205
|
+
child.on("close", (code) => resolve({ ok: code === 0, out, err }));
|
|
1206
|
+
});
|
|
1207
|
+
}
|
|
1208
|
+
function parseNames(out) {
|
|
1209
|
+
return out.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
1210
|
+
}
|
|
1211
|
+
function isFatal(err) {
|
|
1212
|
+
if (!err) return void 0;
|
|
1213
|
+
const e = err.trim();
|
|
1214
|
+
if (/not a git repository|fatal:/i.test(e)) return e;
|
|
1215
|
+
return void 0;
|
|
1216
|
+
}
|
|
1217
|
+
async function gitChangedFiles(root, opts) {
|
|
1218
|
+
const scope = opts.scope ?? "worktree";
|
|
1219
|
+
const filter = "--diff-filter=ACMRT";
|
|
1220
|
+
let relative = [];
|
|
1221
|
+
if (scope === "staged") {
|
|
1222
|
+
const r = await runGit(root, ["diff", "--cached", "--name-only", filter]);
|
|
1223
|
+
const fatal = isFatal(r.err);
|
|
1224
|
+
if (!r.ok) return { files: [], ...fatal ? { error: fatal } : {} };
|
|
1225
|
+
relative = parseNames(r.out);
|
|
1226
|
+
} else if (scope === "branch") {
|
|
1227
|
+
const base = opts.base ?? "HEAD~1";
|
|
1228
|
+
const head = opts.head ?? "HEAD";
|
|
1229
|
+
const r = await runGit(root, ["diff", "--name-only", `${base}...${head}`, filter]);
|
|
1230
|
+
const fatal = isFatal(r.err);
|
|
1231
|
+
if (!r.ok) return { files: [], ...fatal ? { error: fatal } : {} };
|
|
1232
|
+
relative = parseNames(r.out);
|
|
1233
|
+
} else {
|
|
1234
|
+
const [unstaged, staged] = await Promise.all([
|
|
1235
|
+
runGit(root, ["diff", "--name-only", filter]),
|
|
1236
|
+
runGit(root, ["diff", "--cached", "--name-only", filter])
|
|
1237
|
+
]);
|
|
1238
|
+
const fatal = isFatal(unstaged.err) ?? isFatal(staged.err);
|
|
1239
|
+
if (!unstaged.ok && !staged.ok) {
|
|
1240
|
+
return { files: [], ...fatal ? { error: fatal } : {} };
|
|
1241
|
+
}
|
|
1242
|
+
const merged = /* @__PURE__ */ new Set([...parseNames(unstaged.out), ...parseNames(staged.out)]);
|
|
1243
|
+
if (opts.includeUntracked) {
|
|
1244
|
+
const ut = await runGit(root, ["ls-files", "--others", "--exclude-standard"]);
|
|
1245
|
+
for (const f of parseNames(ut.out)) merged.add(f);
|
|
1246
|
+
}
|
|
1247
|
+
relative = [...merged];
|
|
1248
|
+
}
|
|
1249
|
+
const files = relative.map((rel2) => import_node_path8.default.resolve(root, rel2)).filter((abs) => {
|
|
1250
|
+
try {
|
|
1251
|
+
return import_node_fs6.default.statSync(abs).isFile();
|
|
1252
|
+
} catch {
|
|
1253
|
+
return false;
|
|
1254
|
+
}
|
|
1255
|
+
});
|
|
1256
|
+
return { files };
|
|
1257
|
+
}
|
|
1258
|
+
|
|
691
1259
|
// src/mcp.ts
|
|
692
1260
|
var RULE_TARGET_PATH = {
|
|
693
1261
|
cursor: ".cursor/rules/token-shrink.mdc",
|
|
694
1262
|
claude: ".claude/rules/token-shrink.md",
|
|
695
1263
|
cline: ".clinerules/token-shrink.md"
|
|
696
1264
|
};
|
|
1265
|
+
var RULE_VERSION = 3;
|
|
1266
|
+
var RULE_VERSION_MARKER = `# token-shrink rule v${RULE_VERSION}`;
|
|
1267
|
+
var WORKFLOW_GUIDANCE = [
|
|
1268
|
+
"When a pruned skeleton is not enough to write or change code correctly, expand the exact",
|
|
1269
|
+
"definition with expand_symbol.",
|
|
1270
|
+
"",
|
|
1271
|
+
"Call search_symbol_signatures automatically for any identifier you reference that the",
|
|
1272
|
+
"context does not already define, so you always work from exact signatures.",
|
|
1273
|
+
"",
|
|
1274
|
+
"Before multi-file edits, reviews, or work on code with uncommitted or staged changes, call",
|
|
1275
|
+
"git_diff_context and use its impact payload (changed files and their callers) as context.",
|
|
1276
|
+
"",
|
|
1277
|
+
"Pass activeFiles to get_compressed_code_context when a task spans several files, and",
|
|
1278
|
+
"maxTokens whenever the payload must fit a token budget."
|
|
1279
|
+
].join("\n");
|
|
1280
|
+
var LIGHT_GUIDANCE = [
|
|
1281
|
+
"Use expand_symbol when a pruned body is not enough, git_diff_context for changed or",
|
|
1282
|
+
"multi-file work, and search_symbol_signatures to locate definitions repo-wide."
|
|
1283
|
+
].join("\n");
|
|
697
1284
|
var AUTO_RULE_SENTINEL = "# auto-generated by token-shrink";
|
|
698
1285
|
var CURSOR_RULE_PATH = RULE_TARGET_PATH.cursor;
|
|
699
1286
|
var AUTO_RULE_PATH = CURSOR_RULE_PATH;
|
|
@@ -714,6 +1301,9 @@ Before working on a file in this repo, call the \`get_compressed_code_context\`
|
|
|
714
1301
|
that file's path, and use the returned Ring 0 + Ring 1 payload as the context for that file
|
|
715
1302
|
and its direct imports.
|
|
716
1303
|
|
|
1304
|
+
${WORKFLOW_GUIDANCE}
|
|
1305
|
+
|
|
1306
|
+
${RULE_VERSION_MARKER}
|
|
717
1307
|
${AUTO_RULE_SENTINEL}
|
|
718
1308
|
`
|
|
719
1309
|
},
|
|
@@ -728,6 +1318,9 @@ Before working on a file in this repo, call the \`get_compressed_code_context\`
|
|
|
728
1318
|
that file's path, and use the returned Ring 0 + Ring 1 payload as the context for that file
|
|
729
1319
|
and its direct imports.
|
|
730
1320
|
|
|
1321
|
+
${WORKFLOW_GUIDANCE}
|
|
1322
|
+
|
|
1323
|
+
${RULE_VERSION_MARKER}
|
|
731
1324
|
${CLAUDE_RULE_SENTINEL}
|
|
732
1325
|
`
|
|
733
1326
|
},
|
|
@@ -742,23 +1335,32 @@ Before working on a file in this repo, call the \`get_compressed_code_context\`
|
|
|
742
1335
|
that file's path, and use the returned Ring 0 + Ring 1 payload as the context for that file
|
|
743
1336
|
and its direct imports.
|
|
744
1337
|
|
|
1338
|
+
${WORKFLOW_GUIDANCE}
|
|
1339
|
+
|
|
1340
|
+
${RULE_VERSION_MARKER}
|
|
745
1341
|
${CLINE_RULE_SENTINEL}
|
|
746
1342
|
`
|
|
747
1343
|
}
|
|
748
1344
|
};
|
|
749
1345
|
function createAutoRule(root, target) {
|
|
750
1346
|
const spec = ruleTargets[target];
|
|
751
|
-
const rulePath =
|
|
1347
|
+
const rulePath = import_node_path9.default.join(root, spec.relPath);
|
|
1348
|
+
const autoWorkflow = loadConfig(root).autoWorkflow !== false;
|
|
1349
|
+
const body = autoWorkflow ? spec.body : spec.body.replace(WORKFLOW_GUIDANCE, LIGHT_GUIDANCE);
|
|
752
1350
|
try {
|
|
753
|
-
if (
|
|
754
|
-
const existing =
|
|
1351
|
+
if (import_node_fs7.default.existsSync(rulePath)) {
|
|
1352
|
+
const existing = import_node_fs7.default.readFileSync(rulePath, "utf8");
|
|
755
1353
|
if (existing.includes(spec.sentinel)) {
|
|
756
|
-
|
|
1354
|
+
if (existing.includes(RULE_VERSION_MARKER)) {
|
|
1355
|
+
return { created: false, skipped: "exists", filePath: rulePath };
|
|
1356
|
+
}
|
|
1357
|
+
import_node_fs7.default.writeFileSync(rulePath, body, "utf8");
|
|
1358
|
+
return { created: true, skipped: "none", filePath: rulePath };
|
|
757
1359
|
}
|
|
758
1360
|
return { created: false, skipped: "user", filePath: rulePath };
|
|
759
1361
|
}
|
|
760
|
-
|
|
761
|
-
|
|
1362
|
+
import_node_fs7.default.mkdirSync(import_node_path9.default.dirname(rulePath), { recursive: true });
|
|
1363
|
+
import_node_fs7.default.writeFileSync(rulePath, body, "utf8");
|
|
762
1364
|
return { created: true, skipped: "none", filePath: rulePath };
|
|
763
1365
|
} catch (err) {
|
|
764
1366
|
process.stderr.write(
|
|
@@ -777,27 +1379,154 @@ function resolveTargets(ruleTarget) {
|
|
|
777
1379
|
if (list.includes("all")) return ["cursor", "claude", "cline"];
|
|
778
1380
|
return list;
|
|
779
1381
|
}
|
|
1382
|
+
function isWithin(parent, child) {
|
|
1383
|
+
const rel2 = import_node_path9.default.relative(import_node_path9.default.resolve(parent), import_node_path9.default.resolve(child));
|
|
1384
|
+
return rel2 === "" || !rel2.startsWith("..") && !import_node_path9.default.isAbsolute(rel2);
|
|
1385
|
+
}
|
|
1386
|
+
function resolveActiveFiles(single, many) {
|
|
1387
|
+
if (many && many.length > 0) {
|
|
1388
|
+
if (single) {
|
|
1389
|
+
throw new Error("Pass either `activeFilePath` or `activeFiles`, not both.");
|
|
1390
|
+
}
|
|
1391
|
+
return many;
|
|
1392
|
+
}
|
|
1393
|
+
if (single) return [single];
|
|
1394
|
+
throw new Error("`activeFilePath` or `activeFiles` is required");
|
|
1395
|
+
}
|
|
780
1396
|
async function startMcpServer(opts = {}) {
|
|
781
|
-
const root = import_node_path6.default.resolve(opts.root ?? process.env.ROOT ?? process.cwd());
|
|
782
1397
|
const log = (msg) => {
|
|
783
1398
|
if (!opts.silent) process.stderr.write(`[token-shrink] ${msg}
|
|
784
1399
|
`);
|
|
785
1400
|
};
|
|
786
|
-
const
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
1401
|
+
const textReply = (text) => ({
|
|
1402
|
+
content: [{ type: "text", text }]
|
|
1403
|
+
});
|
|
1404
|
+
const explicitRoot = opts.root?.trim() || process.env.ROOT?.trim() || "";
|
|
1405
|
+
const autoMode = explicitRoot === "";
|
|
1406
|
+
let root = "";
|
|
1407
|
+
let watcher = null;
|
|
1408
|
+
let searchIndex = null;
|
|
1409
|
+
let lifecycle = Promise.resolve();
|
|
1410
|
+
let config = { ...EMPTY_CONFIG };
|
|
1411
|
+
let configFsw = null;
|
|
1412
|
+
let configReloadTimer = null;
|
|
1413
|
+
const stopConfigWatch = () => {
|
|
1414
|
+
if (configReloadTimer) {
|
|
1415
|
+
clearTimeout(configReloadTimer);
|
|
1416
|
+
configReloadTimer = null;
|
|
1417
|
+
}
|
|
1418
|
+
if (configFsw) {
|
|
1419
|
+
configFsw.close();
|
|
1420
|
+
configFsw = null;
|
|
1421
|
+
}
|
|
1422
|
+
};
|
|
1423
|
+
const reloadForRoot = async (r) => {
|
|
1424
|
+
const next = loadConfig(r, (m) => log(m));
|
|
1425
|
+
const changed = JSON.stringify(next) !== JSON.stringify(config);
|
|
1426
|
+
config = next;
|
|
1427
|
+
if (!changed || !watcher || root !== r) return;
|
|
1428
|
+
log(".tokenshrinkrc.json changed \u2014 re-indexing with the new rules.");
|
|
1429
|
+
stopConfigWatch();
|
|
1430
|
+
const old = watcher;
|
|
1431
|
+
watcher = null;
|
|
1432
|
+
root = "";
|
|
1433
|
+
await old.close().catch(() => {
|
|
1434
|
+
});
|
|
1435
|
+
await attachWatcher(r, false);
|
|
1436
|
+
};
|
|
1437
|
+
const startConfigWatch = (r) => {
|
|
1438
|
+
stopConfigWatch();
|
|
1439
|
+
const cfgPath = configPathFor(r);
|
|
1440
|
+
if (!import_node_fs7.default.existsSync(cfgPath)) return;
|
|
1441
|
+
try {
|
|
1442
|
+
configFsw = import_node_fs7.default.watch(cfgPath, () => {
|
|
1443
|
+
if (configReloadTimer) clearTimeout(configReloadTimer);
|
|
1444
|
+
configReloadTimer = setTimeout(() => {
|
|
1445
|
+
void enqueue(() => reloadForRoot(r));
|
|
1446
|
+
}, 200);
|
|
1447
|
+
});
|
|
1448
|
+
} catch {
|
|
1449
|
+
}
|
|
1450
|
+
};
|
|
1451
|
+
const writeRules = (r) => {
|
|
1452
|
+
if (opts.createRule === false) return;
|
|
791
1453
|
for (const target of resolveTargets(opts.ruleTarget)) {
|
|
792
|
-
const res = createAutoRule(
|
|
1454
|
+
const res = createAutoRule(r, target);
|
|
793
1455
|
if (res.created) {
|
|
794
1456
|
log(`Wrote ${target} rule to ${res.filePath}`);
|
|
795
1457
|
} else if (res.skipped === "user") {
|
|
796
1458
|
log(`${target} rule exists (user-authored); leaving it untouched.`);
|
|
797
1459
|
}
|
|
798
1460
|
}
|
|
1461
|
+
};
|
|
1462
|
+
const attachWatcher = (r, waitForIndex) => {
|
|
1463
|
+
stopConfigWatch();
|
|
1464
|
+
config = loadConfig(r, (m) => log(m));
|
|
1465
|
+
const search = new SymbolSearch();
|
|
1466
|
+
const w = createWatcher({
|
|
1467
|
+
root: r,
|
|
1468
|
+
ignored: opts.ignored,
|
|
1469
|
+
config,
|
|
1470
|
+
onIndexed: (abs, entry) => {
|
|
1471
|
+
if (entry.symbols && entry.symbols.length > 0) search.setFile(abs, entry.symbols);
|
|
1472
|
+
else search.removeFile(abs);
|
|
1473
|
+
},
|
|
1474
|
+
onRemoved: (abs) => search.removeFile(abs)
|
|
1475
|
+
});
|
|
1476
|
+
searchIndex = search;
|
|
1477
|
+
root = r;
|
|
1478
|
+
watcher = w;
|
|
1479
|
+
writeRules(r);
|
|
1480
|
+
log(`Indexing ${r} in the background\u2026`);
|
|
1481
|
+
const indexed = w.indexAll().then((n) => log(`Indexed ${n} files.`)).catch((err) => log(`Indexing ${r} failed: ${err?.message ?? err}`));
|
|
1482
|
+
startConfigWatch(r);
|
|
1483
|
+
return waitForIndex ? indexed : Promise.resolve();
|
|
1484
|
+
};
|
|
1485
|
+
const enqueue = (fn) => {
|
|
1486
|
+
const run = lifecycle.then(fn);
|
|
1487
|
+
lifecycle = run.then(
|
|
1488
|
+
() => {
|
|
1489
|
+
},
|
|
1490
|
+
() => {
|
|
1491
|
+
}
|
|
1492
|
+
);
|
|
1493
|
+
return run;
|
|
1494
|
+
};
|
|
1495
|
+
if (explicitRoot) {
|
|
1496
|
+
void enqueue(() => attachWatcher(import_node_path9.default.resolve(explicitRoot), false));
|
|
1497
|
+
} else {
|
|
1498
|
+
const fromCwd = detectProjectRoot(process.cwd());
|
|
1499
|
+
if (fromCwd) {
|
|
1500
|
+
log(`Auto-detected project root ${fromCwd} (from cwd). Pass --root to pin it.`);
|
|
1501
|
+
void enqueue(() => attachWatcher(fromCwd, false));
|
|
1502
|
+
} else {
|
|
1503
|
+
log(
|
|
1504
|
+
"No --root and no project markers around the current directory \u2014 will auto-detect the project from the first tool call."
|
|
1505
|
+
);
|
|
1506
|
+
}
|
|
799
1507
|
}
|
|
800
|
-
|
|
1508
|
+
const ensureReadyFor = (activeFilePath) => enqueue(async () => {
|
|
1509
|
+
const abs = import_node_path9.default.resolve(activeFilePath);
|
|
1510
|
+
if (watcher && root) {
|
|
1511
|
+
if (!autoMode || isWithin(root, abs)) return;
|
|
1512
|
+
const next = detectProjectRoot(abs);
|
|
1513
|
+
if (!next || next === root) return;
|
|
1514
|
+
log(`Active file is in a different project (${next}); re-indexing (was ${root}).`);
|
|
1515
|
+
await watcher.close().catch(() => {
|
|
1516
|
+
});
|
|
1517
|
+
watcher = null;
|
|
1518
|
+
root = "";
|
|
1519
|
+
}
|
|
1520
|
+
if (root) return;
|
|
1521
|
+
const detected = detectProjectRoot(abs);
|
|
1522
|
+
const target = detected ?? process.cwd();
|
|
1523
|
+
if (detected) {
|
|
1524
|
+
log(`Auto-detected project root ${target} from ${abs}.`);
|
|
1525
|
+
} else {
|
|
1526
|
+
log(`No project markers around ${abs}; falling back to ${target}.`);
|
|
1527
|
+
}
|
|
1528
|
+
await attachWatcher(target, true);
|
|
1529
|
+
});
|
|
801
1530
|
const server = new import_mcp.McpServer(
|
|
802
1531
|
{ name: "token-shrink", version: "2.0.0" },
|
|
803
1532
|
{ capabilities: { tools: {} } }
|
|
@@ -806,18 +1535,28 @@ async function startMcpServer(opts = {}) {
|
|
|
806
1535
|
"get_compressed_code_context",
|
|
807
1536
|
{
|
|
808
1537
|
title: "Get Compressed Code Context",
|
|
809
|
-
description: "Returns a compressed, framework-aware AST context payload for
|
|
1538
|
+
description: "Returns a compressed, framework-aware AST context payload for one or more files: each active file\u2019s full source (Ring 0) plus pruned skeletons of their direct imports (Ring 1). Implementation bodies are removed but type signatures, interfaces, and module exports are preserved for ~80-90% token reduction. Use `activeFiles` to pin multiple files as Ring 0 and `maxTokens` to cap the payload with a relevance-ranked Ring 1.",
|
|
810
1539
|
inputSchema: {
|
|
811
|
-
activeFilePath: import_zod.z.string().describe("Path to the file the agent is working on"),
|
|
1540
|
+
activeFilePath: import_zod.z.string().optional().describe("Path to the file the agent is working on (or use `activeFiles`)"),
|
|
1541
|
+
activeFiles: import_zod.z.array(import_zod.z.string()).optional().describe("Multiple files to keep as Ring 0 (full text); mutually exclusive with `activeFilePath`"),
|
|
812
1542
|
maxSkeletons: import_zod.z.number().int().min(1).max(200).optional().describe("Cap on number of dependency skeletons to include"),
|
|
1543
|
+
maxTokens: import_zod.z.number().int().positive().optional().describe("Hard token budget for the whole payload; Ring 1 is relevance-ranked and packed to fit"),
|
|
813
1544
|
includeStats: import_zod.z.boolean().optional().describe("Append approximate token-count stats")
|
|
814
1545
|
}
|
|
815
1546
|
},
|
|
816
|
-
async ({ activeFilePath, maxSkeletons, includeStats }) => {
|
|
817
|
-
const
|
|
1547
|
+
async ({ activeFilePath, activeFiles, maxSkeletons, maxTokens, includeStats }) => {
|
|
1548
|
+
const paths = resolveActiveFiles(activeFilePath, activeFiles);
|
|
1549
|
+
for (const p of paths) await ensureReadyFor(p);
|
|
1550
|
+
if (!watcher) {
|
|
1551
|
+
throw new Error("token-shrink watcher failed to start");
|
|
1552
|
+
}
|
|
1553
|
+
const result = assembleMany(paths, watcher.cache.entries, {
|
|
818
1554
|
maxSkeletons,
|
|
1555
|
+
maxTokens,
|
|
819
1556
|
includeStats
|
|
820
1557
|
});
|
|
1558
|
+
const ts = result.tokenStats;
|
|
1559
|
+
const budgetNote = ts.budget !== void 0 ? ` budget=${ts.budget} trimmed=${ts.trimmed}` : "";
|
|
821
1560
|
return {
|
|
822
1561
|
content: [
|
|
823
1562
|
{
|
|
@@ -826,19 +1565,274 @@ async function startMcpServer(opts = {}) {
|
|
|
826
1565
|
},
|
|
827
1566
|
{
|
|
828
1567
|
type: "text",
|
|
829
|
-
text: `[stats]
|
|
1568
|
+
text: `[stats] files=${result.activeFilePaths.length} dependencies=${result.included.length} unresolved=${result.unresolved.length} tokens=${ts.totalTokens}${budgetNote}`
|
|
830
1569
|
}
|
|
831
1570
|
]
|
|
832
1571
|
};
|
|
833
1572
|
}
|
|
834
1573
|
);
|
|
1574
|
+
server.registerTool(
|
|
1575
|
+
"expand_symbol",
|
|
1576
|
+
{
|
|
1577
|
+
title: "Expand Symbol",
|
|
1578
|
+
description: "Returns the full, un-pruned source of a named definition (function, method, class, interface, enum, arrow/const function, \u2026) from one file. Use it when a Ring-1 skeleton is not enough \u2014 e.g. you need the exact algorithm inside a pruned body before writing or changing code.",
|
|
1579
|
+
inputSchema: {
|
|
1580
|
+
filePath: import_zod.z.string().describe("Path to the file containing the symbol"),
|
|
1581
|
+
symbolName: import_zod.z.string().describe("Name of the definition to expand (exact name preferred; falls back to case-insensitive/substring)"),
|
|
1582
|
+
maxMatches: import_zod.z.number().int().min(1).max(20).optional().describe("Maximum matching definitions to return (default 5)")
|
|
1583
|
+
}
|
|
1584
|
+
},
|
|
1585
|
+
async ({ filePath, symbolName, maxMatches }) => {
|
|
1586
|
+
await ensureReadyFor(filePath);
|
|
1587
|
+
const abs = import_node_path9.default.resolve(filePath);
|
|
1588
|
+
let source;
|
|
1589
|
+
try {
|
|
1590
|
+
source = import_node_fs7.default.readFileSync(abs, "utf8");
|
|
1591
|
+
} catch {
|
|
1592
|
+
return textReply(`File not found or unreadable: ${filePath}`);
|
|
1593
|
+
}
|
|
1594
|
+
const spec = languageForFile(abs);
|
|
1595
|
+
if (!spec) {
|
|
1596
|
+
return textReply(`No token-shrink grammar for file type: ${filePath}`);
|
|
1597
|
+
}
|
|
1598
|
+
const { symbols } = await analyze(abs, source, { skipPrune: true });
|
|
1599
|
+
if (symbols.length === 0) {
|
|
1600
|
+
return textReply(
|
|
1601
|
+
`No parseable definitions found in ${filePath} \u2014 the grammar may be unavailable (first run offline) or the language exposes no name-carrying definitions yet.`
|
|
1602
|
+
);
|
|
1603
|
+
}
|
|
1604
|
+
const matches = matchSymbols(symbols, symbolName).slice(0, maxMatches ?? 5);
|
|
1605
|
+
if (matches.length === 0) {
|
|
1606
|
+
const names = [...new Set(symbols.map((s) => s.name))].slice(0, 12).join(", ");
|
|
1607
|
+
return textReply(
|
|
1608
|
+
`No symbol named "${symbolName}" in ${filePath}.` + (names ? ` Other definitions there: ${names}.` : "")
|
|
1609
|
+
);
|
|
1610
|
+
}
|
|
1611
|
+
const fence = import_node_path9.default.extname(abs).replace(/^\./, "") || "text";
|
|
1612
|
+
const parts = matches.map((m) => {
|
|
1613
|
+
const body = source.slice(m.start, m.end).trim();
|
|
1614
|
+
return `### ${m.name} (${m.kind}) \u2014 ${abs}:${m.line}
|
|
1615
|
+
|
|
1616
|
+
\`\`\`` + fence + "\n" + body + "\n```\n";
|
|
1617
|
+
});
|
|
1618
|
+
const disambig = matches.length > 1 ? `
|
|
1619
|
+
_Multiple definitions matched (${matches.length}); each is shown above._` : "";
|
|
1620
|
+
return textReply(parts.join("\n") + disambig);
|
|
1621
|
+
}
|
|
1622
|
+
);
|
|
1623
|
+
server.registerTool(
|
|
1624
|
+
"git_diff_context",
|
|
1625
|
+
{
|
|
1626
|
+
title: "Git Diff Context",
|
|
1627
|
+
description: 'Builds an impact-analysis payload from git changes: every changed file is kept as Ring 0 (full code), their imports AND the files that import them (file-level callers) are attached as pruned Ring-1 skeletons. Use for PR reviews, regression fixes and multi-file tasks where no single "active file" exists.',
|
|
1628
|
+
inputSchema: {
|
|
1629
|
+
scope: import_zod.z.enum(["worktree", "staged", "branch"]).optional().describe("Diff scope: 'worktree' (default, staged+unstaged), 'staged', or 'branch' (base...head)"),
|
|
1630
|
+
base: import_zod.z.string().optional().describe("Base ref for scope=branch (default HEAD~1)"),
|
|
1631
|
+
head: import_zod.z.string().optional().describe("Head ref for scope=branch (default HEAD)"),
|
|
1632
|
+
includeUntracked: import_zod.z.boolean().optional().describe("Include untracked files (worktree scope only)"),
|
|
1633
|
+
maxFiles: import_zod.z.number().int().min(1).max(200).optional().describe("Maximum changed files to include (default 30)"),
|
|
1634
|
+
maxImporters: import_zod.z.number().int().min(0).max(100).optional().describe("Maximum importer skeletons to append (default 20)"),
|
|
1635
|
+
maxSkeletons: import_zod.z.number().int().min(1).max(200).optional().describe("Cap on dependency skeletons per payload"),
|
|
1636
|
+
maxTokens: import_zod.z.number().int().positive().optional().describe("Hard token budget for the whole payload"),
|
|
1637
|
+
includeStats: import_zod.z.boolean().optional().describe("Append approximate token-count stats")
|
|
1638
|
+
}
|
|
1639
|
+
},
|
|
1640
|
+
async ({
|
|
1641
|
+
scope,
|
|
1642
|
+
base,
|
|
1643
|
+
head,
|
|
1644
|
+
includeUntracked,
|
|
1645
|
+
maxFiles,
|
|
1646
|
+
maxImporters,
|
|
1647
|
+
maxSkeletons,
|
|
1648
|
+
maxTokens,
|
|
1649
|
+
includeStats
|
|
1650
|
+
}) => {
|
|
1651
|
+
await ensureReadyFor(process.cwd());
|
|
1652
|
+
if (!watcher || !root) {
|
|
1653
|
+
throw new Error("token-shrink watcher failed to start");
|
|
1654
|
+
}
|
|
1655
|
+
const repoRoot = root;
|
|
1656
|
+
const changedResult = await gitChangedFiles(repoRoot, {
|
|
1657
|
+
scope: scope ?? "worktree",
|
|
1658
|
+
base,
|
|
1659
|
+
head,
|
|
1660
|
+
includeUntracked
|
|
1661
|
+
});
|
|
1662
|
+
if (changedResult.error) {
|
|
1663
|
+
return textReply(`git error: ${changedResult.error}`);
|
|
1664
|
+
}
|
|
1665
|
+
if (changedResult.files.length === 0) {
|
|
1666
|
+
return textReply("No changed files (clean worktree, or empty diff for the requested scope).");
|
|
1667
|
+
}
|
|
1668
|
+
const fileCap = maxFiles ?? 30;
|
|
1669
|
+
const changed = changedResult.files.slice(0, fileCap);
|
|
1670
|
+
const filesTruncated = changedResult.files.length > changed.length;
|
|
1671
|
+
for (const abs of changed) {
|
|
1672
|
+
if (!watcher.cache.entries.has(abs)) {
|
|
1673
|
+
try {
|
|
1674
|
+
await watcher.index(abs);
|
|
1675
|
+
} catch {
|
|
1676
|
+
}
|
|
1677
|
+
}
|
|
1678
|
+
}
|
|
1679
|
+
const assembled = assembleMany(changed, watcher.cache.entries, {
|
|
1680
|
+
maxSkeletons,
|
|
1681
|
+
maxTokens,
|
|
1682
|
+
includeStats
|
|
1683
|
+
});
|
|
1684
|
+
const changedSet = new Set(changed);
|
|
1685
|
+
const importerOf = /* @__PURE__ */ new Map();
|
|
1686
|
+
for (const [fileAbs, entry] of watcher.cache.entries) {
|
|
1687
|
+
for (const imp of entry.imports) {
|
|
1688
|
+
if (!changedSet.has(imp)) continue;
|
|
1689
|
+
const list = importerOf.get(imp) ?? [];
|
|
1690
|
+
list.push(fileAbs);
|
|
1691
|
+
importerOf.set(imp, list);
|
|
1692
|
+
}
|
|
1693
|
+
}
|
|
1694
|
+
const importerList = [...new Set([...importerOf.values()].flat())].filter(
|
|
1695
|
+
(f) => !changedSet.has(f)
|
|
1696
|
+
);
|
|
1697
|
+
const importerCap = maxImporters ?? 20;
|
|
1698
|
+
const importers = importerList.slice(0, importerCap);
|
|
1699
|
+
const importersTruncated = importerList.length > importers.length;
|
|
1700
|
+
const relLabel = (abs) => {
|
|
1701
|
+
const rel2 = import_node_path9.default.relative(repoRoot, abs);
|
|
1702
|
+
return rel2 && !rel2.startsWith("..") ? rel2 : abs;
|
|
1703
|
+
};
|
|
1704
|
+
const fence = (abs) => import_node_path9.default.extname(abs).replace(/^\./, "") || "text";
|
|
1705
|
+
const parts = [];
|
|
1706
|
+
parts.push(
|
|
1707
|
+
`# Git Impact Context (${changed.length} changed file${changed.length === 1 ? "" : "s"})`,
|
|
1708
|
+
""
|
|
1709
|
+
);
|
|
1710
|
+
parts.push(`- changed files: ${changed.map(relLabel).join(", ")}`);
|
|
1711
|
+
if (importers.length > 0) {
|
|
1712
|
+
parts.push(`- importing files (callers): ${importers.map(relLabel).join(", ")}`);
|
|
1713
|
+
}
|
|
1714
|
+
parts.push("", "---", "");
|
|
1715
|
+
parts.push("## Changed files \u2014 full code", "");
|
|
1716
|
+
for (const abs of changed) {
|
|
1717
|
+
parts.push(`### \`${relLabel(abs)}\``, "");
|
|
1718
|
+
let source = "";
|
|
1719
|
+
try {
|
|
1720
|
+
source = import_node_fs7.default.readFileSync(abs, "utf8");
|
|
1721
|
+
} catch {
|
|
1722
|
+
}
|
|
1723
|
+
parts.push(`\`\`\`${fence(abs)}`, source.trim() || "(unreadable file)", "```", "");
|
|
1724
|
+
}
|
|
1725
|
+
parts.push(`## Ring 1 \u2014 Pruned dependencies (${assembled.included.length})`, "");
|
|
1726
|
+
parts.push("", "Implementation bodies removed; type signatures, interfaces and exports retained.", "");
|
|
1727
|
+
if (assembled.included.length === 0) {
|
|
1728
|
+
parts.push("_No local dependency skeletons available._", "");
|
|
1729
|
+
}
|
|
1730
|
+
for (const inc of assembled.included) {
|
|
1731
|
+
const entry = watcher.cache.entries.get(inc.filePath);
|
|
1732
|
+
const label = relLabel(inc.filePath);
|
|
1733
|
+
parts.push(`### \`${label}\``, "");
|
|
1734
|
+
if (entry) {
|
|
1735
|
+
parts.push(`\`\`\`${fence(inc.filePath)}`, entry.skeleton.trim(), "```", "");
|
|
1736
|
+
} else {
|
|
1737
|
+
parts.push("_Unindexed file._", "");
|
|
1738
|
+
}
|
|
1739
|
+
parts.push("");
|
|
1740
|
+
}
|
|
1741
|
+
if (assembled.tokenStats.budget !== void 0 && assembled.tokenStats.trimmed > 0) {
|
|
1742
|
+
parts.push(
|
|
1743
|
+
`_Note: token budget of ${assembled.tokenStats.budget} excluded ${assembled.tokenStats.trimmed} lower-priority dependencies._`,
|
|
1744
|
+
""
|
|
1745
|
+
);
|
|
1746
|
+
}
|
|
1747
|
+
if (importers.length > 0) {
|
|
1748
|
+
parts.push(`## Ring 2 \u2014 Files importing the diff (${importers.length})`, "");
|
|
1749
|
+
parts.push("", "Pruned skeletons of modules that call into the changed files.", "");
|
|
1750
|
+
for (const abs of importers) {
|
|
1751
|
+
const entry = watcher.cache.entries.get(abs);
|
|
1752
|
+
parts.push(`### \`${relLabel(abs)}\``, "");
|
|
1753
|
+
if (entry) {
|
|
1754
|
+
parts.push(`\`\`\`${fence(abs)}`, entry.skeleton.trim(), "```", "");
|
|
1755
|
+
} else {
|
|
1756
|
+
parts.push("_Unindexed file._", "");
|
|
1757
|
+
}
|
|
1758
|
+
parts.push("");
|
|
1759
|
+
}
|
|
1760
|
+
if (importersTruncated) {
|
|
1761
|
+
parts.push(
|
|
1762
|
+
`_\u2026and ${importerList.length - importers.length} more importing files (raise maxImporters)._`,
|
|
1763
|
+
""
|
|
1764
|
+
);
|
|
1765
|
+
}
|
|
1766
|
+
}
|
|
1767
|
+
if (filesTruncated) {
|
|
1768
|
+
parts.push(
|
|
1769
|
+
`_Note: capped to ${fileCap} changed files (${changedResult.files.length} total); raise maxFiles to include more._`,
|
|
1770
|
+
""
|
|
1771
|
+
);
|
|
1772
|
+
}
|
|
1773
|
+
if (assembled.unresolved.length > 0) {
|
|
1774
|
+
parts.push("## Unresolved imports", "");
|
|
1775
|
+
for (const u of assembled.unresolved) parts.push(`- \`${u}\``);
|
|
1776
|
+
parts.push("");
|
|
1777
|
+
}
|
|
1778
|
+
const ts = assembled.tokenStats;
|
|
1779
|
+
const budgetNote = ts.budget !== void 0 ? ` budget=${ts.budget} trimmed=${ts.trimmed}` : "";
|
|
1780
|
+
const stats = `[git-stats] files=${changed.length} ring1=${assembled.included.length} importers=${importers.length} unresolved=${assembled.unresolved.length} tokens=${ts.totalTokens}${budgetNote}`;
|
|
1781
|
+
return {
|
|
1782
|
+
content: [
|
|
1783
|
+
{ type: "text", text: parts.join("\n") },
|
|
1784
|
+
{ type: "text", text: stats }
|
|
1785
|
+
]
|
|
1786
|
+
};
|
|
1787
|
+
}
|
|
1788
|
+
);
|
|
1789
|
+
server.registerTool(
|
|
1790
|
+
"search_symbol_signatures",
|
|
1791
|
+
{
|
|
1792
|
+
title: "Search Symbol Signatures",
|
|
1793
|
+
description: "Fast, repo-wide lookup of named definitions (functions, methods, classes, interfaces, enums, \u2026). Returns compact `file:line \u2014 signature` lines instead of raw search hits. Use it to discover where a symbol lives and what its exact signature is before reading or editing code.",
|
|
1794
|
+
inputSchema: {
|
|
1795
|
+
query: import_zod.z.string().describe("Search text (matched against symbol names; falls back to signatures)"),
|
|
1796
|
+
maxResults: import_zod.z.number().int().min(1).max(100).optional().describe("Maximum results to return (default 10)"),
|
|
1797
|
+
kind: import_zod.z.enum(["function", "method", "arrow", "class", "interface", "enum", "type", "other"]).optional().describe("Only return definitions of this kind")
|
|
1798
|
+
}
|
|
1799
|
+
},
|
|
1800
|
+
async ({ query, maxResults, kind }) => {
|
|
1801
|
+
await ensureReadyFor(process.cwd());
|
|
1802
|
+
if (!searchIndex) {
|
|
1803
|
+
throw new Error("token-shrink symbol index failed to start");
|
|
1804
|
+
}
|
|
1805
|
+
if (searchIndex.size === 0 && watcher && watcher.cache.entries.size > 0) {
|
|
1806
|
+
searchIndex.loadCache(watcher.cache.entries);
|
|
1807
|
+
}
|
|
1808
|
+
const hits = searchIndex.search(query, {
|
|
1809
|
+
maxResults: maxResults ?? 10,
|
|
1810
|
+
kind
|
|
1811
|
+
});
|
|
1812
|
+
if (hits.length === 0) {
|
|
1813
|
+
return textReply(`No symbols match "${query}". Try a different name or kind.`);
|
|
1814
|
+
}
|
|
1815
|
+
const lines = hits.map((h) => `- \`${h.label}\``);
|
|
1816
|
+
return textReply(
|
|
1817
|
+
`${lines.join("\n")}
|
|
1818
|
+
|
|
1819
|
+
_Found ${hits.length} symbol${hits.length === 1 ? "" : "s"} matching "${query}"._`
|
|
1820
|
+
);
|
|
1821
|
+
}
|
|
1822
|
+
);
|
|
835
1823
|
const transport = new import_stdio.StdioServerTransport();
|
|
836
1824
|
await server.connect(transport);
|
|
837
1825
|
log("MCP server connected.");
|
|
838
1826
|
return server;
|
|
839
1827
|
}
|
|
840
|
-
var argv1 = process.argv[1] ?
|
|
841
|
-
|
|
1828
|
+
var argv1 = process.argv[1] ? import_node_path9.default.basename(process.argv[1]) : "";
|
|
1829
|
+
var argv1Real = "";
|
|
1830
|
+
try {
|
|
1831
|
+
argv1Real = process.argv[1] ? import_node_path9.default.basename(import_node_fs7.default.realpathSync(process.argv[1])) : "";
|
|
1832
|
+
} catch {
|
|
1833
|
+
}
|
|
1834
|
+
var invokedAsMcp = argv1 === "mcp.js" || argv1 === "mcp.mjs" || argv1 === "mcp.cjs" || argv1 === "mcp.ts" || argv1Real === "mcp.js" || argv1Real === "mcp.mjs" || argv1Real === "mcp.cjs" || argv1Real === "mcp.ts";
|
|
1835
|
+
if (invokedAsMcp) {
|
|
842
1836
|
const rootArg = (() => {
|
|
843
1837
|
const i = process.argv.indexOf("--root");
|
|
844
1838
|
if (i !== -1 && process.argv[i + 1]) return process.argv[i + 1];
|
|
@@ -870,6 +1864,8 @@ if (argv1 === "mcp.js" || argv1 === "mcp.mjs" || argv1 === "mcp.cjs" || argv1 ==
|
|
|
870
1864
|
CLINE_RULE_SENTINEL,
|
|
871
1865
|
CURSOR_RULE_PATH,
|
|
872
1866
|
RULE_TARGET_PATH,
|
|
1867
|
+
RULE_VERSION,
|
|
1868
|
+
RULE_VERSION_MARKER,
|
|
873
1869
|
createAutoRule,
|
|
874
1870
|
createCursorRule,
|
|
875
1871
|
startMcpServer
|