@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/dist/cli.cjs CHANGED
@@ -188,103 +188,180 @@ __export(cli_exports, {
188
188
  module.exports = __toCommonJS(cli_exports);
189
189
  var import_fastify = __toESM(require("fastify"), 1);
190
190
  var import_picocolors = __toESM(require("picocolors"), 1);
191
- var import_node_path6 = __toESM(require("path"), 1);
191
+ var import_node_fs5 = __toESM(require("fs"), 1);
192
+ var import_node_path7 = __toESM(require("path"), 1);
192
193
 
193
194
  // src/server/assembler.ts
194
195
  var import_node_fs = __toESM(require("fs"), 1);
195
196
  var import_node_path = __toESM(require("path"), 1);
196
- function assemble(activeFilePath, cache, opts = {}) {
197
- const { includeStats = false, maxSkeletons = 50, pruneActiveFile = false } = opts;
198
- const abs = import_node_path.default.resolve(activeFilePath);
199
- const activeSource = import_node_fs.default.existsSync(abs) ? import_node_fs.default.readFileSync(abs, "utf8") : "";
200
- const activeEntry = cache.get(abs);
197
+ var MARKDOWN_OVERHEAD_TOKENS = 40;
198
+ function assembleMany(activeFiles, cache, opts = {}) {
199
+ const {
200
+ includeStats = false,
201
+ maxSkeletons = 50,
202
+ maxTokens,
203
+ pruneActiveFile = false
204
+ } = opts;
205
+ const absList = activeFiles.map((p) => import_node_path.default.resolve(p));
206
+ const sources = absList.map((abs) => {
207
+ try {
208
+ return import_node_fs.default.readFileSync(abs, "utf8");
209
+ } catch {
210
+ return "";
211
+ }
212
+ });
213
+ const activeSet = new Set(absList);
214
+ const ring0Texts = absList.map((abs, i) => {
215
+ const entry = cache.get(abs);
216
+ return pruneActiveFile && entry ? entry.skeleton : sources[i];
217
+ });
201
218
  const included = [];
202
219
  const unresolved = [];
203
220
  const seen = /* @__PURE__ */ new Set();
204
- if (activeEntry) {
205
- for (const imp of activeEntry.imports) {
206
- const entry = cache.get(imp);
207
- if (!entry) {
208
- unresolved.push(imp);
209
- continue;
221
+ const addUnresolved = (p) => {
222
+ if (!unresolved.includes(p)) unresolved.push(p);
223
+ };
224
+ const addIncluded = (p, allowUncached) => {
225
+ if (seen.has(p)) return;
226
+ const entry = cache.get(p);
227
+ if (!entry && !allowUncached) {
228
+ addUnresolved(p);
229
+ return;
230
+ }
231
+ seen.add(p);
232
+ included.push({ filePath: p, language: entry?.language ?? null });
233
+ };
234
+ for (let i = 0; i < absList.length; i++) {
235
+ const abs = absList[i];
236
+ const entry = cache.get(abs);
237
+ if (entry) {
238
+ for (const imp of entry.imports) {
239
+ if (activeSet.has(imp)) continue;
240
+ addIncluded(imp, false);
241
+ }
242
+ } else if (sources[i]) {
243
+ for (const spec of extractSpecifiers(sources[i])) {
244
+ const resolved = resolveLocal(abs, spec);
245
+ if (!resolved) {
246
+ addUnresolved(spec);
247
+ continue;
248
+ }
249
+ if (activeSet.has(resolved)) continue;
250
+ addIncluded(resolved, true);
210
251
  }
211
- if (seen.has(imp)) continue;
212
- seen.add(imp);
213
- included.push({ filePath: imp, language: entry.language });
214
- if (included.length >= maxSkeletons) break;
215
252
  }
216
253
  }
217
- if (!activeEntry && activeSource) {
218
- const specifiers = extractSpecifiers(activeSource);
219
- for (const spec of specifiers) {
220
- const resolved = resolveLocal(abs, spec);
221
- if (!resolved) {
222
- unresolved.push(spec);
223
- continue;
224
- }
225
- if (seen.has(resolved)) continue;
226
- seen.add(resolved);
227
- const cached = cache.get(resolved);
228
- included.push({ filePath: resolved, language: cached?.language ?? null });
229
- if (included.length >= maxSkeletons) break;
254
+ const ordered = maxTokens === void 0 ? included : rankByRelevance(included, cache);
255
+ const budget = maxTokens === void 0 ? void 0 : Math.max(0, maxTokens - MARKDOWN_OVERHEAD_TOKENS);
256
+ const selected = [];
257
+ let trimmed = 0;
258
+ let dependencyTokens = 0;
259
+ for (const cand of ordered) {
260
+ if (selected.length >= maxSkeletons) break;
261
+ const entry = cache.get(cand.filePath);
262
+ const cost = entry ? approximateTokens(entry.skeleton) : 8;
263
+ if (budget !== void 0 && dependencyTokens + cost > budget) {
264
+ trimmed++;
265
+ continue;
230
266
  }
267
+ selected.push(cand);
268
+ dependencyTokens += cost;
231
269
  }
270
+ const activeTokens = ring0Texts.reduce(
271
+ (acc, t) => acc + approximateTokens(t),
272
+ 0
273
+ );
232
274
  const lines = [];
233
275
  lines.push("# Compressed Code Context", "");
234
- lines.push(`Active file: \`${rel(abs)}\``, "");
235
- lines.push("## Ring 0 \u2014 Active file (full text)", "");
236
- const ring0 = pruneActiveFile && activeEntry ? activeEntry.skeleton : activeSource;
237
- lines.push(`\`\`\`${ext(activeFilePath)}`);
238
- lines.push(ring0.trim() || "(empty or unreadable file)");
239
- lines.push("```", "");
276
+ if (absList.length === 1) {
277
+ lines.push(`Active file: \`${rel(absList[0])}\``, "");
278
+ } else {
279
+ lines.push("Active files:", "");
280
+ for (const p of absList) lines.push(`- \`${rel(p)}\``);
281
+ lines.push("");
282
+ }
283
+ lines.push(
284
+ absList.length === 1 ? "## Ring 0 \u2014 Active file (full text)" : "## Ring 0 \u2014 Active files (full text)",
285
+ ""
286
+ );
287
+ for (let i = 0; i < absList.length; i++) {
288
+ if (absList.length > 1) lines.push(`### \`${rel(absList[i])}\``, "");
289
+ const body = ring0Texts[i].trim() || "(empty or unreadable file)";
290
+ lines.push(`\`\`\`${ext(absList[i])}`, body, "```", "");
291
+ }
240
292
  lines.push(
241
- `## Ring 1 \u2014 Pruned dependencies (${included.length})`,
293
+ `## Ring 1 \u2014 Pruned dependencies (${selected.length})`,
242
294
  "",
243
295
  "Implementation bodies removed; type signatures, interfaces and exports retained.",
244
296
  ""
245
297
  );
246
- if (included.length === 0) {
298
+ if (selected.length === 0) {
247
299
  lines.push("_No local dependency skeletons available._", "");
248
300
  }
249
- for (const inc of included) {
301
+ for (const inc of selected) {
250
302
  const entry = cache.get(inc.filePath);
251
303
  const label = rel(inc.filePath);
252
304
  lines.push(`### \`${label}\``, "");
253
305
  if (entry) {
254
- lines.push(`\`\`\`${ext(inc.filePath)}`);
255
- lines.push(entry.skeleton.trim());
256
- lines.push("```", "");
306
+ lines.push(`\`\`\`${ext(inc.filePath)}`, entry.skeleton.trim(), "```", "");
257
307
  } else {
258
308
  lines.push("_Unindexed file._", "");
259
309
  }
260
310
  }
311
+ if (maxTokens !== void 0 && trimmed > 0) {
312
+ lines.push(
313
+ `_Note: token budget of ${maxTokens} excluded ${trimmed} lower-priority dependenc${trimmed === 1 ? "y" : "ies"}._`,
314
+ ""
315
+ );
316
+ }
261
317
  if (unresolved.length > 0) {
262
318
  lines.push("## Unresolved imports", "");
263
319
  for (const u of unresolved) lines.push(`- \`${u}\``);
264
320
  lines.push("");
265
321
  }
266
322
  if (includeStats) {
267
- const depTokens = included.reduce(
268
- (acc, inc) => {
269
- const e = cache.get(inc.filePath);
270
- return e ? acc + approximateTokens(e.skeleton) : acc;
271
- },
272
- 0
273
- );
274
- lines.push("---", "");
323
+ const budgetNote = maxTokens !== void 0 ? ` \xB7 budget ${maxTokens} (${trimmed} deps trimmed)` : "";
275
324
  lines.push(
276
- `_Token estimate \u2014 active: ${approximateTokens(activeSource)} \xB7 pruned deps: ${depTokens}._`,
325
+ "---",
326
+ "",
327
+ `_Token estimate \u2014 active: ${activeTokens} \xB7 pruned deps: ${dependencyTokens}${budgetNote}._`,
277
328
  ""
278
329
  );
279
330
  }
280
331
  return {
281
332
  markdown: lines.join("\n"),
282
- activeFilePath: abs,
283
- activeSource,
284
- included,
285
- unresolved
333
+ activeFilePath: absList[0] ?? "",
334
+ activeFilePaths: absList,
335
+ activeSource: sources.join("\n"),
336
+ included: selected,
337
+ unresolved,
338
+ tokenStats: {
339
+ activeTokens,
340
+ dependencyTokens,
341
+ totalTokens: activeTokens + dependencyTokens,
342
+ ...maxTokens !== void 0 ? { budget: maxTokens } : {},
343
+ trimmed
344
+ }
286
345
  };
287
346
  }
347
+ function rankByRelevance(candidates, cache) {
348
+ const fanIn = /* @__PURE__ */ new Map();
349
+ for (const entry of cache.values()) {
350
+ for (const imp of entry.imports) fanIn.set(imp, (fanIn.get(imp) ?? 0) + 1);
351
+ }
352
+ return candidates.map((cand, idx) => ({ cand, idx })).sort((a, b) => {
353
+ const fa = fanIn.get(a.cand.filePath) ?? 0;
354
+ const fb = fanIn.get(b.cand.filePath) ?? 0;
355
+ if (fa !== fb) return fb - fa;
356
+ const da = pathDepth(a.cand.filePath);
357
+ const db = pathDepth(b.cand.filePath);
358
+ if (da !== db) return da - db;
359
+ return a.idx - b.idx;
360
+ }).map((x) => x.cand);
361
+ }
362
+ function pathDepth(p) {
363
+ return p.split(/[/\\]+/).filter(Boolean).length;
364
+ }
288
365
  function extractSpecifiers(source) {
289
366
  const found = [];
290
367
  const re = /(?:from\s+['"`]|import\s+['"`]|require\s*\(\s*['"`])([^'"`]+)['"`]/g;
@@ -341,12 +418,12 @@ function approximateTokens(text) {
341
418
  }
342
419
 
343
420
  // src/watcher/sync.ts
344
- var import_node_fs3 = __toESM(require("fs"), 1);
345
- var import_node_path5 = __toESM(require("path"), 1);
421
+ var import_node_fs4 = __toESM(require("fs"), 1);
422
+ var import_node_path6 = __toESM(require("path"), 1);
346
423
  var import_node_crypto = require("crypto");
347
424
  var import_chokidar = __toESM(require("chokidar"), 1);
348
425
 
349
- // src/parser/pruner.ts
426
+ // src/parser/analyze.ts
350
427
  var import_web_tree_sitter = __toESM(require("web-tree-sitter"), 1);
351
428
  var import_node_module = require("module");
352
429
  var import_node_path4 = __toESM(require("path"), 1);
@@ -431,7 +508,85 @@ async function warmGrammars(specs = []) {
431
508
  return results.filter((r) => r.status === "fulfilled").length;
432
509
  }
433
510
 
434
- // src/parser/pruner.ts
511
+ // src/parser/symbols.ts
512
+ var KIND_BY_TYPE = {
513
+ // TypeScript / JavaScript / TSX / JSX
514
+ function_declaration: "function",
515
+ generator_function_declaration: "function",
516
+ function_expression: "function",
517
+ function_signature_item: "function",
518
+ method_definition: "method",
519
+ method_signature: "method",
520
+ class_declaration: "class",
521
+ abstract_class_declaration: "class",
522
+ interface_declaration: "interface",
523
+ enum_declaration: "enum",
524
+ type_alias_declaration: "type",
525
+ // Python
526
+ function_definition: "function",
527
+ class_definition: "class",
528
+ // Go
529
+ method_declaration: "method",
530
+ type_spec: "type",
531
+ // Rust
532
+ function_item: "function",
533
+ struct_item: "class",
534
+ enum_item: "enum",
535
+ trait_item: "interface",
536
+ type_item: "type",
537
+ // Java / Kotlin / PHP / Dart / Swift (best-effort generic names)
538
+ constructor_declaration: "method",
539
+ module_declaration: "class",
540
+ protocol_declaration: "interface"
541
+ };
542
+ var SIGNATURE_MAX = 200;
543
+ function collectSymbols(root, source) {
544
+ const out = [];
545
+ const walk2 = (node) => {
546
+ const kind = KIND_BY_TYPE[node.type];
547
+ if (kind) {
548
+ const nameNode = node.childForFieldName("name");
549
+ if (nameNode && nameNode.text.trim()) {
550
+ out.push(symbolFrom(nameNode.text.trim(), kind, node.startIndex, node.endIndex, source));
551
+ }
552
+ } else if (node.type === "variable_declarator") {
553
+ const value = node.childForFieldName("value");
554
+ const valueType = value?.type;
555
+ if (valueType === "arrow_function" || valueType === "function_expression") {
556
+ const nameNode = node.childForFieldName("name");
557
+ if (nameNode && nameNode.text.trim()) {
558
+ out.push(
559
+ symbolFrom(nameNode.text.trim(), "arrow", node.startIndex, node.endIndex, source)
560
+ );
561
+ }
562
+ }
563
+ }
564
+ for (let i = 0; i < node.childCount; i++) {
565
+ const child = node.child(i);
566
+ if (child) walk2(child);
567
+ }
568
+ };
569
+ walk2(root);
570
+ return out;
571
+ }
572
+ function symbolFrom(name, kind, start, end, source) {
573
+ return {
574
+ name,
575
+ kind,
576
+ line: source.slice(0, start).split("\n").length,
577
+ start,
578
+ end,
579
+ signature: signaturePreview(source, start, end)
580
+ };
581
+ }
582
+ function signaturePreview(source, start, end) {
583
+ const nl = source.indexOf("\n", start);
584
+ const endOfLine = nl === -1 ? end : nl;
585
+ const first = source.slice(start, Math.min(endOfLine, end)).trim();
586
+ return first.length > SIGNATURE_MAX ? `${first.slice(0, SIGNATURE_MAX)}\u2026` : first;
587
+ }
588
+
589
+ // src/parser/analyze.ts
435
590
  var languageCache = /* @__PURE__ */ new Map();
436
591
  var initPromise = null;
437
592
  function ensureParserInit() {
@@ -464,35 +619,6 @@ function loadLanguage(spec, force = false) {
464
619
  return language;
465
620
  })();
466
621
  }
467
- async function prune(filePath, source, opts = {}) {
468
- const spec = languageForFile(filePath);
469
- if (!spec || spec.rules.length === 0) {
470
- return { code: source, language: null, removed: 0 };
471
- }
472
- const language = await loadLanguage(spec, opts.forceDownload);
473
- const parser = new import_web_tree_sitter.default();
474
- parser.setLanguage(language);
475
- const tree = parser.parse(source);
476
- const ranges = [];
477
- for (const rule of spec.rules) {
478
- const query = language.query(rule.query);
479
- const captures = query.captures(tree.rootNode);
480
- for (const cap of captures) {
481
- if (cap.node.startIndex === cap.node.endIndex) continue;
482
- if (rule.replacement.keepIf?.test(cap.node.text)) continue;
483
- ranges.push({
484
- start: cap.node.startIndex,
485
- end: cap.node.endIndex,
486
- token: rule.replacement.token
487
- });
488
- }
489
- query.delete();
490
- }
491
- tree.delete();
492
- parser.delete();
493
- const { code, removed } = spliceRanges(source, ranges);
494
- return { code, language: spec.name, removed };
495
- }
496
622
  function spliceRanges(source, ranges) {
497
623
  let removed = 0;
498
624
  const sorted = [...ranges].sort((a, b) => b.start - a.start);
@@ -504,6 +630,128 @@ function spliceRanges(source, ranges) {
504
630
  }
505
631
  return { code: out, removed };
506
632
  }
633
+ async function analyze(filePath, source, opts = {}) {
634
+ const spec = languageForFile(filePath);
635
+ if (!spec || spec.rules.length === 0) {
636
+ return { code: source, language: null, removed: 0, symbols: [] };
637
+ }
638
+ let language;
639
+ try {
640
+ language = await loadLanguage(spec, opts.forceDownload);
641
+ } catch {
642
+ return { code: source, language: null, removed: 0, symbols: [] };
643
+ }
644
+ const parser = new import_web_tree_sitter.default();
645
+ parser.setLanguage(language);
646
+ const tree = parser.parse(source);
647
+ try {
648
+ const symbols = collectSymbols(tree.rootNode, source);
649
+ if (opts.skipPrune) {
650
+ return { code: source, language: spec.name, removed: 0, symbols };
651
+ }
652
+ const keepRanges = [
653
+ ...opts.keepBlocksInside ?? [],
654
+ ...opts.preserveAnnotations?.length ? annotatedKeepRanges(symbols, source, opts.preserveAnnotations) : []
655
+ ];
656
+ const isProtected = (start, end) => keepRanges.some((k) => k.start <= start && end <= k.end);
657
+ const ranges = [];
658
+ for (const rule of spec.rules) {
659
+ const query = language.query(rule.query);
660
+ const captures = query.captures(tree.rootNode);
661
+ for (const cap of captures) {
662
+ if (cap.node.startIndex === cap.node.endIndex) continue;
663
+ if (rule.replacement.keepIf?.test(cap.node.text)) continue;
664
+ if (isProtected(cap.node.startIndex, cap.node.endIndex)) continue;
665
+ ranges.push({
666
+ start: cap.node.startIndex,
667
+ end: cap.node.endIndex,
668
+ token: rule.replacement.token
669
+ });
670
+ }
671
+ query.delete();
672
+ }
673
+ const { code, removed } = spliceRanges(source, ranges);
674
+ return { code, language: spec.name, removed, symbols };
675
+ } finally {
676
+ tree.delete();
677
+ parser.delete();
678
+ }
679
+ }
680
+ var ANNOTATION_WINDOW = 240;
681
+ function annotatedKeepRanges(symbols, source, annotations) {
682
+ if (annotations.length === 0) return [];
683
+ const markers = annotations.filter(Boolean).map(escapeRegExp);
684
+ if (markers.length === 0) return [];
685
+ const re = new RegExp(`@(${markers.join("|")})\\b`, "i");
686
+ return symbols.filter((s) => {
687
+ const windowStart = Math.max(0, s.start - ANNOTATION_WINDOW);
688
+ const tail = source.slice(windowStart, s.start);
689
+ const cut = Math.max(tail.lastIndexOf("}\n"), tail.lastIndexOf(";\n"));
690
+ const relevant = cut === -1 ? tail : tail.slice(cut + 1);
691
+ return re.test(relevant);
692
+ }).map((s) => ({ start: s.start, end: s.end }));
693
+ }
694
+ function escapeRegExp(text) {
695
+ return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
696
+ }
697
+
698
+ // src/config.ts
699
+ var import_node_fs3 = __toESM(require("fs"), 1);
700
+ var import_node_path5 = __toESM(require("path"), 1);
701
+ function matchesAny(file, patterns) {
702
+ const rel2 = normalizeSlashes(file);
703
+ for (const raw of patterns) {
704
+ if (!raw) continue;
705
+ const pattern = normalizeSlashes(raw).replace(/^\.\//, "");
706
+ if (matchesGlob(rel2, pattern)) return true;
707
+ }
708
+ return false;
709
+ }
710
+ function matchesGlob(file, pattern) {
711
+ if (file === pattern) return true;
712
+ if (!/[?*[]/.test(pattern)) {
713
+ return file.endsWith(`/${pattern}`);
714
+ }
715
+ const re = globToRegExp(pattern);
716
+ return re.test(file);
717
+ }
718
+ function globToRegExp(glob) {
719
+ let re = "^";
720
+ for (let i = 0; i < glob.length; i++) {
721
+ const c = glob[i];
722
+ if (c === "*") {
723
+ if (glob[i + 1] === "*") {
724
+ i++;
725
+ if (glob[i + 1] === "/") {
726
+ i++;
727
+ re += "(?:.*/)?";
728
+ } else {
729
+ re += ".*";
730
+ }
731
+ } else {
732
+ re += "[^/]*";
733
+ }
734
+ } else if (c === "?") {
735
+ re += "[^/]";
736
+ } else if (c === "[") {
737
+ const close = glob.indexOf("]", i);
738
+ if (close === -1) {
739
+ re += "\\[";
740
+ } else {
741
+ const inner = glob.slice(i + 1, close).replace(/\\/g, "\\\\");
742
+ re += `[${inner}]`;
743
+ i = close;
744
+ }
745
+ } else {
746
+ re += c.replace(/[.+^${}()|\\]/g, "\\$&");
747
+ }
748
+ }
749
+ re += "$";
750
+ return new RegExp(re);
751
+ }
752
+ function normalizeSlashes(p) {
753
+ return p.split(import_node_path5.default.sep).join("/").replace(/^\.\//, "");
754
+ }
507
755
 
508
756
  // src/watcher/sync.ts
509
757
  var DEFAULT_IGNORED = [
@@ -523,7 +771,7 @@ var ContextCache = class {
523
771
  }
524
772
  /** Returns the cached skeleton for a file, if present. */
525
773
  getSkeleton(filePath) {
526
- return this.entries.get(import_node_path5.default.resolve(filePath)) ?? null;
774
+ return this.entries.get(import_node_path6.default.resolve(filePath)) ?? null;
527
775
  }
528
776
  get imports() {
529
777
  return this.entries;
@@ -545,18 +793,18 @@ function extractImports(_filePath, source) {
545
793
  function resolveImport(importer, specifier, root) {
546
794
  if (!/^[.~@]/.test(specifier)) {
547
795
  if (specifier.startsWith("@/")) {
548
- return resolveCandidate(import_node_path5.default.join(root, specifier.slice(2)));
796
+ return resolveCandidate(import_node_path6.default.join(root, specifier.slice(2)));
549
797
  }
550
798
  if (specifier.startsWith("@")) {
551
799
  return null;
552
800
  }
553
801
  if (specifier.startsWith("~")) {
554
- return resolveCandidate(import_node_path5.default.join(root, specifier.slice(1)));
802
+ return resolveCandidate(import_node_path6.default.join(root, specifier.slice(1)));
555
803
  }
556
804
  return null;
557
805
  }
558
- const base = import_node_path5.default.dirname(import_node_path5.default.resolve(importer));
559
- return resolveCandidate(import_node_path5.default.resolve(base, specifier));
806
+ const base = import_node_path6.default.dirname(import_node_path6.default.resolve(importer));
807
+ return resolveCandidate(import_node_path6.default.resolve(base, specifier));
560
808
  }
561
809
  function resolveCandidate(p) {
562
810
  const candidates = [
@@ -579,29 +827,34 @@ function resolveCandidate(p) {
579
827
  `${p}.h`,
580
828
  `${p}.hpp`,
581
829
  `${p}.php`,
582
- import_node_path5.default.join(p, "index.ts"),
583
- import_node_path5.default.join(p, "index.js"),
584
- import_node_path5.default.join(p, "index.tsx"),
585
- import_node_path5.default.join(p, "index.jsx"),
586
- import_node_path5.default.join(p, "index.py")
830
+ import_node_path6.default.join(p, "index.ts"),
831
+ import_node_path6.default.join(p, "index.js"),
832
+ import_node_path6.default.join(p, "index.tsx"),
833
+ import_node_path6.default.join(p, "index.jsx"),
834
+ import_node_path6.default.join(p, "index.py")
587
835
  ];
588
836
  for (const c of candidates) {
589
- if (import_node_fs3.default.existsSync(c) && import_node_fs3.default.statSync(c).isFile()) return import_node_path5.default.resolve(c);
837
+ if (import_node_fs4.default.existsSync(c) && import_node_fs4.default.statSync(c).isFile()) return import_node_path6.default.resolve(c);
590
838
  }
591
839
  return null;
592
840
  }
593
841
  function createWatcher(opts) {
594
842
  const cache = new ContextCache();
595
- const { root, ignored = DEFAULT_IGNORED, onIndexed } = opts;
843
+ const { root, ignored = DEFAULT_IGNORED, config, onIndexed, onRemoved } = opts;
844
+ const rel2 = (abs) => import_node_path6.default.relative(root, abs);
845
+ const effectiveIgnored = [
846
+ ...ignored,
847
+ ...config?.ignorePatterns?.length ? [(_abs) => matchesAny(rel2(String(_abs)), config.ignorePatterns)] : []
848
+ ];
596
849
  const pending = /* @__PURE__ */ new Map();
597
850
  const DEBOUNCE_MS = 100;
598
851
  async function handle(filePath) {
599
- const abs = import_node_path5.default.resolve(filePath);
852
+ const abs = import_node_path6.default.resolve(filePath);
600
853
  let source;
601
854
  try {
602
- const st = import_node_fs3.default.statSync(abs);
855
+ const st = import_node_fs4.default.statSync(abs);
603
856
  if (!st.isFile()) return;
604
- source = import_node_fs3.default.readFileSync(abs, "utf8");
857
+ source = import_node_fs4.default.readFileSync(abs, "utf8");
605
858
  } catch {
606
859
  return;
607
860
  }
@@ -609,13 +862,18 @@ function createWatcher(opts) {
609
862
  const cached = cache.entries.get(abs);
610
863
  if (cached && cached.hash === hash) return;
611
864
  await cache.ensureInit();
612
- const { code, language } = await prune(abs, source);
613
- const imports = await extractImports(abs, source);
865
+ const keepFull = config?.keepUnpruned?.length ? matchesAny(import_node_path6.default.relative(root, abs), config.keepUnpruned) : false;
866
+ const { code, language, symbols } = await analyze(abs, source, {
867
+ skipPrune: keepFull,
868
+ preserveAnnotations: config?.preserveAnnotations
869
+ });
870
+ const imports = extractImports(abs, source);
614
871
  const entry = {
615
872
  hash,
616
873
  skeleton: code,
617
874
  language,
618
- imports: imports.map((spec) => resolveImport(abs, spec, root)).filter((p) => p !== null)
875
+ imports: imports.map((spec) => resolveImport(abs, spec, root)).filter((p) => p !== null),
876
+ ...symbols.length > 0 ? { symbols } : {}
619
877
  };
620
878
  cache.entries.set(abs, entry);
621
879
  onIndexed?.(abs, entry);
@@ -625,7 +883,7 @@ function createWatcher(opts) {
625
883
  // sockets) are never opened with fs.watch (which raises UVException).
626
884
  ignored(_p, stats) {
627
885
  if (stats && !stats.isFile() && !stats.isDirectory()) return true;
628
- return isIgnored(import_node_path5.default.resolve(String(_p)), ignored);
886
+ return isIgnored(import_node_path6.default.resolve(String(_p)), effectiveIgnored);
629
887
  },
630
888
  alwaysStat: true,
631
889
  ignoreInitial: true,
@@ -633,7 +891,7 @@ function createWatcher(opts) {
633
891
  awaitWriteFinish: { stabilityThreshold: 50, pollInterval: 10 }
634
892
  });
635
893
  function debounce(filePath) {
636
- const abs = import_node_path5.default.resolve(filePath);
894
+ const abs = import_node_path6.default.resolve(filePath);
637
895
  const existing = pending.get(abs);
638
896
  if (existing) clearTimeout(existing);
639
897
  if (!existing) void handle(abs);
@@ -645,8 +903,9 @@ function createWatcher(opts) {
645
903
  watcher.on("add", debounce);
646
904
  watcher.on("change", debounce);
647
905
  watcher.on("unlink", (filePath) => {
648
- const abs = import_node_path5.default.resolve(filePath);
906
+ const abs = import_node_path6.default.resolve(filePath);
649
907
  cache.entries.delete(abs);
908
+ onRemoved?.(abs);
650
909
  });
651
910
  return {
652
911
  cache,
@@ -659,7 +918,7 @@ function createWatcher(opts) {
659
918
  */
660
919
  async indexAll() {
661
920
  const files = [];
662
- await walk(root, (f) => files.push(f), ignored);
921
+ await walk(root, (f) => files.push(f), effectiveIgnored);
663
922
  let ok = 0;
664
923
  for (const f of files) {
665
924
  try {
@@ -674,9 +933,9 @@ function createWatcher(opts) {
674
933
  };
675
934
  }
676
935
  async function walk(root, push, ignored) {
677
- const entries = await import_node_fs3.default.promises.readdir(root, { withFileTypes: true });
936
+ const entries = await import_node_fs4.default.promises.readdir(root, { withFileTypes: true });
678
937
  for (const e of entries) {
679
- const abs = import_node_path5.default.join(root, e.name);
938
+ const abs = import_node_path6.default.join(root, e.name);
680
939
  if (isIgnored(abs, ignored)) continue;
681
940
  if (e.isDirectory()) {
682
941
  await walk(abs, push, ignored);
@@ -687,6 +946,10 @@ async function walk(root, push, ignored) {
687
946
  }
688
947
  function isIgnored(abs, ignored) {
689
948
  for (const m of ignored) {
949
+ if (typeof m === "function") {
950
+ if (m(abs)) return true;
951
+ continue;
952
+ }
690
953
  if (typeof m === "string" && abs.includes(m)) return true;
691
954
  if (m instanceof RegExp && m.test(abs)) return true;
692
955
  }
@@ -715,8 +978,9 @@ async function startServer(opts = {}) {
715
978
  const args = parseArgv();
716
979
  const port = opts.port ?? Number(args.port ?? process.env.PORT ?? 3e3);
717
980
  const host = opts.host ?? args.host ?? "0.0.0.0";
718
- const root = import_node_path6.default.resolve(opts.root ?? args.root ?? process.cwd());
981
+ const root = import_node_path7.default.resolve(opts.root ?? args.root ?? process.cwd());
719
982
  const silent = opts.silent ?? args.silent === "true";
983
+ const maxTokensDefault = args["max-tokens"] ? Number(args["max-tokens"]) : void 0;
720
984
  const log = (msg) => {
721
985
  if (!silent) console.log(import_picocolors.default.dim(msg));
722
986
  };
@@ -735,18 +999,27 @@ async function startServer(opts = {}) {
735
999
  }));
736
1000
  app.post("/v1/context", async (req, reply) => {
737
1001
  const body = req.body ?? {};
738
- if (!body.activeFilePath) {
739
- return reply.status(400).send({ error: "`activeFilePath` is required" });
1002
+ const hasMany = Array.isArray(body.activeFiles) && body.activeFiles.length > 0;
1003
+ if (hasMany && body.activeFilePath) {
1004
+ return reply.status(400).send({ error: "Pass either `activeFilePath` or `activeFiles`, not both." });
1005
+ }
1006
+ const paths = hasMany ? body.activeFiles : body.activeFilePath ? [body.activeFilePath] : [];
1007
+ if (paths.length === 0) {
1008
+ return reply.status(400).send({ error: "`activeFilePath` or `activeFiles` is required" });
740
1009
  }
741
- const result = assemble(body.activeFilePath, watcher.cache.entries, {
1010
+ const maxTokens = body.maxTokens !== void 0 ? body.maxTokens : maxTokensDefault;
1011
+ const result = assembleMany(paths, watcher.cache.entries, {
742
1012
  maxSkeletons: body.maxSkeletons,
1013
+ maxTokens: Number.isFinite(maxTokens) ? maxTokens : void 0,
743
1014
  includeStats: body.includeStats
744
1015
  });
745
1016
  return {
746
1017
  markdown: result.markdown,
747
1018
  activeFilePath: result.activeFilePath,
1019
+ activeFilePaths: result.activeFilePaths,
748
1020
  dependencies: result.included.map((i) => i.filePath),
749
- unresolved: result.unresolved
1021
+ unresolved: result.unresolved,
1022
+ tokenStats: result.tokenStats
750
1023
  };
751
1024
  });
752
1025
  app.setNotFoundHandler(async (req, reply) => {
@@ -757,8 +1030,13 @@ async function startServer(opts = {}) {
757
1030
  log(`Listening on http://${host}:${port}`);
758
1031
  return { app, watcher };
759
1032
  }
760
- var argv1 = process.argv[1] ? import_node_path6.default.basename(process.argv[1]) : "";
761
- if (argv1 === "cli.js" || argv1 === "cli.mjs" || argv1 === "cli.cjs" || argv1 === "cli.ts") {
1033
+ var argv1 = process.argv[1] ? import_node_path7.default.basename(process.argv[1]) : "";
1034
+ var argv1Real = "";
1035
+ try {
1036
+ argv1Real = process.argv[1] ? import_node_path7.default.basename(import_node_fs5.default.realpathSync(process.argv[1])) : "";
1037
+ } catch {
1038
+ }
1039
+ 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") {
762
1040
  startServer().catch((err) => {
763
1041
  console.error(import_picocolors.default.red(`[token-shrink] ${err.message}`));
764
1042
  process.exitCode = 1;