@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/index.cjs
CHANGED
|
@@ -189,20 +189,37 @@ __export(index_exports, {
|
|
|
189
189
|
CLAUDE_RULE_SENTINEL: () => CLAUDE_RULE_SENTINEL,
|
|
190
190
|
CLINE_RULE_PATH: () => CLINE_RULE_PATH,
|
|
191
191
|
CLINE_RULE_SENTINEL: () => CLINE_RULE_SENTINEL,
|
|
192
|
+
CONFIG_FILE_NAME: () => CONFIG_FILE_NAME,
|
|
192
193
|
CURSOR_RULE_PATH: () => CURSOR_RULE_PATH,
|
|
193
194
|
DEFAULT_IGNORED: () => DEFAULT_IGNORED,
|
|
195
|
+
EMPTY_CONFIG: () => EMPTY_CONFIG,
|
|
196
|
+
PROJECT_MANIFEST_FILES: () => PROJECT_MANIFEST_FILES,
|
|
194
197
|
RULE_TARGET_PATH: () => RULE_TARGET_PATH,
|
|
198
|
+
SymbolSearch: () => SymbolSearch,
|
|
199
|
+
VCS_MARKER_DIRS: () => VCS_MARKER_DIRS,
|
|
195
200
|
allExtensions: () => allExtensions,
|
|
201
|
+
analyze: () => analyze,
|
|
196
202
|
approximateTokens: () => approximateTokens,
|
|
197
203
|
assemble: () => assemble,
|
|
204
|
+
assembleMany: () => assembleMany,
|
|
205
|
+
collectSymbols: () => collectSymbols,
|
|
206
|
+
configPathFor: () => configPathFor,
|
|
198
207
|
createAutoRule: () => createAutoRule,
|
|
199
208
|
createCursorRule: () => createCursorRule,
|
|
200
209
|
createWatcher: () => createWatcher,
|
|
210
|
+
detectProjectRoot: () => detectProjectRoot,
|
|
211
|
+
ensureParserInit: () => ensureParserInit,
|
|
201
212
|
extractImports: () => extractImports,
|
|
202
213
|
extractSpecifiers: () => extractSpecifiers,
|
|
203
214
|
getGrammar: () => getGrammar,
|
|
215
|
+
globToRegExp: () => globToRegExp,
|
|
204
216
|
hashOf: () => hashOf,
|
|
205
217
|
languageForFile: () => languageForFile,
|
|
218
|
+
loadConfig: () => loadConfig,
|
|
219
|
+
loadLanguage: () => loadLanguage,
|
|
220
|
+
matchSymbols: () => matchSymbols,
|
|
221
|
+
matchesAny: () => matchesAny,
|
|
222
|
+
matchesGlob: () => matchesGlob,
|
|
206
223
|
prune: () => prune,
|
|
207
224
|
registry: () => registry,
|
|
208
225
|
resolveImport: () => resolveImport,
|
|
@@ -215,7 +232,7 @@ __export(index_exports, {
|
|
|
215
232
|
});
|
|
216
233
|
module.exports = __toCommonJS(index_exports);
|
|
217
234
|
|
|
218
|
-
// src/parser/
|
|
235
|
+
// src/parser/analyze.ts
|
|
219
236
|
var import_web_tree_sitter = __toESM(require("web-tree-sitter"), 1);
|
|
220
237
|
var import_node_module = require("module");
|
|
221
238
|
var import_node_path3 = __toESM(require("path"), 1);
|
|
@@ -300,7 +317,97 @@ async function warmGrammars(specs = []) {
|
|
|
300
317
|
return results.filter((r) => r.status === "fulfilled").length;
|
|
301
318
|
}
|
|
302
319
|
|
|
303
|
-
// src/parser/
|
|
320
|
+
// src/parser/symbols.ts
|
|
321
|
+
var KIND_BY_TYPE = {
|
|
322
|
+
// TypeScript / JavaScript / TSX / JSX
|
|
323
|
+
function_declaration: "function",
|
|
324
|
+
generator_function_declaration: "function",
|
|
325
|
+
function_expression: "function",
|
|
326
|
+
function_signature_item: "function",
|
|
327
|
+
method_definition: "method",
|
|
328
|
+
method_signature: "method",
|
|
329
|
+
class_declaration: "class",
|
|
330
|
+
abstract_class_declaration: "class",
|
|
331
|
+
interface_declaration: "interface",
|
|
332
|
+
enum_declaration: "enum",
|
|
333
|
+
type_alias_declaration: "type",
|
|
334
|
+
// Python
|
|
335
|
+
function_definition: "function",
|
|
336
|
+
class_definition: "class",
|
|
337
|
+
// Go
|
|
338
|
+
method_declaration: "method",
|
|
339
|
+
type_spec: "type",
|
|
340
|
+
// Rust
|
|
341
|
+
function_item: "function",
|
|
342
|
+
struct_item: "class",
|
|
343
|
+
enum_item: "enum",
|
|
344
|
+
trait_item: "interface",
|
|
345
|
+
type_item: "type",
|
|
346
|
+
// Java / Kotlin / PHP / Dart / Swift (best-effort generic names)
|
|
347
|
+
constructor_declaration: "method",
|
|
348
|
+
module_declaration: "class",
|
|
349
|
+
protocol_declaration: "interface"
|
|
350
|
+
};
|
|
351
|
+
var SIGNATURE_MAX = 200;
|
|
352
|
+
function collectSymbols(root, source) {
|
|
353
|
+
const out = [];
|
|
354
|
+
const walk2 = (node) => {
|
|
355
|
+
const kind = KIND_BY_TYPE[node.type];
|
|
356
|
+
if (kind) {
|
|
357
|
+
const nameNode = node.childForFieldName("name");
|
|
358
|
+
if (nameNode && nameNode.text.trim()) {
|
|
359
|
+
out.push(symbolFrom(nameNode.text.trim(), kind, node.startIndex, node.endIndex, source));
|
|
360
|
+
}
|
|
361
|
+
} else if (node.type === "variable_declarator") {
|
|
362
|
+
const value = node.childForFieldName("value");
|
|
363
|
+
const valueType = value?.type;
|
|
364
|
+
if (valueType === "arrow_function" || valueType === "function_expression") {
|
|
365
|
+
const nameNode = node.childForFieldName("name");
|
|
366
|
+
if (nameNode && nameNode.text.trim()) {
|
|
367
|
+
out.push(
|
|
368
|
+
symbolFrom(nameNode.text.trim(), "arrow", node.startIndex, node.endIndex, source)
|
|
369
|
+
);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
374
|
+
const child = node.child(i);
|
|
375
|
+
if (child) walk2(child);
|
|
376
|
+
}
|
|
377
|
+
};
|
|
378
|
+
walk2(root);
|
|
379
|
+
return out;
|
|
380
|
+
}
|
|
381
|
+
function matchSymbols(symbols, query, kind) {
|
|
382
|
+
const q = query.trim();
|
|
383
|
+
if (!q) return [];
|
|
384
|
+
const pool = kind ? symbols.filter((s) => s.kind === kind) : symbols;
|
|
385
|
+
const byName = pool.filter((s) => s.name === q);
|
|
386
|
+
if (byName.length > 0) return byName;
|
|
387
|
+
const byNameCi = pool.filter((s) => s.name.toLowerCase() === q.toLowerCase());
|
|
388
|
+
if (byNameCi.length > 0) return byNameCi;
|
|
389
|
+
const lower = q.toLowerCase();
|
|
390
|
+
const bySubstring = pool.filter((s) => s.name.toLowerCase().includes(lower));
|
|
391
|
+
return bySubstring.length > 0 ? bySubstring : pool.filter((s) => (s.signature ?? "").toLowerCase().includes(lower));
|
|
392
|
+
}
|
|
393
|
+
function symbolFrom(name, kind, start, end, source) {
|
|
394
|
+
return {
|
|
395
|
+
name,
|
|
396
|
+
kind,
|
|
397
|
+
line: source.slice(0, start).split("\n").length,
|
|
398
|
+
start,
|
|
399
|
+
end,
|
|
400
|
+
signature: signaturePreview(source, start, end)
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
function signaturePreview(source, start, end) {
|
|
404
|
+
const nl = source.indexOf("\n", start);
|
|
405
|
+
const endOfLine = nl === -1 ? end : nl;
|
|
406
|
+
const first = source.slice(start, Math.min(endOfLine, end)).trim();
|
|
407
|
+
return first.length > SIGNATURE_MAX ? `${first.slice(0, SIGNATURE_MAX)}\u2026` : first;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// src/parser/analyze.ts
|
|
304
411
|
var languageCache = /* @__PURE__ */ new Map();
|
|
305
412
|
var initPromise = null;
|
|
306
413
|
function ensureParserInit() {
|
|
@@ -333,35 +440,6 @@ function loadLanguage(spec, force = false) {
|
|
|
333
440
|
return language;
|
|
334
441
|
})();
|
|
335
442
|
}
|
|
336
|
-
async function prune(filePath, source, opts = {}) {
|
|
337
|
-
const spec = languageForFile(filePath);
|
|
338
|
-
if (!spec || spec.rules.length === 0) {
|
|
339
|
-
return { code: source, language: null, removed: 0 };
|
|
340
|
-
}
|
|
341
|
-
const language = await loadLanguage(spec, opts.forceDownload);
|
|
342
|
-
const parser = new import_web_tree_sitter.default();
|
|
343
|
-
parser.setLanguage(language);
|
|
344
|
-
const tree = parser.parse(source);
|
|
345
|
-
const ranges = [];
|
|
346
|
-
for (const rule of spec.rules) {
|
|
347
|
-
const query = language.query(rule.query);
|
|
348
|
-
const captures = query.captures(tree.rootNode);
|
|
349
|
-
for (const cap of captures) {
|
|
350
|
-
if (cap.node.startIndex === cap.node.endIndex) continue;
|
|
351
|
-
if (rule.replacement.keepIf?.test(cap.node.text)) continue;
|
|
352
|
-
ranges.push({
|
|
353
|
-
start: cap.node.startIndex,
|
|
354
|
-
end: cap.node.endIndex,
|
|
355
|
-
token: rule.replacement.token
|
|
356
|
-
});
|
|
357
|
-
}
|
|
358
|
-
query.delete();
|
|
359
|
-
}
|
|
360
|
-
tree.delete();
|
|
361
|
-
parser.delete();
|
|
362
|
-
const { code, removed } = spliceRanges(source, ranges);
|
|
363
|
-
return { code, language: spec.name, removed };
|
|
364
|
-
}
|
|
365
443
|
function spliceRanges(source, ranges) {
|
|
366
444
|
let removed = 0;
|
|
367
445
|
const sorted = [...ranges].sort((a, b) => b.start - a.start);
|
|
@@ -373,13 +451,354 @@ function spliceRanges(source, ranges) {
|
|
|
373
451
|
}
|
|
374
452
|
return { code: out, removed };
|
|
375
453
|
}
|
|
454
|
+
async function analyze(filePath, source, opts = {}) {
|
|
455
|
+
const spec = languageForFile(filePath);
|
|
456
|
+
if (!spec || spec.rules.length === 0) {
|
|
457
|
+
return { code: source, language: null, removed: 0, symbols: [] };
|
|
458
|
+
}
|
|
459
|
+
let language;
|
|
460
|
+
try {
|
|
461
|
+
language = await loadLanguage(spec, opts.forceDownload);
|
|
462
|
+
} catch {
|
|
463
|
+
return { code: source, language: null, removed: 0, symbols: [] };
|
|
464
|
+
}
|
|
465
|
+
const parser = new import_web_tree_sitter.default();
|
|
466
|
+
parser.setLanguage(language);
|
|
467
|
+
const tree = parser.parse(source);
|
|
468
|
+
try {
|
|
469
|
+
const symbols = collectSymbols(tree.rootNode, source);
|
|
470
|
+
if (opts.skipPrune) {
|
|
471
|
+
return { code: source, language: spec.name, removed: 0, symbols };
|
|
472
|
+
}
|
|
473
|
+
const keepRanges = [
|
|
474
|
+
...opts.keepBlocksInside ?? [],
|
|
475
|
+
...opts.preserveAnnotations?.length ? annotatedKeepRanges(symbols, source, opts.preserveAnnotations) : []
|
|
476
|
+
];
|
|
477
|
+
const isProtected = (start, end) => keepRanges.some((k) => k.start <= start && end <= k.end);
|
|
478
|
+
const ranges = [];
|
|
479
|
+
for (const rule of spec.rules) {
|
|
480
|
+
const query = language.query(rule.query);
|
|
481
|
+
const captures = query.captures(tree.rootNode);
|
|
482
|
+
for (const cap of captures) {
|
|
483
|
+
if (cap.node.startIndex === cap.node.endIndex) continue;
|
|
484
|
+
if (rule.replacement.keepIf?.test(cap.node.text)) continue;
|
|
485
|
+
if (isProtected(cap.node.startIndex, cap.node.endIndex)) continue;
|
|
486
|
+
ranges.push({
|
|
487
|
+
start: cap.node.startIndex,
|
|
488
|
+
end: cap.node.endIndex,
|
|
489
|
+
token: rule.replacement.token
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
query.delete();
|
|
493
|
+
}
|
|
494
|
+
const { code, removed } = spliceRanges(source, ranges);
|
|
495
|
+
return { code, language: spec.name, removed, symbols };
|
|
496
|
+
} finally {
|
|
497
|
+
tree.delete();
|
|
498
|
+
parser.delete();
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
async function prune(filePath, source, opts = {}) {
|
|
502
|
+
const res = await analyze(filePath, source, opts);
|
|
503
|
+
return { code: res.code, language: res.language, removed: res.removed };
|
|
504
|
+
}
|
|
505
|
+
var ANNOTATION_WINDOW = 240;
|
|
506
|
+
function annotatedKeepRanges(symbols, source, annotations) {
|
|
507
|
+
if (annotations.length === 0) return [];
|
|
508
|
+
const markers = annotations.filter(Boolean).map(escapeRegExp);
|
|
509
|
+
if (markers.length === 0) return [];
|
|
510
|
+
const re = new RegExp(`@(${markers.join("|")})\\b`, "i");
|
|
511
|
+
return symbols.filter((s) => {
|
|
512
|
+
const windowStart = Math.max(0, s.start - ANNOTATION_WINDOW);
|
|
513
|
+
const tail = source.slice(windowStart, s.start);
|
|
514
|
+
const cut = Math.max(tail.lastIndexOf("}\n"), tail.lastIndexOf(";\n"));
|
|
515
|
+
const relevant = cut === -1 ? tail : tail.slice(cut + 1);
|
|
516
|
+
return re.test(relevant);
|
|
517
|
+
}).map((s) => ({ start: s.start, end: s.end }));
|
|
518
|
+
}
|
|
519
|
+
function escapeRegExp(text) {
|
|
520
|
+
return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
521
|
+
}
|
|
376
522
|
|
|
377
523
|
// src/index.ts
|
|
378
524
|
init_registry();
|
|
379
525
|
|
|
380
|
-
// src/
|
|
526
|
+
// src/detect.ts
|
|
381
527
|
var import_node_fs2 = __toESM(require("fs"), 1);
|
|
382
528
|
var import_node_path4 = __toESM(require("path"), 1);
|
|
529
|
+
var VCS_MARKER_DIRS = [".git", ".hg", ".svn"];
|
|
530
|
+
var PROJECT_MANIFEST_FILES = [
|
|
531
|
+
// JavaScript / TypeScript
|
|
532
|
+
"package.json",
|
|
533
|
+
"tsconfig.json",
|
|
534
|
+
// Python
|
|
535
|
+
"pyproject.toml",
|
|
536
|
+
"setup.py",
|
|
537
|
+
"setup.cfg",
|
|
538
|
+
"requirements.txt",
|
|
539
|
+
// Go / Rust / Dart / Swift
|
|
540
|
+
"go.mod",
|
|
541
|
+
"Cargo.toml",
|
|
542
|
+
"pubspec.yaml",
|
|
543
|
+
"Package.swift",
|
|
544
|
+
// Java / Kotlin
|
|
545
|
+
"pom.xml",
|
|
546
|
+
"build.gradle",
|
|
547
|
+
"build.gradle.kts",
|
|
548
|
+
"settings.gradle",
|
|
549
|
+
"settings.gradle.kts",
|
|
550
|
+
// PHP / Ruby / Elixir
|
|
551
|
+
"composer.json",
|
|
552
|
+
"Gemfile",
|
|
553
|
+
"mix.exs"
|
|
554
|
+
];
|
|
555
|
+
var MANIFEST_FILE_SET = new Set(PROJECT_MANIFEST_FILES);
|
|
556
|
+
var MAX_WALK_DEPTH = 64;
|
|
557
|
+
function detectProjectRoot(fromPath) {
|
|
558
|
+
const resolved = import_node_path4.default.resolve(fromPath);
|
|
559
|
+
let dir = isDirectory(resolved) ? resolved : import_node_path4.default.dirname(resolved);
|
|
560
|
+
let nearestManifest = null;
|
|
561
|
+
for (let depth = 0; depth < MAX_WALK_DEPTH; depth++) {
|
|
562
|
+
const names = readDirNames(dir);
|
|
563
|
+
if (names) {
|
|
564
|
+
if (VCS_MARKER_DIRS.some((vcs) => names.has(vcs))) return dir;
|
|
565
|
+
if (nearestManifest === null && hasManifest(names)) nearestManifest = dir;
|
|
566
|
+
}
|
|
567
|
+
const parent = import_node_path4.default.dirname(dir);
|
|
568
|
+
if (parent === dir) break;
|
|
569
|
+
dir = parent;
|
|
570
|
+
}
|
|
571
|
+
return nearestManifest;
|
|
572
|
+
}
|
|
573
|
+
function isDirectory(p) {
|
|
574
|
+
try {
|
|
575
|
+
return import_node_fs2.default.statSync(p).isDirectory();
|
|
576
|
+
} catch {
|
|
577
|
+
return false;
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
function readDirNames(dir) {
|
|
581
|
+
try {
|
|
582
|
+
return new Set(import_node_fs2.default.readdirSync(dir));
|
|
583
|
+
} catch {
|
|
584
|
+
return null;
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
function hasManifest(names) {
|
|
588
|
+
for (const name of names) {
|
|
589
|
+
if (MANIFEST_FILE_SET.has(name)) return true;
|
|
590
|
+
}
|
|
591
|
+
return false;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// src/search/index.ts
|
|
595
|
+
function words(text) {
|
|
596
|
+
const out = /* @__PURE__ */ new Set();
|
|
597
|
+
for (const part of text.split(/[^A-Za-z0-9_$]+/)) {
|
|
598
|
+
if (!part) continue;
|
|
599
|
+
for (const seg of part.split(/(?<=[a-z0-9])(?=[A-Z])/)) {
|
|
600
|
+
const w = seg.toLowerCase();
|
|
601
|
+
if (w) out.add(w);
|
|
602
|
+
}
|
|
603
|
+
for (const seg of part.split("_")) {
|
|
604
|
+
const w = seg.toLowerCase();
|
|
605
|
+
if (w) out.add(w);
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
return [...out];
|
|
609
|
+
}
|
|
610
|
+
var SymbolSearch = class {
|
|
611
|
+
docs = [];
|
|
612
|
+
byFile = /* @__PURE__ */ new Map();
|
|
613
|
+
inverted = /* @__PURE__ */ new Map();
|
|
614
|
+
get size() {
|
|
615
|
+
return this.docs.length;
|
|
616
|
+
}
|
|
617
|
+
/** Replace the docs for one file (or remove when `symbols` is empty). */
|
|
618
|
+
setFile(filePath, symbols) {
|
|
619
|
+
this.removeFile(filePath);
|
|
620
|
+
if (symbols.length === 0) return;
|
|
621
|
+
const docs = symbols.map((s) => ({ ...s, filePath }));
|
|
622
|
+
const ids = docs.map((d) => {
|
|
623
|
+
const id = this.docs.length;
|
|
624
|
+
this.docs.push(d);
|
|
625
|
+
for (const w of words(`${d.name}`)) {
|
|
626
|
+
const list = this.inverted.get(w) ?? [];
|
|
627
|
+
list.push(id);
|
|
628
|
+
this.inverted.set(w, list);
|
|
629
|
+
}
|
|
630
|
+
return id;
|
|
631
|
+
});
|
|
632
|
+
this.byFile.set(filePath, docs);
|
|
633
|
+
void ids;
|
|
634
|
+
}
|
|
635
|
+
removeFile(filePath) {
|
|
636
|
+
const docs = this.byFile.get(filePath);
|
|
637
|
+
if (!docs) return;
|
|
638
|
+
const removed = new Set(docs);
|
|
639
|
+
this.docs = this.docs.filter((d) => !removed.has(d));
|
|
640
|
+
this.byFile.delete(filePath);
|
|
641
|
+
this.rebuildInverted();
|
|
642
|
+
}
|
|
643
|
+
/** Bulk-load a whole cache (used at attach time and cold lazy builds). */
|
|
644
|
+
loadCache(entries) {
|
|
645
|
+
this.docs = [];
|
|
646
|
+
this.byFile.clear();
|
|
647
|
+
for (const [abs, entry] of entries) {
|
|
648
|
+
if (!entry.symbols || entry.symbols.length === 0) continue;
|
|
649
|
+
const docs = entry.symbols.map((s) => ({ ...s, filePath: abs }));
|
|
650
|
+
this.byFile.set(abs, docs);
|
|
651
|
+
this.docs.push(...docs);
|
|
652
|
+
}
|
|
653
|
+
this.rebuildInverted();
|
|
654
|
+
}
|
|
655
|
+
/** Ranked symbol hits for `query`. */
|
|
656
|
+
search(query, opts = {}) {
|
|
657
|
+
const maxResults = opts.maxResults ?? 10;
|
|
658
|
+
const q = query.trim().toLowerCase();
|
|
659
|
+
if (!q) return [];
|
|
660
|
+
const queryWords = words(q);
|
|
661
|
+
let candidates;
|
|
662
|
+
if (queryWords.length > 0) {
|
|
663
|
+
const ids = /* @__PURE__ */ new Set();
|
|
664
|
+
for (const w of queryWords) {
|
|
665
|
+
const post = this.inverted.get(w);
|
|
666
|
+
if (post) for (const id of post) ids.add(id);
|
|
667
|
+
}
|
|
668
|
+
candidates = [...ids].map((id) => this.docs[id]);
|
|
669
|
+
} else {
|
|
670
|
+
candidates = this.docs;
|
|
671
|
+
}
|
|
672
|
+
if (opts.kind) candidates = candidates.filter((d) => d.kind === opts.kind);
|
|
673
|
+
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);
|
|
674
|
+
return scored.map((s) => ({ ...s, label: this.label(s) }));
|
|
675
|
+
}
|
|
676
|
+
score(d, q, queryWords) {
|
|
677
|
+
const name = d.name.toLowerCase();
|
|
678
|
+
let score = 0;
|
|
679
|
+
if (name === q) score += 100;
|
|
680
|
+
if (name.startsWith(q)) score += 60;
|
|
681
|
+
if (name.includes(q)) score += 30;
|
|
682
|
+
if ((d.signature ?? "").toLowerCase().includes(q)) score += 8;
|
|
683
|
+
for (const w of queryWords) {
|
|
684
|
+
if (name.includes(w)) score += 4;
|
|
685
|
+
if ((d.signature ?? "").toLowerCase().includes(w)) score += 1;
|
|
686
|
+
}
|
|
687
|
+
if (score <= 0) return null;
|
|
688
|
+
return { ...d, score };
|
|
689
|
+
}
|
|
690
|
+
label(d) {
|
|
691
|
+
const sig = d.signature && d.signature.length > 0 ? ` \u2014 ${d.signature}` : "";
|
|
692
|
+
return `${d.filePath}:${d.line}${sig}`;
|
|
693
|
+
}
|
|
694
|
+
rebuildInverted() {
|
|
695
|
+
this.inverted.clear();
|
|
696
|
+
this.docs.forEach((d, id) => {
|
|
697
|
+
for (const w of words(d.name)) {
|
|
698
|
+
const list = this.inverted.get(w) ?? [];
|
|
699
|
+
list.push(id);
|
|
700
|
+
this.inverted.set(w, list);
|
|
701
|
+
}
|
|
702
|
+
});
|
|
703
|
+
}
|
|
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
|
+
}
|
|
798
|
+
|
|
799
|
+
// src/watcher/sync.ts
|
|
800
|
+
var import_node_fs4 = __toESM(require("fs"), 1);
|
|
801
|
+
var import_node_path6 = __toESM(require("path"), 1);
|
|
383
802
|
var import_node_crypto = require("crypto");
|
|
384
803
|
var import_chokidar = __toESM(require("chokidar"), 1);
|
|
385
804
|
var DEFAULT_IGNORED = [
|
|
@@ -399,7 +818,7 @@ var ContextCache = class {
|
|
|
399
818
|
}
|
|
400
819
|
/** Returns the cached skeleton for a file, if present. */
|
|
401
820
|
getSkeleton(filePath) {
|
|
402
|
-
return this.entries.get(
|
|
821
|
+
return this.entries.get(import_node_path6.default.resolve(filePath)) ?? null;
|
|
403
822
|
}
|
|
404
823
|
get imports() {
|
|
405
824
|
return this.entries;
|
|
@@ -421,18 +840,18 @@ function extractImports(_filePath, source) {
|
|
|
421
840
|
function resolveImport(importer, specifier, root) {
|
|
422
841
|
if (!/^[.~@]/.test(specifier)) {
|
|
423
842
|
if (specifier.startsWith("@/")) {
|
|
424
|
-
return resolveCandidate(
|
|
843
|
+
return resolveCandidate(import_node_path6.default.join(root, specifier.slice(2)));
|
|
425
844
|
}
|
|
426
845
|
if (specifier.startsWith("@")) {
|
|
427
846
|
return null;
|
|
428
847
|
}
|
|
429
848
|
if (specifier.startsWith("~")) {
|
|
430
|
-
return resolveCandidate(
|
|
849
|
+
return resolveCandidate(import_node_path6.default.join(root, specifier.slice(1)));
|
|
431
850
|
}
|
|
432
851
|
return null;
|
|
433
852
|
}
|
|
434
|
-
const base =
|
|
435
|
-
return resolveCandidate(
|
|
853
|
+
const base = import_node_path6.default.dirname(import_node_path6.default.resolve(importer));
|
|
854
|
+
return resolveCandidate(import_node_path6.default.resolve(base, specifier));
|
|
436
855
|
}
|
|
437
856
|
function resolveCandidate(p) {
|
|
438
857
|
const candidates = [
|
|
@@ -455,29 +874,34 @@ function resolveCandidate(p) {
|
|
|
455
874
|
`${p}.h`,
|
|
456
875
|
`${p}.hpp`,
|
|
457
876
|
`${p}.php`,
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
877
|
+
import_node_path6.default.join(p, "index.ts"),
|
|
878
|
+
import_node_path6.default.join(p, "index.js"),
|
|
879
|
+
import_node_path6.default.join(p, "index.tsx"),
|
|
880
|
+
import_node_path6.default.join(p, "index.jsx"),
|
|
881
|
+
import_node_path6.default.join(p, "index.py")
|
|
463
882
|
];
|
|
464
883
|
for (const c of candidates) {
|
|
465
|
-
if (
|
|
884
|
+
if (import_node_fs4.default.existsSync(c) && import_node_fs4.default.statSync(c).isFile()) return import_node_path6.default.resolve(c);
|
|
466
885
|
}
|
|
467
886
|
return null;
|
|
468
887
|
}
|
|
469
888
|
function createWatcher(opts) {
|
|
470
889
|
const cache = new ContextCache();
|
|
471
|
-
const { root, ignored = DEFAULT_IGNORED, onIndexed } = opts;
|
|
890
|
+
const { root, ignored = DEFAULT_IGNORED, config, onIndexed, onRemoved } = opts;
|
|
891
|
+
const rel2 = (abs) => import_node_path6.default.relative(root, abs);
|
|
892
|
+
const effectiveIgnored = [
|
|
893
|
+
...ignored,
|
|
894
|
+
...config?.ignorePatterns?.length ? [(_abs) => matchesAny(rel2(String(_abs)), config.ignorePatterns)] : []
|
|
895
|
+
];
|
|
472
896
|
const pending = /* @__PURE__ */ new Map();
|
|
473
897
|
const DEBOUNCE_MS = 100;
|
|
474
898
|
async function handle(filePath) {
|
|
475
|
-
const abs =
|
|
899
|
+
const abs = import_node_path6.default.resolve(filePath);
|
|
476
900
|
let source;
|
|
477
901
|
try {
|
|
478
|
-
const st =
|
|
902
|
+
const st = import_node_fs4.default.statSync(abs);
|
|
479
903
|
if (!st.isFile()) return;
|
|
480
|
-
source =
|
|
904
|
+
source = import_node_fs4.default.readFileSync(abs, "utf8");
|
|
481
905
|
} catch {
|
|
482
906
|
return;
|
|
483
907
|
}
|
|
@@ -485,13 +909,18 @@ function createWatcher(opts) {
|
|
|
485
909
|
const cached = cache.entries.get(abs);
|
|
486
910
|
if (cached && cached.hash === hash) return;
|
|
487
911
|
await cache.ensureInit();
|
|
488
|
-
const
|
|
489
|
-
const
|
|
912
|
+
const keepFull = config?.keepUnpruned?.length ? matchesAny(import_node_path6.default.relative(root, abs), config.keepUnpruned) : false;
|
|
913
|
+
const { code, language, symbols } = await analyze(abs, source, {
|
|
914
|
+
skipPrune: keepFull,
|
|
915
|
+
preserveAnnotations: config?.preserveAnnotations
|
|
916
|
+
});
|
|
917
|
+
const imports = extractImports(abs, source);
|
|
490
918
|
const entry = {
|
|
491
919
|
hash,
|
|
492
920
|
skeleton: code,
|
|
493
921
|
language,
|
|
494
|
-
imports: imports.map((spec) => resolveImport(abs, spec, root)).filter((p) => p !== null)
|
|
922
|
+
imports: imports.map((spec) => resolveImport(abs, spec, root)).filter((p) => p !== null),
|
|
923
|
+
...symbols.length > 0 ? { symbols } : {}
|
|
495
924
|
};
|
|
496
925
|
cache.entries.set(abs, entry);
|
|
497
926
|
onIndexed?.(abs, entry);
|
|
@@ -501,7 +930,7 @@ function createWatcher(opts) {
|
|
|
501
930
|
// sockets) are never opened with fs.watch (which raises UVException).
|
|
502
931
|
ignored(_p, stats) {
|
|
503
932
|
if (stats && !stats.isFile() && !stats.isDirectory()) return true;
|
|
504
|
-
return isIgnored(
|
|
933
|
+
return isIgnored(import_node_path6.default.resolve(String(_p)), effectiveIgnored);
|
|
505
934
|
},
|
|
506
935
|
alwaysStat: true,
|
|
507
936
|
ignoreInitial: true,
|
|
@@ -509,7 +938,7 @@ function createWatcher(opts) {
|
|
|
509
938
|
awaitWriteFinish: { stabilityThreshold: 50, pollInterval: 10 }
|
|
510
939
|
});
|
|
511
940
|
function debounce(filePath) {
|
|
512
|
-
const abs =
|
|
941
|
+
const abs = import_node_path6.default.resolve(filePath);
|
|
513
942
|
const existing = pending.get(abs);
|
|
514
943
|
if (existing) clearTimeout(existing);
|
|
515
944
|
if (!existing) void handle(abs);
|
|
@@ -521,8 +950,9 @@ function createWatcher(opts) {
|
|
|
521
950
|
watcher.on("add", debounce);
|
|
522
951
|
watcher.on("change", debounce);
|
|
523
952
|
watcher.on("unlink", (filePath) => {
|
|
524
|
-
const abs =
|
|
953
|
+
const abs = import_node_path6.default.resolve(filePath);
|
|
525
954
|
cache.entries.delete(abs);
|
|
955
|
+
onRemoved?.(abs);
|
|
526
956
|
});
|
|
527
957
|
return {
|
|
528
958
|
cache,
|
|
@@ -535,7 +965,7 @@ function createWatcher(opts) {
|
|
|
535
965
|
*/
|
|
536
966
|
async indexAll() {
|
|
537
967
|
const files = [];
|
|
538
|
-
await walk(root, (f) => files.push(f),
|
|
968
|
+
await walk(root, (f) => files.push(f), effectiveIgnored);
|
|
539
969
|
let ok = 0;
|
|
540
970
|
for (const f of files) {
|
|
541
971
|
try {
|
|
@@ -550,9 +980,9 @@ function createWatcher(opts) {
|
|
|
550
980
|
};
|
|
551
981
|
}
|
|
552
982
|
async function walk(root, push, ignored) {
|
|
553
|
-
const entries = await
|
|
983
|
+
const entries = await import_node_fs4.default.promises.readdir(root, { withFileTypes: true });
|
|
554
984
|
for (const e of entries) {
|
|
555
|
-
const abs =
|
|
985
|
+
const abs = import_node_path6.default.join(root, e.name);
|
|
556
986
|
if (isIgnored(abs, ignored)) continue;
|
|
557
987
|
if (e.isDirectory()) {
|
|
558
988
|
await walk(abs, push, ignored);
|
|
@@ -563,6 +993,10 @@ async function walk(root, push, ignored) {
|
|
|
563
993
|
}
|
|
564
994
|
function isIgnored(abs, ignored) {
|
|
565
995
|
for (const m of ignored) {
|
|
996
|
+
if (typeof m === "function") {
|
|
997
|
+
if (m(abs)) return true;
|
|
998
|
+
continue;
|
|
999
|
+
}
|
|
566
1000
|
if (typeof m === "string" && abs.includes(m)) return true;
|
|
567
1001
|
if (m instanceof RegExp && m.test(abs)) return true;
|
|
568
1002
|
}
|
|
@@ -570,100 +1004,179 @@ function isIgnored(abs, ignored) {
|
|
|
570
1004
|
}
|
|
571
1005
|
|
|
572
1006
|
// src/server/assembler.ts
|
|
573
|
-
var
|
|
574
|
-
var
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
const
|
|
578
|
-
|
|
579
|
-
|
|
1007
|
+
var import_node_fs5 = __toESM(require("fs"), 1);
|
|
1008
|
+
var import_node_path7 = __toESM(require("path"), 1);
|
|
1009
|
+
var MARKDOWN_OVERHEAD_TOKENS = 40;
|
|
1010
|
+
function assembleMany(activeFiles, cache, opts = {}) {
|
|
1011
|
+
const {
|
|
1012
|
+
includeStats = false,
|
|
1013
|
+
maxSkeletons = 50,
|
|
1014
|
+
maxTokens,
|
|
1015
|
+
pruneActiveFile = false
|
|
1016
|
+
} = opts;
|
|
1017
|
+
const absList = activeFiles.map((p) => import_node_path7.default.resolve(p));
|
|
1018
|
+
const sources = absList.map((abs) => {
|
|
1019
|
+
try {
|
|
1020
|
+
return import_node_fs5.default.readFileSync(abs, "utf8");
|
|
1021
|
+
} catch {
|
|
1022
|
+
return "";
|
|
1023
|
+
}
|
|
1024
|
+
});
|
|
1025
|
+
const activeSet = new Set(absList);
|
|
1026
|
+
const ring0Texts = absList.map((abs, i) => {
|
|
1027
|
+
const entry = cache.get(abs);
|
|
1028
|
+
return pruneActiveFile && entry ? entry.skeleton : sources[i];
|
|
1029
|
+
});
|
|
580
1030
|
const included = [];
|
|
581
1031
|
const unresolved = [];
|
|
582
1032
|
const seen = /* @__PURE__ */ new Set();
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
}
|
|
595
|
-
}
|
|
596
|
-
|
|
597
|
-
const
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
}
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
1033
|
+
const addUnresolved = (p) => {
|
|
1034
|
+
if (!unresolved.includes(p)) unresolved.push(p);
|
|
1035
|
+
};
|
|
1036
|
+
const addIncluded = (p, allowUncached) => {
|
|
1037
|
+
if (seen.has(p)) return;
|
|
1038
|
+
const entry = cache.get(p);
|
|
1039
|
+
if (!entry && !allowUncached) {
|
|
1040
|
+
addUnresolved(p);
|
|
1041
|
+
return;
|
|
1042
|
+
}
|
|
1043
|
+
seen.add(p);
|
|
1044
|
+
included.push({ filePath: p, language: entry?.language ?? null });
|
|
1045
|
+
};
|
|
1046
|
+
for (let i = 0; i < absList.length; i++) {
|
|
1047
|
+
const abs = absList[i];
|
|
1048
|
+
const entry = cache.get(abs);
|
|
1049
|
+
if (entry) {
|
|
1050
|
+
for (const imp of entry.imports) {
|
|
1051
|
+
if (activeSet.has(imp)) continue;
|
|
1052
|
+
addIncluded(imp, false);
|
|
1053
|
+
}
|
|
1054
|
+
} else if (sources[i]) {
|
|
1055
|
+
for (const spec of extractSpecifiers(sources[i])) {
|
|
1056
|
+
const resolved = resolveLocal(abs, spec);
|
|
1057
|
+
if (!resolved) {
|
|
1058
|
+
addUnresolved(spec);
|
|
1059
|
+
continue;
|
|
1060
|
+
}
|
|
1061
|
+
if (activeSet.has(resolved)) continue;
|
|
1062
|
+
addIncluded(resolved, true);
|
|
1063
|
+
}
|
|
609
1064
|
}
|
|
610
1065
|
}
|
|
1066
|
+
const ordered = maxTokens === void 0 ? included : rankByRelevance(included, cache);
|
|
1067
|
+
const budget = maxTokens === void 0 ? void 0 : Math.max(0, maxTokens - MARKDOWN_OVERHEAD_TOKENS);
|
|
1068
|
+
const selected = [];
|
|
1069
|
+
let trimmed = 0;
|
|
1070
|
+
let dependencyTokens = 0;
|
|
1071
|
+
for (const cand of ordered) {
|
|
1072
|
+
if (selected.length >= maxSkeletons) break;
|
|
1073
|
+
const entry = cache.get(cand.filePath);
|
|
1074
|
+
const cost = entry ? approximateTokens(entry.skeleton) : 8;
|
|
1075
|
+
if (budget !== void 0 && dependencyTokens + cost > budget) {
|
|
1076
|
+
trimmed++;
|
|
1077
|
+
continue;
|
|
1078
|
+
}
|
|
1079
|
+
selected.push(cand);
|
|
1080
|
+
dependencyTokens += cost;
|
|
1081
|
+
}
|
|
1082
|
+
const activeTokens = ring0Texts.reduce(
|
|
1083
|
+
(acc, t) => acc + approximateTokens(t),
|
|
1084
|
+
0
|
|
1085
|
+
);
|
|
611
1086
|
const lines = [];
|
|
612
1087
|
lines.push("# Compressed Code Context", "");
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
1088
|
+
if (absList.length === 1) {
|
|
1089
|
+
lines.push(`Active file: \`${rel(absList[0])}\``, "");
|
|
1090
|
+
} else {
|
|
1091
|
+
lines.push("Active files:", "");
|
|
1092
|
+
for (const p of absList) lines.push(`- \`${rel(p)}\``);
|
|
1093
|
+
lines.push("");
|
|
1094
|
+
}
|
|
1095
|
+
lines.push(
|
|
1096
|
+
absList.length === 1 ? "## Ring 0 \u2014 Active file (full text)" : "## Ring 0 \u2014 Active files (full text)",
|
|
1097
|
+
""
|
|
1098
|
+
);
|
|
1099
|
+
for (let i = 0; i < absList.length; i++) {
|
|
1100
|
+
if (absList.length > 1) lines.push(`### \`${rel(absList[i])}\``, "");
|
|
1101
|
+
const body = ring0Texts[i].trim() || "(empty or unreadable file)";
|
|
1102
|
+
lines.push(`\`\`\`${ext(absList[i])}`, body, "```", "");
|
|
1103
|
+
}
|
|
619
1104
|
lines.push(
|
|
620
|
-
`## Ring 1 \u2014 Pruned dependencies (${
|
|
1105
|
+
`## Ring 1 \u2014 Pruned dependencies (${selected.length})`,
|
|
621
1106
|
"",
|
|
622
1107
|
"Implementation bodies removed; type signatures, interfaces and exports retained.",
|
|
623
1108
|
""
|
|
624
1109
|
);
|
|
625
|
-
if (
|
|
1110
|
+
if (selected.length === 0) {
|
|
626
1111
|
lines.push("_No local dependency skeletons available._", "");
|
|
627
1112
|
}
|
|
628
|
-
for (const inc of
|
|
1113
|
+
for (const inc of selected) {
|
|
629
1114
|
const entry = cache.get(inc.filePath);
|
|
630
1115
|
const label = rel(inc.filePath);
|
|
631
1116
|
lines.push(`### \`${label}\``, "");
|
|
632
1117
|
if (entry) {
|
|
633
|
-
lines.push(`\`\`\`${ext(inc.filePath)}
|
|
634
|
-
lines.push(entry.skeleton.trim());
|
|
635
|
-
lines.push("```", "");
|
|
1118
|
+
lines.push(`\`\`\`${ext(inc.filePath)}`, entry.skeleton.trim(), "```", "");
|
|
636
1119
|
} else {
|
|
637
1120
|
lines.push("_Unindexed file._", "");
|
|
638
1121
|
}
|
|
639
1122
|
}
|
|
1123
|
+
if (maxTokens !== void 0 && trimmed > 0) {
|
|
1124
|
+
lines.push(
|
|
1125
|
+
`_Note: token budget of ${maxTokens} excluded ${trimmed} lower-priority dependenc${trimmed === 1 ? "y" : "ies"}._`,
|
|
1126
|
+
""
|
|
1127
|
+
);
|
|
1128
|
+
}
|
|
640
1129
|
if (unresolved.length > 0) {
|
|
641
1130
|
lines.push("## Unresolved imports", "");
|
|
642
1131
|
for (const u of unresolved) lines.push(`- \`${u}\``);
|
|
643
1132
|
lines.push("");
|
|
644
1133
|
}
|
|
645
1134
|
if (includeStats) {
|
|
646
|
-
const
|
|
647
|
-
(acc, inc) => {
|
|
648
|
-
const e = cache.get(inc.filePath);
|
|
649
|
-
return e ? acc + approximateTokens(e.skeleton) : acc;
|
|
650
|
-
},
|
|
651
|
-
0
|
|
652
|
-
);
|
|
653
|
-
lines.push("---", "");
|
|
1135
|
+
const budgetNote = maxTokens !== void 0 ? ` \xB7 budget ${maxTokens} (${trimmed} deps trimmed)` : "";
|
|
654
1136
|
lines.push(
|
|
655
|
-
|
|
1137
|
+
"---",
|
|
1138
|
+
"",
|
|
1139
|
+
`_Token estimate \u2014 active: ${activeTokens} \xB7 pruned deps: ${dependencyTokens}${budgetNote}._`,
|
|
656
1140
|
""
|
|
657
1141
|
);
|
|
658
1142
|
}
|
|
659
1143
|
return {
|
|
660
1144
|
markdown: lines.join("\n"),
|
|
661
|
-
activeFilePath:
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
1145
|
+
activeFilePath: absList[0] ?? "",
|
|
1146
|
+
activeFilePaths: absList,
|
|
1147
|
+
activeSource: sources.join("\n"),
|
|
1148
|
+
included: selected,
|
|
1149
|
+
unresolved,
|
|
1150
|
+
tokenStats: {
|
|
1151
|
+
activeTokens,
|
|
1152
|
+
dependencyTokens,
|
|
1153
|
+
totalTokens: activeTokens + dependencyTokens,
|
|
1154
|
+
...maxTokens !== void 0 ? { budget: maxTokens } : {},
|
|
1155
|
+
trimmed
|
|
1156
|
+
}
|
|
665
1157
|
};
|
|
666
1158
|
}
|
|
1159
|
+
function assemble(activeFilePath, cache, opts = {}) {
|
|
1160
|
+
return assembleMany([activeFilePath], cache, opts);
|
|
1161
|
+
}
|
|
1162
|
+
function rankByRelevance(candidates, cache) {
|
|
1163
|
+
const fanIn = /* @__PURE__ */ new Map();
|
|
1164
|
+
for (const entry of cache.values()) {
|
|
1165
|
+
for (const imp of entry.imports) fanIn.set(imp, (fanIn.get(imp) ?? 0) + 1);
|
|
1166
|
+
}
|
|
1167
|
+
return candidates.map((cand, idx) => ({ cand, idx })).sort((a, b) => {
|
|
1168
|
+
const fa = fanIn.get(a.cand.filePath) ?? 0;
|
|
1169
|
+
const fb = fanIn.get(b.cand.filePath) ?? 0;
|
|
1170
|
+
if (fa !== fb) return fb - fa;
|
|
1171
|
+
const da = pathDepth(a.cand.filePath);
|
|
1172
|
+
const db = pathDepth(b.cand.filePath);
|
|
1173
|
+
if (da !== db) return da - db;
|
|
1174
|
+
return a.idx - b.idx;
|
|
1175
|
+
}).map((x) => x.cand);
|
|
1176
|
+
}
|
|
1177
|
+
function pathDepth(p) {
|
|
1178
|
+
return p.split(/[/\\]+/).filter(Boolean).length;
|
|
1179
|
+
}
|
|
667
1180
|
function extractSpecifiers(source) {
|
|
668
1181
|
const found = [];
|
|
669
1182
|
const re = /(?:from\s+['"`]|import\s+['"`]|require\s*\(\s*['"`])([^'"`]+)['"`]/g;
|
|
@@ -677,8 +1190,8 @@ function resolveLocal(importer, specifier) {
|
|
|
677
1190
|
if (!/^[.~@]/.test(specifier)) return null;
|
|
678
1191
|
if (specifier.startsWith("@/")) specifier = specifier.slice(2);
|
|
679
1192
|
else if (specifier.startsWith("~")) specifier = specifier.slice(1);
|
|
680
|
-
const base =
|
|
681
|
-
const p =
|
|
1193
|
+
const base = import_node_path7.default.dirname(import_node_path7.default.resolve(importer));
|
|
1194
|
+
const p = import_node_path7.default.resolve(base, specifier);
|
|
682
1195
|
const candidates = [
|
|
683
1196
|
p,
|
|
684
1197
|
`${p}.ts`,
|
|
@@ -694,11 +1207,11 @@ function resolveLocal(importer, specifier) {
|
|
|
694
1207
|
`${p}.kt`,
|
|
695
1208
|
`${p}.c`,
|
|
696
1209
|
`${p}.cpp`,
|
|
697
|
-
|
|
698
|
-
|
|
1210
|
+
import_node_path7.default.join(p, "index.ts"),
|
|
1211
|
+
import_node_path7.default.join(p, "index.js")
|
|
699
1212
|
];
|
|
700
1213
|
for (const c of candidates) {
|
|
701
|
-
if (
|
|
1214
|
+
if (import_node_fs5.default.existsSync(c) && import_node_fs5.default.statSync(c).isFile()) return import_node_path7.default.resolve(c);
|
|
702
1215
|
}
|
|
703
1216
|
return null;
|
|
704
1217
|
}
|
|
@@ -711,7 +1224,7 @@ function rel(p) {
|
|
|
711
1224
|
return p;
|
|
712
1225
|
}
|
|
713
1226
|
function ext(p) {
|
|
714
|
-
return
|
|
1227
|
+
return import_node_path7.default.extname(p).replace(/^\./, "") || "text";
|
|
715
1228
|
}
|
|
716
1229
|
function approximateTokens(text) {
|
|
717
1230
|
if (!text) return 0;
|
|
@@ -722,7 +1235,8 @@ function approximateTokens(text) {
|
|
|
722
1235
|
// src/cli.ts
|
|
723
1236
|
var import_fastify = __toESM(require("fastify"), 1);
|
|
724
1237
|
var import_picocolors = __toESM(require("picocolors"), 1);
|
|
725
|
-
var
|
|
1238
|
+
var import_node_fs6 = __toESM(require("fs"), 1);
|
|
1239
|
+
var import_node_path8 = __toESM(require("path"), 1);
|
|
726
1240
|
function parseArgv(argv = process.argv) {
|
|
727
1241
|
const out = {};
|
|
728
1242
|
for (let i = 0; i < argv.length; i++) {
|
|
@@ -744,8 +1258,9 @@ async function startServer(opts = {}) {
|
|
|
744
1258
|
const args = parseArgv();
|
|
745
1259
|
const port = opts.port ?? Number(args.port ?? process.env.PORT ?? 3e3);
|
|
746
1260
|
const host = opts.host ?? args.host ?? "0.0.0.0";
|
|
747
|
-
const root =
|
|
1261
|
+
const root = import_node_path8.default.resolve(opts.root ?? args.root ?? process.cwd());
|
|
748
1262
|
const silent = opts.silent ?? args.silent === "true";
|
|
1263
|
+
const maxTokensDefault = args["max-tokens"] ? Number(args["max-tokens"]) : void 0;
|
|
749
1264
|
const log = (msg) => {
|
|
750
1265
|
if (!silent) console.log(import_picocolors.default.dim(msg));
|
|
751
1266
|
};
|
|
@@ -764,18 +1279,27 @@ async function startServer(opts = {}) {
|
|
|
764
1279
|
}));
|
|
765
1280
|
app.post("/v1/context", async (req, reply) => {
|
|
766
1281
|
const body = req.body ?? {};
|
|
767
|
-
|
|
768
|
-
|
|
1282
|
+
const hasMany = Array.isArray(body.activeFiles) && body.activeFiles.length > 0;
|
|
1283
|
+
if (hasMany && body.activeFilePath) {
|
|
1284
|
+
return reply.status(400).send({ error: "Pass either `activeFilePath` or `activeFiles`, not both." });
|
|
769
1285
|
}
|
|
770
|
-
const
|
|
1286
|
+
const paths = hasMany ? body.activeFiles : body.activeFilePath ? [body.activeFilePath] : [];
|
|
1287
|
+
if (paths.length === 0) {
|
|
1288
|
+
return reply.status(400).send({ error: "`activeFilePath` or `activeFiles` is required" });
|
|
1289
|
+
}
|
|
1290
|
+
const maxTokens = body.maxTokens !== void 0 ? body.maxTokens : maxTokensDefault;
|
|
1291
|
+
const result = assembleMany(paths, watcher.cache.entries, {
|
|
771
1292
|
maxSkeletons: body.maxSkeletons,
|
|
1293
|
+
maxTokens: Number.isFinite(maxTokens) ? maxTokens : void 0,
|
|
772
1294
|
includeStats: body.includeStats
|
|
773
1295
|
});
|
|
774
1296
|
return {
|
|
775
1297
|
markdown: result.markdown,
|
|
776
1298
|
activeFilePath: result.activeFilePath,
|
|
1299
|
+
activeFilePaths: result.activeFilePaths,
|
|
777
1300
|
dependencies: result.included.map((i) => i.filePath),
|
|
778
|
-
unresolved: result.unresolved
|
|
1301
|
+
unresolved: result.unresolved,
|
|
1302
|
+
tokenStats: result.tokenStats
|
|
779
1303
|
};
|
|
780
1304
|
});
|
|
781
1305
|
app.setNotFoundHandler(async (req, reply) => {
|
|
@@ -786,8 +1310,13 @@ async function startServer(opts = {}) {
|
|
|
786
1310
|
log(`Listening on http://${host}:${port}`);
|
|
787
1311
|
return { app, watcher };
|
|
788
1312
|
}
|
|
789
|
-
var argv1 = process.argv[1] ?
|
|
790
|
-
|
|
1313
|
+
var argv1 = process.argv[1] ? import_node_path8.default.basename(process.argv[1]) : "";
|
|
1314
|
+
var argv1Real = "";
|
|
1315
|
+
try {
|
|
1316
|
+
argv1Real = process.argv[1] ? import_node_path8.default.basename(import_node_fs6.default.realpathSync(process.argv[1])) : "";
|
|
1317
|
+
} catch {
|
|
1318
|
+
}
|
|
1319
|
+
if (argv1 === "cli.js" || argv1 === "cli.mjs" || argv1 === "cli.cjs" || argv1 === "cli.ts" || argv1Real === "cli.js" || argv1Real === "cli.mjs" || argv1Real === "cli.cjs" || argv1Real === "cli.ts") {
|
|
791
1320
|
startServer().catch((err) => {
|
|
792
1321
|
console.error(import_picocolors.default.red(`[token-shrink] ${err.message}`));
|
|
793
1322
|
process.exitCode = 1;
|
|
@@ -798,13 +1327,109 @@ if (argv1 === "cli.js" || argv1 === "cli.mjs" || argv1 === "cli.cjs" || argv1 ==
|
|
|
798
1327
|
var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
799
1328
|
var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
800
1329
|
var import_zod = require("zod");
|
|
801
|
-
var
|
|
802
|
-
var
|
|
1330
|
+
var import_node_fs8 = __toESM(require("fs"), 1);
|
|
1331
|
+
var import_node_path10 = __toESM(require("path"), 1);
|
|
1332
|
+
init_registry();
|
|
1333
|
+
|
|
1334
|
+
// src/git.ts
|
|
1335
|
+
var import_node_child_process = require("child_process");
|
|
1336
|
+
var import_node_path9 = __toESM(require("path"), 1);
|
|
1337
|
+
var import_node_fs7 = __toESM(require("fs"), 1);
|
|
1338
|
+
function runGit(root, args) {
|
|
1339
|
+
return new Promise((resolve) => {
|
|
1340
|
+
const child = (0, import_node_child_process.spawn)("git", ["--no-pager", ...args], {
|
|
1341
|
+
cwd: root,
|
|
1342
|
+
env: { ...process.env, LC_ALL: "C" },
|
|
1343
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
1344
|
+
});
|
|
1345
|
+
let out = "";
|
|
1346
|
+
let err = "";
|
|
1347
|
+
child.stdout.on("data", (d) => {
|
|
1348
|
+
out += d;
|
|
1349
|
+
});
|
|
1350
|
+
child.stderr.on("data", (d) => {
|
|
1351
|
+
err += d;
|
|
1352
|
+
});
|
|
1353
|
+
child.on("error", (e) => resolve({ ok: false, out: "", err: e.message }));
|
|
1354
|
+
child.on("close", (code) => resolve({ ok: code === 0, out, err }));
|
|
1355
|
+
});
|
|
1356
|
+
}
|
|
1357
|
+
function parseNames(out) {
|
|
1358
|
+
return out.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
1359
|
+
}
|
|
1360
|
+
function isFatal(err) {
|
|
1361
|
+
if (!err) return void 0;
|
|
1362
|
+
const e = err.trim();
|
|
1363
|
+
if (/not a git repository|fatal:/i.test(e)) return e;
|
|
1364
|
+
return void 0;
|
|
1365
|
+
}
|
|
1366
|
+
async function gitChangedFiles(root, opts) {
|
|
1367
|
+
const scope = opts.scope ?? "worktree";
|
|
1368
|
+
const filter = "--diff-filter=ACMRT";
|
|
1369
|
+
let relative = [];
|
|
1370
|
+
if (scope === "staged") {
|
|
1371
|
+
const r = await runGit(root, ["diff", "--cached", "--name-only", filter]);
|
|
1372
|
+
const fatal = isFatal(r.err);
|
|
1373
|
+
if (!r.ok) return { files: [], ...fatal ? { error: fatal } : {} };
|
|
1374
|
+
relative = parseNames(r.out);
|
|
1375
|
+
} else if (scope === "branch") {
|
|
1376
|
+
const base = opts.base ?? "HEAD~1";
|
|
1377
|
+
const head = opts.head ?? "HEAD";
|
|
1378
|
+
const r = await runGit(root, ["diff", "--name-only", `${base}...${head}`, filter]);
|
|
1379
|
+
const fatal = isFatal(r.err);
|
|
1380
|
+
if (!r.ok) return { files: [], ...fatal ? { error: fatal } : {} };
|
|
1381
|
+
relative = parseNames(r.out);
|
|
1382
|
+
} else {
|
|
1383
|
+
const [unstaged, staged] = await Promise.all([
|
|
1384
|
+
runGit(root, ["diff", "--name-only", filter]),
|
|
1385
|
+
runGit(root, ["diff", "--cached", "--name-only", filter])
|
|
1386
|
+
]);
|
|
1387
|
+
const fatal = isFatal(unstaged.err) ?? isFatal(staged.err);
|
|
1388
|
+
if (!unstaged.ok && !staged.ok) {
|
|
1389
|
+
return { files: [], ...fatal ? { error: fatal } : {} };
|
|
1390
|
+
}
|
|
1391
|
+
const merged = /* @__PURE__ */ new Set([...parseNames(unstaged.out), ...parseNames(staged.out)]);
|
|
1392
|
+
if (opts.includeUntracked) {
|
|
1393
|
+
const ut = await runGit(root, ["ls-files", "--others", "--exclude-standard"]);
|
|
1394
|
+
for (const f of parseNames(ut.out)) merged.add(f);
|
|
1395
|
+
}
|
|
1396
|
+
relative = [...merged];
|
|
1397
|
+
}
|
|
1398
|
+
const files = relative.map((rel2) => import_node_path9.default.resolve(root, rel2)).filter((abs) => {
|
|
1399
|
+
try {
|
|
1400
|
+
return import_node_fs7.default.statSync(abs).isFile();
|
|
1401
|
+
} catch {
|
|
1402
|
+
return false;
|
|
1403
|
+
}
|
|
1404
|
+
});
|
|
1405
|
+
return { files };
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
// src/mcp.ts
|
|
803
1409
|
var RULE_TARGET_PATH = {
|
|
804
1410
|
cursor: ".cursor/rules/token-shrink.mdc",
|
|
805
1411
|
claude: ".claude/rules/token-shrink.md",
|
|
806
1412
|
cline: ".clinerules/token-shrink.md"
|
|
807
1413
|
};
|
|
1414
|
+
var RULE_VERSION = 3;
|
|
1415
|
+
var RULE_VERSION_MARKER = `# token-shrink rule v${RULE_VERSION}`;
|
|
1416
|
+
var WORKFLOW_GUIDANCE = [
|
|
1417
|
+
"When a pruned skeleton is not enough to write or change code correctly, expand the exact",
|
|
1418
|
+
"definition with expand_symbol.",
|
|
1419
|
+
"",
|
|
1420
|
+
"Call search_symbol_signatures automatically for any identifier you reference that the",
|
|
1421
|
+
"context does not already define, so you always work from exact signatures.",
|
|
1422
|
+
"",
|
|
1423
|
+
"Before multi-file edits, reviews, or work on code with uncommitted or staged changes, call",
|
|
1424
|
+
"git_diff_context and use its impact payload (changed files and their callers) as context.",
|
|
1425
|
+
"",
|
|
1426
|
+
"Pass activeFiles to get_compressed_code_context when a task spans several files, and",
|
|
1427
|
+
"maxTokens whenever the payload must fit a token budget."
|
|
1428
|
+
].join("\n");
|
|
1429
|
+
var LIGHT_GUIDANCE = [
|
|
1430
|
+
"Use expand_symbol when a pruned body is not enough, git_diff_context for changed or",
|
|
1431
|
+
"multi-file work, and search_symbol_signatures to locate definitions repo-wide."
|
|
1432
|
+
].join("\n");
|
|
808
1433
|
var AUTO_RULE_SENTINEL = "# auto-generated by token-shrink";
|
|
809
1434
|
var CURSOR_RULE_PATH = RULE_TARGET_PATH.cursor;
|
|
810
1435
|
var AUTO_RULE_PATH = CURSOR_RULE_PATH;
|
|
@@ -825,6 +1450,9 @@ Before working on a file in this repo, call the \`get_compressed_code_context\`
|
|
|
825
1450
|
that file's path, and use the returned Ring 0 + Ring 1 payload as the context for that file
|
|
826
1451
|
and its direct imports.
|
|
827
1452
|
|
|
1453
|
+
${WORKFLOW_GUIDANCE}
|
|
1454
|
+
|
|
1455
|
+
${RULE_VERSION_MARKER}
|
|
828
1456
|
${AUTO_RULE_SENTINEL}
|
|
829
1457
|
`
|
|
830
1458
|
},
|
|
@@ -839,6 +1467,9 @@ Before working on a file in this repo, call the \`get_compressed_code_context\`
|
|
|
839
1467
|
that file's path, and use the returned Ring 0 + Ring 1 payload as the context for that file
|
|
840
1468
|
and its direct imports.
|
|
841
1469
|
|
|
1470
|
+
${WORKFLOW_GUIDANCE}
|
|
1471
|
+
|
|
1472
|
+
${RULE_VERSION_MARKER}
|
|
842
1473
|
${CLAUDE_RULE_SENTINEL}
|
|
843
1474
|
`
|
|
844
1475
|
},
|
|
@@ -853,23 +1484,32 @@ Before working on a file in this repo, call the \`get_compressed_code_context\`
|
|
|
853
1484
|
that file's path, and use the returned Ring 0 + Ring 1 payload as the context for that file
|
|
854
1485
|
and its direct imports.
|
|
855
1486
|
|
|
1487
|
+
${WORKFLOW_GUIDANCE}
|
|
1488
|
+
|
|
1489
|
+
${RULE_VERSION_MARKER}
|
|
856
1490
|
${CLINE_RULE_SENTINEL}
|
|
857
1491
|
`
|
|
858
1492
|
}
|
|
859
1493
|
};
|
|
860
1494
|
function createAutoRule(root, target) {
|
|
861
1495
|
const spec = ruleTargets[target];
|
|
862
|
-
const rulePath =
|
|
1496
|
+
const rulePath = import_node_path10.default.join(root, spec.relPath);
|
|
1497
|
+
const autoWorkflow = loadConfig(root).autoWorkflow !== false;
|
|
1498
|
+
const body = autoWorkflow ? spec.body : spec.body.replace(WORKFLOW_GUIDANCE, LIGHT_GUIDANCE);
|
|
863
1499
|
try {
|
|
864
|
-
if (
|
|
865
|
-
const existing =
|
|
1500
|
+
if (import_node_fs8.default.existsSync(rulePath)) {
|
|
1501
|
+
const existing = import_node_fs8.default.readFileSync(rulePath, "utf8");
|
|
866
1502
|
if (existing.includes(spec.sentinel)) {
|
|
867
|
-
|
|
1503
|
+
if (existing.includes(RULE_VERSION_MARKER)) {
|
|
1504
|
+
return { created: false, skipped: "exists", filePath: rulePath };
|
|
1505
|
+
}
|
|
1506
|
+
import_node_fs8.default.writeFileSync(rulePath, body, "utf8");
|
|
1507
|
+
return { created: true, skipped: "none", filePath: rulePath };
|
|
868
1508
|
}
|
|
869
1509
|
return { created: false, skipped: "user", filePath: rulePath };
|
|
870
1510
|
}
|
|
871
|
-
|
|
872
|
-
|
|
1511
|
+
import_node_fs8.default.mkdirSync(import_node_path10.default.dirname(rulePath), { recursive: true });
|
|
1512
|
+
import_node_fs8.default.writeFileSync(rulePath, body, "utf8");
|
|
873
1513
|
return { created: true, skipped: "none", filePath: rulePath };
|
|
874
1514
|
} catch (err) {
|
|
875
1515
|
process.stderr.write(
|
|
@@ -888,27 +1528,154 @@ function resolveTargets(ruleTarget) {
|
|
|
888
1528
|
if (list.includes("all")) return ["cursor", "claude", "cline"];
|
|
889
1529
|
return list;
|
|
890
1530
|
}
|
|
1531
|
+
function isWithin(parent, child) {
|
|
1532
|
+
const rel2 = import_node_path10.default.relative(import_node_path10.default.resolve(parent), import_node_path10.default.resolve(child));
|
|
1533
|
+
return rel2 === "" || !rel2.startsWith("..") && !import_node_path10.default.isAbsolute(rel2);
|
|
1534
|
+
}
|
|
1535
|
+
function resolveActiveFiles(single, many) {
|
|
1536
|
+
if (many && many.length > 0) {
|
|
1537
|
+
if (single) {
|
|
1538
|
+
throw new Error("Pass either `activeFilePath` or `activeFiles`, not both.");
|
|
1539
|
+
}
|
|
1540
|
+
return many;
|
|
1541
|
+
}
|
|
1542
|
+
if (single) return [single];
|
|
1543
|
+
throw new Error("`activeFilePath` or `activeFiles` is required");
|
|
1544
|
+
}
|
|
891
1545
|
async function startMcpServer(opts = {}) {
|
|
892
|
-
const root = import_node_path7.default.resolve(opts.root ?? process.env.ROOT ?? process.cwd());
|
|
893
1546
|
const log = (msg) => {
|
|
894
1547
|
if (!opts.silent) process.stderr.write(`[token-shrink] ${msg}
|
|
895
1548
|
`);
|
|
896
1549
|
};
|
|
897
|
-
const
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
1550
|
+
const textReply = (text) => ({
|
|
1551
|
+
content: [{ type: "text", text }]
|
|
1552
|
+
});
|
|
1553
|
+
const explicitRoot = opts.root?.trim() || process.env.ROOT?.trim() || "";
|
|
1554
|
+
const autoMode = explicitRoot === "";
|
|
1555
|
+
let root = "";
|
|
1556
|
+
let watcher = null;
|
|
1557
|
+
let searchIndex = null;
|
|
1558
|
+
let lifecycle = Promise.resolve();
|
|
1559
|
+
let config = { ...EMPTY_CONFIG };
|
|
1560
|
+
let configFsw = null;
|
|
1561
|
+
let configReloadTimer = null;
|
|
1562
|
+
const stopConfigWatch = () => {
|
|
1563
|
+
if (configReloadTimer) {
|
|
1564
|
+
clearTimeout(configReloadTimer);
|
|
1565
|
+
configReloadTimer = null;
|
|
1566
|
+
}
|
|
1567
|
+
if (configFsw) {
|
|
1568
|
+
configFsw.close();
|
|
1569
|
+
configFsw = null;
|
|
1570
|
+
}
|
|
1571
|
+
};
|
|
1572
|
+
const reloadForRoot = async (r) => {
|
|
1573
|
+
const next = loadConfig(r, (m) => log(m));
|
|
1574
|
+
const changed = JSON.stringify(next) !== JSON.stringify(config);
|
|
1575
|
+
config = next;
|
|
1576
|
+
if (!changed || !watcher || root !== r) return;
|
|
1577
|
+
log(".tokenshrinkrc.json changed \u2014 re-indexing with the new rules.");
|
|
1578
|
+
stopConfigWatch();
|
|
1579
|
+
const old = watcher;
|
|
1580
|
+
watcher = null;
|
|
1581
|
+
root = "";
|
|
1582
|
+
await old.close().catch(() => {
|
|
1583
|
+
});
|
|
1584
|
+
await attachWatcher(r, false);
|
|
1585
|
+
};
|
|
1586
|
+
const startConfigWatch = (r) => {
|
|
1587
|
+
stopConfigWatch();
|
|
1588
|
+
const cfgPath = configPathFor(r);
|
|
1589
|
+
if (!import_node_fs8.default.existsSync(cfgPath)) return;
|
|
1590
|
+
try {
|
|
1591
|
+
configFsw = import_node_fs8.default.watch(cfgPath, () => {
|
|
1592
|
+
if (configReloadTimer) clearTimeout(configReloadTimer);
|
|
1593
|
+
configReloadTimer = setTimeout(() => {
|
|
1594
|
+
void enqueue(() => reloadForRoot(r));
|
|
1595
|
+
}, 200);
|
|
1596
|
+
});
|
|
1597
|
+
} catch {
|
|
1598
|
+
}
|
|
1599
|
+
};
|
|
1600
|
+
const writeRules = (r) => {
|
|
1601
|
+
if (opts.createRule === false) return;
|
|
902
1602
|
for (const target of resolveTargets(opts.ruleTarget)) {
|
|
903
|
-
const res = createAutoRule(
|
|
1603
|
+
const res = createAutoRule(r, target);
|
|
904
1604
|
if (res.created) {
|
|
905
1605
|
log(`Wrote ${target} rule to ${res.filePath}`);
|
|
906
1606
|
} else if (res.skipped === "user") {
|
|
907
1607
|
log(`${target} rule exists (user-authored); leaving it untouched.`);
|
|
908
1608
|
}
|
|
909
1609
|
}
|
|
1610
|
+
};
|
|
1611
|
+
const attachWatcher = (r, waitForIndex) => {
|
|
1612
|
+
stopConfigWatch();
|
|
1613
|
+
config = loadConfig(r, (m) => log(m));
|
|
1614
|
+
const search = new SymbolSearch();
|
|
1615
|
+
const w = createWatcher({
|
|
1616
|
+
root: r,
|
|
1617
|
+
ignored: opts.ignored,
|
|
1618
|
+
config,
|
|
1619
|
+
onIndexed: (abs, entry) => {
|
|
1620
|
+
if (entry.symbols && entry.symbols.length > 0) search.setFile(abs, entry.symbols);
|
|
1621
|
+
else search.removeFile(abs);
|
|
1622
|
+
},
|
|
1623
|
+
onRemoved: (abs) => search.removeFile(abs)
|
|
1624
|
+
});
|
|
1625
|
+
searchIndex = search;
|
|
1626
|
+
root = r;
|
|
1627
|
+
watcher = w;
|
|
1628
|
+
writeRules(r);
|
|
1629
|
+
log(`Indexing ${r} in the background\u2026`);
|
|
1630
|
+
const indexed = w.indexAll().then((n) => log(`Indexed ${n} files.`)).catch((err) => log(`Indexing ${r} failed: ${err?.message ?? err}`));
|
|
1631
|
+
startConfigWatch(r);
|
|
1632
|
+
return waitForIndex ? indexed : Promise.resolve();
|
|
1633
|
+
};
|
|
1634
|
+
const enqueue = (fn) => {
|
|
1635
|
+
const run = lifecycle.then(fn);
|
|
1636
|
+
lifecycle = run.then(
|
|
1637
|
+
() => {
|
|
1638
|
+
},
|
|
1639
|
+
() => {
|
|
1640
|
+
}
|
|
1641
|
+
);
|
|
1642
|
+
return run;
|
|
1643
|
+
};
|
|
1644
|
+
if (explicitRoot) {
|
|
1645
|
+
void enqueue(() => attachWatcher(import_node_path10.default.resolve(explicitRoot), false));
|
|
1646
|
+
} else {
|
|
1647
|
+
const fromCwd = detectProjectRoot(process.cwd());
|
|
1648
|
+
if (fromCwd) {
|
|
1649
|
+
log(`Auto-detected project root ${fromCwd} (from cwd). Pass --root to pin it.`);
|
|
1650
|
+
void enqueue(() => attachWatcher(fromCwd, false));
|
|
1651
|
+
} else {
|
|
1652
|
+
log(
|
|
1653
|
+
"No --root and no project markers around the current directory \u2014 will auto-detect the project from the first tool call."
|
|
1654
|
+
);
|
|
1655
|
+
}
|
|
910
1656
|
}
|
|
911
|
-
|
|
1657
|
+
const ensureReadyFor = (activeFilePath) => enqueue(async () => {
|
|
1658
|
+
const abs = import_node_path10.default.resolve(activeFilePath);
|
|
1659
|
+
if (watcher && root) {
|
|
1660
|
+
if (!autoMode || isWithin(root, abs)) return;
|
|
1661
|
+
const next = detectProjectRoot(abs);
|
|
1662
|
+
if (!next || next === root) return;
|
|
1663
|
+
log(`Active file is in a different project (${next}); re-indexing (was ${root}).`);
|
|
1664
|
+
await watcher.close().catch(() => {
|
|
1665
|
+
});
|
|
1666
|
+
watcher = null;
|
|
1667
|
+
root = "";
|
|
1668
|
+
}
|
|
1669
|
+
if (root) return;
|
|
1670
|
+
const detected = detectProjectRoot(abs);
|
|
1671
|
+
const target = detected ?? process.cwd();
|
|
1672
|
+
if (detected) {
|
|
1673
|
+
log(`Auto-detected project root ${target} from ${abs}.`);
|
|
1674
|
+
} else {
|
|
1675
|
+
log(`No project markers around ${abs}; falling back to ${target}.`);
|
|
1676
|
+
}
|
|
1677
|
+
await attachWatcher(target, true);
|
|
1678
|
+
});
|
|
912
1679
|
const server = new import_mcp.McpServer(
|
|
913
1680
|
{ name: "token-shrink", version: "2.0.0" },
|
|
914
1681
|
{ capabilities: { tools: {} } }
|
|
@@ -917,18 +1684,28 @@ async function startMcpServer(opts = {}) {
|
|
|
917
1684
|
"get_compressed_code_context",
|
|
918
1685
|
{
|
|
919
1686
|
title: "Get Compressed Code Context",
|
|
920
|
-
description: "Returns a compressed, framework-aware AST context payload for
|
|
1687
|
+
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.",
|
|
921
1688
|
inputSchema: {
|
|
922
|
-
activeFilePath: import_zod.z.string().describe("Path to the file the agent is working on"),
|
|
1689
|
+
activeFilePath: import_zod.z.string().optional().describe("Path to the file the agent is working on (or use `activeFiles`)"),
|
|
1690
|
+
activeFiles: import_zod.z.array(import_zod.z.string()).optional().describe("Multiple files to keep as Ring 0 (full text); mutually exclusive with `activeFilePath`"),
|
|
923
1691
|
maxSkeletons: import_zod.z.number().int().min(1).max(200).optional().describe("Cap on number of dependency skeletons to include"),
|
|
1692
|
+
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"),
|
|
924
1693
|
includeStats: import_zod.z.boolean().optional().describe("Append approximate token-count stats")
|
|
925
1694
|
}
|
|
926
1695
|
},
|
|
927
|
-
async ({ activeFilePath, maxSkeletons, includeStats }) => {
|
|
928
|
-
const
|
|
1696
|
+
async ({ activeFilePath, activeFiles, maxSkeletons, maxTokens, includeStats }) => {
|
|
1697
|
+
const paths = resolveActiveFiles(activeFilePath, activeFiles);
|
|
1698
|
+
for (const p of paths) await ensureReadyFor(p);
|
|
1699
|
+
if (!watcher) {
|
|
1700
|
+
throw new Error("token-shrink watcher failed to start");
|
|
1701
|
+
}
|
|
1702
|
+
const result = assembleMany(paths, watcher.cache.entries, {
|
|
929
1703
|
maxSkeletons,
|
|
1704
|
+
maxTokens,
|
|
930
1705
|
includeStats
|
|
931
1706
|
});
|
|
1707
|
+
const ts = result.tokenStats;
|
|
1708
|
+
const budgetNote = ts.budget !== void 0 ? ` budget=${ts.budget} trimmed=${ts.trimmed}` : "";
|
|
932
1709
|
return {
|
|
933
1710
|
content: [
|
|
934
1711
|
{
|
|
@@ -937,19 +1714,274 @@ async function startMcpServer(opts = {}) {
|
|
|
937
1714
|
},
|
|
938
1715
|
{
|
|
939
1716
|
type: "text",
|
|
940
|
-
text: `[stats]
|
|
1717
|
+
text: `[stats] files=${result.activeFilePaths.length} dependencies=${result.included.length} unresolved=${result.unresolved.length} tokens=${ts.totalTokens}${budgetNote}`
|
|
941
1718
|
}
|
|
942
1719
|
]
|
|
943
1720
|
};
|
|
944
1721
|
}
|
|
945
1722
|
);
|
|
1723
|
+
server.registerTool(
|
|
1724
|
+
"expand_symbol",
|
|
1725
|
+
{
|
|
1726
|
+
title: "Expand Symbol",
|
|
1727
|
+
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.",
|
|
1728
|
+
inputSchema: {
|
|
1729
|
+
filePath: import_zod.z.string().describe("Path to the file containing the symbol"),
|
|
1730
|
+
symbolName: import_zod.z.string().describe("Name of the definition to expand (exact name preferred; falls back to case-insensitive/substring)"),
|
|
1731
|
+
maxMatches: import_zod.z.number().int().min(1).max(20).optional().describe("Maximum matching definitions to return (default 5)")
|
|
1732
|
+
}
|
|
1733
|
+
},
|
|
1734
|
+
async ({ filePath, symbolName, maxMatches }) => {
|
|
1735
|
+
await ensureReadyFor(filePath);
|
|
1736
|
+
const abs = import_node_path10.default.resolve(filePath);
|
|
1737
|
+
let source;
|
|
1738
|
+
try {
|
|
1739
|
+
source = import_node_fs8.default.readFileSync(abs, "utf8");
|
|
1740
|
+
} catch {
|
|
1741
|
+
return textReply(`File not found or unreadable: ${filePath}`);
|
|
1742
|
+
}
|
|
1743
|
+
const spec = languageForFile(abs);
|
|
1744
|
+
if (!spec) {
|
|
1745
|
+
return textReply(`No token-shrink grammar for file type: ${filePath}`);
|
|
1746
|
+
}
|
|
1747
|
+
const { symbols } = await analyze(abs, source, { skipPrune: true });
|
|
1748
|
+
if (symbols.length === 0) {
|
|
1749
|
+
return textReply(
|
|
1750
|
+
`No parseable definitions found in ${filePath} \u2014 the grammar may be unavailable (first run offline) or the language exposes no name-carrying definitions yet.`
|
|
1751
|
+
);
|
|
1752
|
+
}
|
|
1753
|
+
const matches = matchSymbols(symbols, symbolName).slice(0, maxMatches ?? 5);
|
|
1754
|
+
if (matches.length === 0) {
|
|
1755
|
+
const names = [...new Set(symbols.map((s) => s.name))].slice(0, 12).join(", ");
|
|
1756
|
+
return textReply(
|
|
1757
|
+
`No symbol named "${symbolName}" in ${filePath}.` + (names ? ` Other definitions there: ${names}.` : "")
|
|
1758
|
+
);
|
|
1759
|
+
}
|
|
1760
|
+
const fence = import_node_path10.default.extname(abs).replace(/^\./, "") || "text";
|
|
1761
|
+
const parts = matches.map((m) => {
|
|
1762
|
+
const body = source.slice(m.start, m.end).trim();
|
|
1763
|
+
return `### ${m.name} (${m.kind}) \u2014 ${abs}:${m.line}
|
|
1764
|
+
|
|
1765
|
+
\`\`\`` + fence + "\n" + body + "\n```\n";
|
|
1766
|
+
});
|
|
1767
|
+
const disambig = matches.length > 1 ? `
|
|
1768
|
+
_Multiple definitions matched (${matches.length}); each is shown above._` : "";
|
|
1769
|
+
return textReply(parts.join("\n") + disambig);
|
|
1770
|
+
}
|
|
1771
|
+
);
|
|
1772
|
+
server.registerTool(
|
|
1773
|
+
"git_diff_context",
|
|
1774
|
+
{
|
|
1775
|
+
title: "Git Diff Context",
|
|
1776
|
+
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.',
|
|
1777
|
+
inputSchema: {
|
|
1778
|
+
scope: import_zod.z.enum(["worktree", "staged", "branch"]).optional().describe("Diff scope: 'worktree' (default, staged+unstaged), 'staged', or 'branch' (base...head)"),
|
|
1779
|
+
base: import_zod.z.string().optional().describe("Base ref for scope=branch (default HEAD~1)"),
|
|
1780
|
+
head: import_zod.z.string().optional().describe("Head ref for scope=branch (default HEAD)"),
|
|
1781
|
+
includeUntracked: import_zod.z.boolean().optional().describe("Include untracked files (worktree scope only)"),
|
|
1782
|
+
maxFiles: import_zod.z.number().int().min(1).max(200).optional().describe("Maximum changed files to include (default 30)"),
|
|
1783
|
+
maxImporters: import_zod.z.number().int().min(0).max(100).optional().describe("Maximum importer skeletons to append (default 20)"),
|
|
1784
|
+
maxSkeletons: import_zod.z.number().int().min(1).max(200).optional().describe("Cap on dependency skeletons per payload"),
|
|
1785
|
+
maxTokens: import_zod.z.number().int().positive().optional().describe("Hard token budget for the whole payload"),
|
|
1786
|
+
includeStats: import_zod.z.boolean().optional().describe("Append approximate token-count stats")
|
|
1787
|
+
}
|
|
1788
|
+
},
|
|
1789
|
+
async ({
|
|
1790
|
+
scope,
|
|
1791
|
+
base,
|
|
1792
|
+
head,
|
|
1793
|
+
includeUntracked,
|
|
1794
|
+
maxFiles,
|
|
1795
|
+
maxImporters,
|
|
1796
|
+
maxSkeletons,
|
|
1797
|
+
maxTokens,
|
|
1798
|
+
includeStats
|
|
1799
|
+
}) => {
|
|
1800
|
+
await ensureReadyFor(process.cwd());
|
|
1801
|
+
if (!watcher || !root) {
|
|
1802
|
+
throw new Error("token-shrink watcher failed to start");
|
|
1803
|
+
}
|
|
1804
|
+
const repoRoot = root;
|
|
1805
|
+
const changedResult = await gitChangedFiles(repoRoot, {
|
|
1806
|
+
scope: scope ?? "worktree",
|
|
1807
|
+
base,
|
|
1808
|
+
head,
|
|
1809
|
+
includeUntracked
|
|
1810
|
+
});
|
|
1811
|
+
if (changedResult.error) {
|
|
1812
|
+
return textReply(`git error: ${changedResult.error}`);
|
|
1813
|
+
}
|
|
1814
|
+
if (changedResult.files.length === 0) {
|
|
1815
|
+
return textReply("No changed files (clean worktree, or empty diff for the requested scope).");
|
|
1816
|
+
}
|
|
1817
|
+
const fileCap = maxFiles ?? 30;
|
|
1818
|
+
const changed = changedResult.files.slice(0, fileCap);
|
|
1819
|
+
const filesTruncated = changedResult.files.length > changed.length;
|
|
1820
|
+
for (const abs of changed) {
|
|
1821
|
+
if (!watcher.cache.entries.has(abs)) {
|
|
1822
|
+
try {
|
|
1823
|
+
await watcher.index(abs);
|
|
1824
|
+
} catch {
|
|
1825
|
+
}
|
|
1826
|
+
}
|
|
1827
|
+
}
|
|
1828
|
+
const assembled = assembleMany(changed, watcher.cache.entries, {
|
|
1829
|
+
maxSkeletons,
|
|
1830
|
+
maxTokens,
|
|
1831
|
+
includeStats
|
|
1832
|
+
});
|
|
1833
|
+
const changedSet = new Set(changed);
|
|
1834
|
+
const importerOf = /* @__PURE__ */ new Map();
|
|
1835
|
+
for (const [fileAbs, entry] of watcher.cache.entries) {
|
|
1836
|
+
for (const imp of entry.imports) {
|
|
1837
|
+
if (!changedSet.has(imp)) continue;
|
|
1838
|
+
const list = importerOf.get(imp) ?? [];
|
|
1839
|
+
list.push(fileAbs);
|
|
1840
|
+
importerOf.set(imp, list);
|
|
1841
|
+
}
|
|
1842
|
+
}
|
|
1843
|
+
const importerList = [...new Set([...importerOf.values()].flat())].filter(
|
|
1844
|
+
(f) => !changedSet.has(f)
|
|
1845
|
+
);
|
|
1846
|
+
const importerCap = maxImporters ?? 20;
|
|
1847
|
+
const importers = importerList.slice(0, importerCap);
|
|
1848
|
+
const importersTruncated = importerList.length > importers.length;
|
|
1849
|
+
const relLabel = (abs) => {
|
|
1850
|
+
const rel2 = import_node_path10.default.relative(repoRoot, abs);
|
|
1851
|
+
return rel2 && !rel2.startsWith("..") ? rel2 : abs;
|
|
1852
|
+
};
|
|
1853
|
+
const fence = (abs) => import_node_path10.default.extname(abs).replace(/^\./, "") || "text";
|
|
1854
|
+
const parts = [];
|
|
1855
|
+
parts.push(
|
|
1856
|
+
`# Git Impact Context (${changed.length} changed file${changed.length === 1 ? "" : "s"})`,
|
|
1857
|
+
""
|
|
1858
|
+
);
|
|
1859
|
+
parts.push(`- changed files: ${changed.map(relLabel).join(", ")}`);
|
|
1860
|
+
if (importers.length > 0) {
|
|
1861
|
+
parts.push(`- importing files (callers): ${importers.map(relLabel).join(", ")}`);
|
|
1862
|
+
}
|
|
1863
|
+
parts.push("", "---", "");
|
|
1864
|
+
parts.push("## Changed files \u2014 full code", "");
|
|
1865
|
+
for (const abs of changed) {
|
|
1866
|
+
parts.push(`### \`${relLabel(abs)}\``, "");
|
|
1867
|
+
let source = "";
|
|
1868
|
+
try {
|
|
1869
|
+
source = import_node_fs8.default.readFileSync(abs, "utf8");
|
|
1870
|
+
} catch {
|
|
1871
|
+
}
|
|
1872
|
+
parts.push(`\`\`\`${fence(abs)}`, source.trim() || "(unreadable file)", "```", "");
|
|
1873
|
+
}
|
|
1874
|
+
parts.push(`## Ring 1 \u2014 Pruned dependencies (${assembled.included.length})`, "");
|
|
1875
|
+
parts.push("", "Implementation bodies removed; type signatures, interfaces and exports retained.", "");
|
|
1876
|
+
if (assembled.included.length === 0) {
|
|
1877
|
+
parts.push("_No local dependency skeletons available._", "");
|
|
1878
|
+
}
|
|
1879
|
+
for (const inc of assembled.included) {
|
|
1880
|
+
const entry = watcher.cache.entries.get(inc.filePath);
|
|
1881
|
+
const label = relLabel(inc.filePath);
|
|
1882
|
+
parts.push(`### \`${label}\``, "");
|
|
1883
|
+
if (entry) {
|
|
1884
|
+
parts.push(`\`\`\`${fence(inc.filePath)}`, entry.skeleton.trim(), "```", "");
|
|
1885
|
+
} else {
|
|
1886
|
+
parts.push("_Unindexed file._", "");
|
|
1887
|
+
}
|
|
1888
|
+
parts.push("");
|
|
1889
|
+
}
|
|
1890
|
+
if (assembled.tokenStats.budget !== void 0 && assembled.tokenStats.trimmed > 0) {
|
|
1891
|
+
parts.push(
|
|
1892
|
+
`_Note: token budget of ${assembled.tokenStats.budget} excluded ${assembled.tokenStats.trimmed} lower-priority dependencies._`,
|
|
1893
|
+
""
|
|
1894
|
+
);
|
|
1895
|
+
}
|
|
1896
|
+
if (importers.length > 0) {
|
|
1897
|
+
parts.push(`## Ring 2 \u2014 Files importing the diff (${importers.length})`, "");
|
|
1898
|
+
parts.push("", "Pruned skeletons of modules that call into the changed files.", "");
|
|
1899
|
+
for (const abs of importers) {
|
|
1900
|
+
const entry = watcher.cache.entries.get(abs);
|
|
1901
|
+
parts.push(`### \`${relLabel(abs)}\``, "");
|
|
1902
|
+
if (entry) {
|
|
1903
|
+
parts.push(`\`\`\`${fence(abs)}`, entry.skeleton.trim(), "```", "");
|
|
1904
|
+
} else {
|
|
1905
|
+
parts.push("_Unindexed file._", "");
|
|
1906
|
+
}
|
|
1907
|
+
parts.push("");
|
|
1908
|
+
}
|
|
1909
|
+
if (importersTruncated) {
|
|
1910
|
+
parts.push(
|
|
1911
|
+
`_\u2026and ${importerList.length - importers.length} more importing files (raise maxImporters)._`,
|
|
1912
|
+
""
|
|
1913
|
+
);
|
|
1914
|
+
}
|
|
1915
|
+
}
|
|
1916
|
+
if (filesTruncated) {
|
|
1917
|
+
parts.push(
|
|
1918
|
+
`_Note: capped to ${fileCap} changed files (${changedResult.files.length} total); raise maxFiles to include more._`,
|
|
1919
|
+
""
|
|
1920
|
+
);
|
|
1921
|
+
}
|
|
1922
|
+
if (assembled.unresolved.length > 0) {
|
|
1923
|
+
parts.push("## Unresolved imports", "");
|
|
1924
|
+
for (const u of assembled.unresolved) parts.push(`- \`${u}\``);
|
|
1925
|
+
parts.push("");
|
|
1926
|
+
}
|
|
1927
|
+
const ts = assembled.tokenStats;
|
|
1928
|
+
const budgetNote = ts.budget !== void 0 ? ` budget=${ts.budget} trimmed=${ts.trimmed}` : "";
|
|
1929
|
+
const stats = `[git-stats] files=${changed.length} ring1=${assembled.included.length} importers=${importers.length} unresolved=${assembled.unresolved.length} tokens=${ts.totalTokens}${budgetNote}`;
|
|
1930
|
+
return {
|
|
1931
|
+
content: [
|
|
1932
|
+
{ type: "text", text: parts.join("\n") },
|
|
1933
|
+
{ type: "text", text: stats }
|
|
1934
|
+
]
|
|
1935
|
+
};
|
|
1936
|
+
}
|
|
1937
|
+
);
|
|
1938
|
+
server.registerTool(
|
|
1939
|
+
"search_symbol_signatures",
|
|
1940
|
+
{
|
|
1941
|
+
title: "Search Symbol Signatures",
|
|
1942
|
+
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.",
|
|
1943
|
+
inputSchema: {
|
|
1944
|
+
query: import_zod.z.string().describe("Search text (matched against symbol names; falls back to signatures)"),
|
|
1945
|
+
maxResults: import_zod.z.number().int().min(1).max(100).optional().describe("Maximum results to return (default 10)"),
|
|
1946
|
+
kind: import_zod.z.enum(["function", "method", "arrow", "class", "interface", "enum", "type", "other"]).optional().describe("Only return definitions of this kind")
|
|
1947
|
+
}
|
|
1948
|
+
},
|
|
1949
|
+
async ({ query, maxResults, kind }) => {
|
|
1950
|
+
await ensureReadyFor(process.cwd());
|
|
1951
|
+
if (!searchIndex) {
|
|
1952
|
+
throw new Error("token-shrink symbol index failed to start");
|
|
1953
|
+
}
|
|
1954
|
+
if (searchIndex.size === 0 && watcher && watcher.cache.entries.size > 0) {
|
|
1955
|
+
searchIndex.loadCache(watcher.cache.entries);
|
|
1956
|
+
}
|
|
1957
|
+
const hits = searchIndex.search(query, {
|
|
1958
|
+
maxResults: maxResults ?? 10,
|
|
1959
|
+
kind
|
|
1960
|
+
});
|
|
1961
|
+
if (hits.length === 0) {
|
|
1962
|
+
return textReply(`No symbols match "${query}". Try a different name or kind.`);
|
|
1963
|
+
}
|
|
1964
|
+
const lines = hits.map((h) => `- \`${h.label}\``);
|
|
1965
|
+
return textReply(
|
|
1966
|
+
`${lines.join("\n")}
|
|
1967
|
+
|
|
1968
|
+
_Found ${hits.length} symbol${hits.length === 1 ? "" : "s"} matching "${query}"._`
|
|
1969
|
+
);
|
|
1970
|
+
}
|
|
1971
|
+
);
|
|
946
1972
|
const transport = new import_stdio.StdioServerTransport();
|
|
947
1973
|
await server.connect(transport);
|
|
948
1974
|
log("MCP server connected.");
|
|
949
1975
|
return server;
|
|
950
1976
|
}
|
|
951
|
-
var argv12 = process.argv[1] ?
|
|
952
|
-
|
|
1977
|
+
var argv12 = process.argv[1] ? import_node_path10.default.basename(process.argv[1]) : "";
|
|
1978
|
+
var argv1Real2 = "";
|
|
1979
|
+
try {
|
|
1980
|
+
argv1Real2 = process.argv[1] ? import_node_path10.default.basename(import_node_fs8.default.realpathSync(process.argv[1])) : "";
|
|
1981
|
+
} catch {
|
|
1982
|
+
}
|
|
1983
|
+
var invokedAsMcp = argv12 === "mcp.js" || argv12 === "mcp.mjs" || argv12 === "mcp.cjs" || argv12 === "mcp.ts" || argv1Real2 === "mcp.js" || argv1Real2 === "mcp.mjs" || argv1Real2 === "mcp.cjs" || argv1Real2 === "mcp.ts";
|
|
1984
|
+
if (invokedAsMcp) {
|
|
953
1985
|
const rootArg = (() => {
|
|
954
1986
|
const i = process.argv.indexOf("--root");
|
|
955
1987
|
if (i !== -1 && process.argv[i + 1]) return process.argv[i + 1];
|
|
@@ -982,20 +2014,37 @@ var version = "2.0.0";
|
|
|
982
2014
|
CLAUDE_RULE_SENTINEL,
|
|
983
2015
|
CLINE_RULE_PATH,
|
|
984
2016
|
CLINE_RULE_SENTINEL,
|
|
2017
|
+
CONFIG_FILE_NAME,
|
|
985
2018
|
CURSOR_RULE_PATH,
|
|
986
2019
|
DEFAULT_IGNORED,
|
|
2020
|
+
EMPTY_CONFIG,
|
|
2021
|
+
PROJECT_MANIFEST_FILES,
|
|
987
2022
|
RULE_TARGET_PATH,
|
|
2023
|
+
SymbolSearch,
|
|
2024
|
+
VCS_MARKER_DIRS,
|
|
988
2025
|
allExtensions,
|
|
2026
|
+
analyze,
|
|
989
2027
|
approximateTokens,
|
|
990
2028
|
assemble,
|
|
2029
|
+
assembleMany,
|
|
2030
|
+
collectSymbols,
|
|
2031
|
+
configPathFor,
|
|
991
2032
|
createAutoRule,
|
|
992
2033
|
createCursorRule,
|
|
993
2034
|
createWatcher,
|
|
2035
|
+
detectProjectRoot,
|
|
2036
|
+
ensureParserInit,
|
|
994
2037
|
extractImports,
|
|
995
2038
|
extractSpecifiers,
|
|
996
2039
|
getGrammar,
|
|
2040
|
+
globToRegExp,
|
|
997
2041
|
hashOf,
|
|
998
2042
|
languageForFile,
|
|
2043
|
+
loadConfig,
|
|
2044
|
+
loadLanguage,
|
|
2045
|
+
matchSymbols,
|
|
2046
|
+
matchesAny,
|
|
2047
|
+
matchesGlob,
|
|
999
2048
|
prune,
|
|
1000
2049
|
registry,
|
|
1001
2050
|
resolveImport,
|