@ajdev0/token-shrink 2.0.2 → 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,104 +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_fs4 = __toESM(require("fs"), 1);
192
- 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);
193
193
 
194
194
  // src/server/assembler.ts
195
195
  var import_node_fs = __toESM(require("fs"), 1);
196
196
  var import_node_path = __toESM(require("path"), 1);
197
- function assemble(activeFilePath, cache, opts = {}) {
198
- const { includeStats = false, maxSkeletons = 50, pruneActiveFile = false } = opts;
199
- const abs = import_node_path.default.resolve(activeFilePath);
200
- const activeSource = import_node_fs.default.existsSync(abs) ? import_node_fs.default.readFileSync(abs, "utf8") : "";
201
- 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
+ });
202
218
  const included = [];
203
219
  const unresolved = [];
204
220
  const seen = /* @__PURE__ */ new Set();
205
- if (activeEntry) {
206
- for (const imp of activeEntry.imports) {
207
- const entry = cache.get(imp);
208
- if (!entry) {
209
- unresolved.push(imp);
210
- 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);
211
251
  }
212
- if (seen.has(imp)) continue;
213
- seen.add(imp);
214
- included.push({ filePath: imp, language: entry.language });
215
- if (included.length >= maxSkeletons) break;
216
252
  }
217
253
  }
218
- if (!activeEntry && activeSource) {
219
- const specifiers = extractSpecifiers(activeSource);
220
- for (const spec of specifiers) {
221
- const resolved = resolveLocal(abs, spec);
222
- if (!resolved) {
223
- unresolved.push(spec);
224
- continue;
225
- }
226
- if (seen.has(resolved)) continue;
227
- seen.add(resolved);
228
- const cached = cache.get(resolved);
229
- included.push({ filePath: resolved, language: cached?.language ?? null });
230
- 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;
231
266
  }
267
+ selected.push(cand);
268
+ dependencyTokens += cost;
232
269
  }
270
+ const activeTokens = ring0Texts.reduce(
271
+ (acc, t) => acc + approximateTokens(t),
272
+ 0
273
+ );
233
274
  const lines = [];
234
275
  lines.push("# Compressed Code Context", "");
235
- lines.push(`Active file: \`${rel(abs)}\``, "");
236
- lines.push("## Ring 0 \u2014 Active file (full text)", "");
237
- const ring0 = pruneActiveFile && activeEntry ? activeEntry.skeleton : activeSource;
238
- lines.push(`\`\`\`${ext(activeFilePath)}`);
239
- lines.push(ring0.trim() || "(empty or unreadable file)");
240
- 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
+ }
241
292
  lines.push(
242
- `## Ring 1 \u2014 Pruned dependencies (${included.length})`,
293
+ `## Ring 1 \u2014 Pruned dependencies (${selected.length})`,
243
294
  "",
244
295
  "Implementation bodies removed; type signatures, interfaces and exports retained.",
245
296
  ""
246
297
  );
247
- if (included.length === 0) {
298
+ if (selected.length === 0) {
248
299
  lines.push("_No local dependency skeletons available._", "");
249
300
  }
250
- for (const inc of included) {
301
+ for (const inc of selected) {
251
302
  const entry = cache.get(inc.filePath);
252
303
  const label = rel(inc.filePath);
253
304
  lines.push(`### \`${label}\``, "");
254
305
  if (entry) {
255
- lines.push(`\`\`\`${ext(inc.filePath)}`);
256
- lines.push(entry.skeleton.trim());
257
- lines.push("```", "");
306
+ lines.push(`\`\`\`${ext(inc.filePath)}`, entry.skeleton.trim(), "```", "");
258
307
  } else {
259
308
  lines.push("_Unindexed file._", "");
260
309
  }
261
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
+ }
262
317
  if (unresolved.length > 0) {
263
318
  lines.push("## Unresolved imports", "");
264
319
  for (const u of unresolved) lines.push(`- \`${u}\``);
265
320
  lines.push("");
266
321
  }
267
322
  if (includeStats) {
268
- const depTokens = included.reduce(
269
- (acc, inc) => {
270
- const e = cache.get(inc.filePath);
271
- return e ? acc + approximateTokens(e.skeleton) : acc;
272
- },
273
- 0
274
- );
275
- lines.push("---", "");
323
+ const budgetNote = maxTokens !== void 0 ? ` \xB7 budget ${maxTokens} (${trimmed} deps trimmed)` : "";
276
324
  lines.push(
277
- `_Token estimate \u2014 active: ${approximateTokens(activeSource)} \xB7 pruned deps: ${depTokens}._`,
325
+ "---",
326
+ "",
327
+ `_Token estimate \u2014 active: ${activeTokens} \xB7 pruned deps: ${dependencyTokens}${budgetNote}._`,
278
328
  ""
279
329
  );
280
330
  }
281
331
  return {
282
332
  markdown: lines.join("\n"),
283
- activeFilePath: abs,
284
- activeSource,
285
- included,
286
- 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
+ }
287
345
  };
288
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
+ }
289
365
  function extractSpecifiers(source) {
290
366
  const found = [];
291
367
  const re = /(?:from\s+['"`]|import\s+['"`]|require\s*\(\s*['"`])([^'"`]+)['"`]/g;
@@ -342,12 +418,12 @@ function approximateTokens(text) {
342
418
  }
343
419
 
344
420
  // src/watcher/sync.ts
345
- var import_node_fs3 = __toESM(require("fs"), 1);
346
- 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);
347
423
  var import_node_crypto = require("crypto");
348
424
  var import_chokidar = __toESM(require("chokidar"), 1);
349
425
 
350
- // src/parser/pruner.ts
426
+ // src/parser/analyze.ts
351
427
  var import_web_tree_sitter = __toESM(require("web-tree-sitter"), 1);
352
428
  var import_node_module = require("module");
353
429
  var import_node_path4 = __toESM(require("path"), 1);
@@ -432,7 +508,85 @@ async function warmGrammars(specs = []) {
432
508
  return results.filter((r) => r.status === "fulfilled").length;
433
509
  }
434
510
 
435
- // 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
436
590
  var languageCache = /* @__PURE__ */ new Map();
437
591
  var initPromise = null;
438
592
  function ensureParserInit() {
@@ -465,35 +619,6 @@ function loadLanguage(spec, force = false) {
465
619
  return language;
466
620
  })();
467
621
  }
468
- async function prune(filePath, source, opts = {}) {
469
- const spec = languageForFile(filePath);
470
- if (!spec || spec.rules.length === 0) {
471
- return { code: source, language: null, removed: 0 };
472
- }
473
- const language = await loadLanguage(spec, opts.forceDownload);
474
- const parser = new import_web_tree_sitter.default();
475
- parser.setLanguage(language);
476
- const tree = parser.parse(source);
477
- const ranges = [];
478
- for (const rule of spec.rules) {
479
- const query = language.query(rule.query);
480
- const captures = query.captures(tree.rootNode);
481
- for (const cap of captures) {
482
- if (cap.node.startIndex === cap.node.endIndex) continue;
483
- if (rule.replacement.keepIf?.test(cap.node.text)) continue;
484
- ranges.push({
485
- start: cap.node.startIndex,
486
- end: cap.node.endIndex,
487
- token: rule.replacement.token
488
- });
489
- }
490
- query.delete();
491
- }
492
- tree.delete();
493
- parser.delete();
494
- const { code, removed } = spliceRanges(source, ranges);
495
- return { code, language: spec.name, removed };
496
- }
497
622
  function spliceRanges(source, ranges) {
498
623
  let removed = 0;
499
624
  const sorted = [...ranges].sort((a, b) => b.start - a.start);
@@ -505,6 +630,128 @@ function spliceRanges(source, ranges) {
505
630
  }
506
631
  return { code: out, removed };
507
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
+ }
508
755
 
509
756
  // src/watcher/sync.ts
510
757
  var DEFAULT_IGNORED = [
@@ -524,7 +771,7 @@ var ContextCache = class {
524
771
  }
525
772
  /** Returns the cached skeleton for a file, if present. */
526
773
  getSkeleton(filePath) {
527
- return this.entries.get(import_node_path5.default.resolve(filePath)) ?? null;
774
+ return this.entries.get(import_node_path6.default.resolve(filePath)) ?? null;
528
775
  }
529
776
  get imports() {
530
777
  return this.entries;
@@ -546,18 +793,18 @@ function extractImports(_filePath, source) {
546
793
  function resolveImport(importer, specifier, root) {
547
794
  if (!/^[.~@]/.test(specifier)) {
548
795
  if (specifier.startsWith("@/")) {
549
- return resolveCandidate(import_node_path5.default.join(root, specifier.slice(2)));
796
+ return resolveCandidate(import_node_path6.default.join(root, specifier.slice(2)));
550
797
  }
551
798
  if (specifier.startsWith("@")) {
552
799
  return null;
553
800
  }
554
801
  if (specifier.startsWith("~")) {
555
- return resolveCandidate(import_node_path5.default.join(root, specifier.slice(1)));
802
+ return resolveCandidate(import_node_path6.default.join(root, specifier.slice(1)));
556
803
  }
557
804
  return null;
558
805
  }
559
- const base = import_node_path5.default.dirname(import_node_path5.default.resolve(importer));
560
- 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));
561
808
  }
562
809
  function resolveCandidate(p) {
563
810
  const candidates = [
@@ -580,29 +827,34 @@ function resolveCandidate(p) {
580
827
  `${p}.h`,
581
828
  `${p}.hpp`,
582
829
  `${p}.php`,
583
- import_node_path5.default.join(p, "index.ts"),
584
- import_node_path5.default.join(p, "index.js"),
585
- import_node_path5.default.join(p, "index.tsx"),
586
- import_node_path5.default.join(p, "index.jsx"),
587
- 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")
588
835
  ];
589
836
  for (const c of candidates) {
590
- 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);
591
838
  }
592
839
  return null;
593
840
  }
594
841
  function createWatcher(opts) {
595
842
  const cache = new ContextCache();
596
- 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
+ ];
597
849
  const pending = /* @__PURE__ */ new Map();
598
850
  const DEBOUNCE_MS = 100;
599
851
  async function handle(filePath) {
600
- const abs = import_node_path5.default.resolve(filePath);
852
+ const abs = import_node_path6.default.resolve(filePath);
601
853
  let source;
602
854
  try {
603
- const st = import_node_fs3.default.statSync(abs);
855
+ const st = import_node_fs4.default.statSync(abs);
604
856
  if (!st.isFile()) return;
605
- source = import_node_fs3.default.readFileSync(abs, "utf8");
857
+ source = import_node_fs4.default.readFileSync(abs, "utf8");
606
858
  } catch {
607
859
  return;
608
860
  }
@@ -610,13 +862,18 @@ function createWatcher(opts) {
610
862
  const cached = cache.entries.get(abs);
611
863
  if (cached && cached.hash === hash) return;
612
864
  await cache.ensureInit();
613
- const { code, language } = await prune(abs, source);
614
- 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);
615
871
  const entry = {
616
872
  hash,
617
873
  skeleton: code,
618
874
  language,
619
- 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 } : {}
620
877
  };
621
878
  cache.entries.set(abs, entry);
622
879
  onIndexed?.(abs, entry);
@@ -626,7 +883,7 @@ function createWatcher(opts) {
626
883
  // sockets) are never opened with fs.watch (which raises UVException).
627
884
  ignored(_p, stats) {
628
885
  if (stats && !stats.isFile() && !stats.isDirectory()) return true;
629
- return isIgnored(import_node_path5.default.resolve(String(_p)), ignored);
886
+ return isIgnored(import_node_path6.default.resolve(String(_p)), effectiveIgnored);
630
887
  },
631
888
  alwaysStat: true,
632
889
  ignoreInitial: true,
@@ -634,7 +891,7 @@ function createWatcher(opts) {
634
891
  awaitWriteFinish: { stabilityThreshold: 50, pollInterval: 10 }
635
892
  });
636
893
  function debounce(filePath) {
637
- const abs = import_node_path5.default.resolve(filePath);
894
+ const abs = import_node_path6.default.resolve(filePath);
638
895
  const existing = pending.get(abs);
639
896
  if (existing) clearTimeout(existing);
640
897
  if (!existing) void handle(abs);
@@ -646,8 +903,9 @@ function createWatcher(opts) {
646
903
  watcher.on("add", debounce);
647
904
  watcher.on("change", debounce);
648
905
  watcher.on("unlink", (filePath) => {
649
- const abs = import_node_path5.default.resolve(filePath);
906
+ const abs = import_node_path6.default.resolve(filePath);
650
907
  cache.entries.delete(abs);
908
+ onRemoved?.(abs);
651
909
  });
652
910
  return {
653
911
  cache,
@@ -660,7 +918,7 @@ function createWatcher(opts) {
660
918
  */
661
919
  async indexAll() {
662
920
  const files = [];
663
- await walk(root, (f) => files.push(f), ignored);
921
+ await walk(root, (f) => files.push(f), effectiveIgnored);
664
922
  let ok = 0;
665
923
  for (const f of files) {
666
924
  try {
@@ -675,9 +933,9 @@ function createWatcher(opts) {
675
933
  };
676
934
  }
677
935
  async function walk(root, push, ignored) {
678
- 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 });
679
937
  for (const e of entries) {
680
- const abs = import_node_path5.default.join(root, e.name);
938
+ const abs = import_node_path6.default.join(root, e.name);
681
939
  if (isIgnored(abs, ignored)) continue;
682
940
  if (e.isDirectory()) {
683
941
  await walk(abs, push, ignored);
@@ -688,6 +946,10 @@ async function walk(root, push, ignored) {
688
946
  }
689
947
  function isIgnored(abs, ignored) {
690
948
  for (const m of ignored) {
949
+ if (typeof m === "function") {
950
+ if (m(abs)) return true;
951
+ continue;
952
+ }
691
953
  if (typeof m === "string" && abs.includes(m)) return true;
692
954
  if (m instanceof RegExp && m.test(abs)) return true;
693
955
  }
@@ -716,8 +978,9 @@ async function startServer(opts = {}) {
716
978
  const args = parseArgv();
717
979
  const port = opts.port ?? Number(args.port ?? process.env.PORT ?? 3e3);
718
980
  const host = opts.host ?? args.host ?? "0.0.0.0";
719
- 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());
720
982
  const silent = opts.silent ?? args.silent === "true";
983
+ const maxTokensDefault = args["max-tokens"] ? Number(args["max-tokens"]) : void 0;
721
984
  const log = (msg) => {
722
985
  if (!silent) console.log(import_picocolors.default.dim(msg));
723
986
  };
@@ -736,18 +999,27 @@ async function startServer(opts = {}) {
736
999
  }));
737
1000
  app.post("/v1/context", async (req, reply) => {
738
1001
  const body = req.body ?? {};
739
- if (!body.activeFilePath) {
740
- 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" });
741
1009
  }
742
- 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, {
743
1012
  maxSkeletons: body.maxSkeletons,
1013
+ maxTokens: Number.isFinite(maxTokens) ? maxTokens : void 0,
744
1014
  includeStats: body.includeStats
745
1015
  });
746
1016
  return {
747
1017
  markdown: result.markdown,
748
1018
  activeFilePath: result.activeFilePath,
1019
+ activeFilePaths: result.activeFilePaths,
749
1020
  dependencies: result.included.map((i) => i.filePath),
750
- unresolved: result.unresolved
1021
+ unresolved: result.unresolved,
1022
+ tokenStats: result.tokenStats
751
1023
  };
752
1024
  });
753
1025
  app.setNotFoundHandler(async (req, reply) => {
@@ -758,10 +1030,10 @@ async function startServer(opts = {}) {
758
1030
  log(`Listening on http://${host}:${port}`);
759
1031
  return { app, watcher };
760
1032
  }
761
- var argv1 = process.argv[1] ? import_node_path6.default.basename(process.argv[1]) : "";
1033
+ var argv1 = process.argv[1] ? import_node_path7.default.basename(process.argv[1]) : "";
762
1034
  var argv1Real = "";
763
1035
  try {
764
- argv1Real = process.argv[1] ? import_node_path6.default.basename(import_node_fs4.default.realpathSync(process.argv[1])) : "";
1036
+ argv1Real = process.argv[1] ? import_node_path7.default.basename(import_node_fs5.default.realpathSync(process.argv[1])) : "";
765
1037
  } catch {
766
1038
  }
767
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") {