@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.
@@ -0,0 +1,847 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ languageForFile
4
+ } from "./chunk-7SQ6HMWM.js";
5
+
6
+ // src/server/assembler.ts
7
+ import fs from "fs";
8
+ import path from "path";
9
+ var MARKDOWN_OVERHEAD_TOKENS = 40;
10
+ function assembleMany(activeFiles, cache, opts = {}) {
11
+ const {
12
+ includeStats = false,
13
+ maxSkeletons = 50,
14
+ maxTokens,
15
+ pruneActiveFile = false
16
+ } = opts;
17
+ const absList = activeFiles.map((p) => path.resolve(p));
18
+ const sources = absList.map((abs) => {
19
+ try {
20
+ return fs.readFileSync(abs, "utf8");
21
+ } catch {
22
+ return "";
23
+ }
24
+ });
25
+ const activeSet = new Set(absList);
26
+ const ring0Texts = absList.map((abs, i) => {
27
+ const entry = cache.get(abs);
28
+ return pruneActiveFile && entry ? entry.skeleton : sources[i];
29
+ });
30
+ const included = [];
31
+ const unresolved = [];
32
+ const seen = /* @__PURE__ */ new Set();
33
+ const addUnresolved = (p) => {
34
+ if (!unresolved.includes(p)) unresolved.push(p);
35
+ };
36
+ const addIncluded = (p, allowUncached) => {
37
+ if (seen.has(p)) return;
38
+ const entry = cache.get(p);
39
+ if (!entry && !allowUncached) {
40
+ addUnresolved(p);
41
+ return;
42
+ }
43
+ seen.add(p);
44
+ included.push({ filePath: p, language: entry?.language ?? null });
45
+ };
46
+ for (let i = 0; i < absList.length; i++) {
47
+ const abs = absList[i];
48
+ const entry = cache.get(abs);
49
+ if (entry) {
50
+ for (const imp of entry.imports) {
51
+ if (activeSet.has(imp)) continue;
52
+ addIncluded(imp, false);
53
+ }
54
+ } else if (sources[i]) {
55
+ for (const spec of extractSpecifiers(sources[i])) {
56
+ const resolved = resolveLocal(abs, spec);
57
+ if (!resolved) {
58
+ addUnresolved(spec);
59
+ continue;
60
+ }
61
+ if (activeSet.has(resolved)) continue;
62
+ addIncluded(resolved, true);
63
+ }
64
+ }
65
+ }
66
+ const ordered = maxTokens === void 0 ? included : rankByRelevance(included, cache);
67
+ const budget = maxTokens === void 0 ? void 0 : Math.max(0, maxTokens - MARKDOWN_OVERHEAD_TOKENS);
68
+ const selected = [];
69
+ let trimmed = 0;
70
+ let dependencyTokens = 0;
71
+ for (const cand of ordered) {
72
+ if (selected.length >= maxSkeletons) break;
73
+ const entry = cache.get(cand.filePath);
74
+ const cost = entry ? approximateTokens(entry.skeleton) : 8;
75
+ if (budget !== void 0 && dependencyTokens + cost > budget) {
76
+ trimmed++;
77
+ continue;
78
+ }
79
+ selected.push(cand);
80
+ dependencyTokens += cost;
81
+ }
82
+ const activeTokens = ring0Texts.reduce(
83
+ (acc, t) => acc + approximateTokens(t),
84
+ 0
85
+ );
86
+ const lines = [];
87
+ lines.push("# Compressed Code Context", "");
88
+ if (absList.length === 1) {
89
+ lines.push(`Active file: \`${rel(absList[0])}\``, "");
90
+ } else {
91
+ lines.push("Active files:", "");
92
+ for (const p of absList) lines.push(`- \`${rel(p)}\``);
93
+ lines.push("");
94
+ }
95
+ lines.push(
96
+ absList.length === 1 ? "## Ring 0 \u2014 Active file (full text)" : "## Ring 0 \u2014 Active files (full text)",
97
+ ""
98
+ );
99
+ for (let i = 0; i < absList.length; i++) {
100
+ if (absList.length > 1) lines.push(`### \`${rel(absList[i])}\``, "");
101
+ const body = ring0Texts[i].trim() || "(empty or unreadable file)";
102
+ lines.push(`\`\`\`${ext(absList[i])}`, body, "```", "");
103
+ }
104
+ lines.push(
105
+ `## Ring 1 \u2014 Pruned dependencies (${selected.length})`,
106
+ "",
107
+ "Implementation bodies removed; type signatures, interfaces and exports retained.",
108
+ ""
109
+ );
110
+ if (selected.length === 0) {
111
+ lines.push("_No local dependency skeletons available._", "");
112
+ }
113
+ for (const inc of selected) {
114
+ const entry = cache.get(inc.filePath);
115
+ const label = rel(inc.filePath);
116
+ lines.push(`### \`${label}\``, "");
117
+ if (entry) {
118
+ lines.push(`\`\`\`${ext(inc.filePath)}`, entry.skeleton.trim(), "```", "");
119
+ } else {
120
+ lines.push("_Unindexed file._", "");
121
+ }
122
+ }
123
+ if (maxTokens !== void 0 && trimmed > 0) {
124
+ lines.push(
125
+ `_Note: token budget of ${maxTokens} excluded ${trimmed} lower-priority dependenc${trimmed === 1 ? "y" : "ies"}._`,
126
+ ""
127
+ );
128
+ }
129
+ if (unresolved.length > 0) {
130
+ lines.push("## Unresolved imports", "");
131
+ for (const u of unresolved) lines.push(`- \`${u}\``);
132
+ lines.push("");
133
+ }
134
+ if (includeStats) {
135
+ const budgetNote = maxTokens !== void 0 ? ` \xB7 budget ${maxTokens} (${trimmed} deps trimmed)` : "";
136
+ lines.push(
137
+ "---",
138
+ "",
139
+ `_Token estimate \u2014 active: ${activeTokens} \xB7 pruned deps: ${dependencyTokens}${budgetNote}._`,
140
+ ""
141
+ );
142
+ }
143
+ return {
144
+ markdown: lines.join("\n"),
145
+ activeFilePath: absList[0] ?? "",
146
+ activeFilePaths: absList,
147
+ activeSource: sources.join("\n"),
148
+ included: selected,
149
+ unresolved,
150
+ tokenStats: {
151
+ activeTokens,
152
+ dependencyTokens,
153
+ totalTokens: activeTokens + dependencyTokens,
154
+ ...maxTokens !== void 0 ? { budget: maxTokens } : {},
155
+ trimmed
156
+ }
157
+ };
158
+ }
159
+ function assemble(activeFilePath, cache, opts = {}) {
160
+ return assembleMany([activeFilePath], cache, opts);
161
+ }
162
+ function rankByRelevance(candidates, cache) {
163
+ const fanIn = /* @__PURE__ */ new Map();
164
+ for (const entry of cache.values()) {
165
+ for (const imp of entry.imports) fanIn.set(imp, (fanIn.get(imp) ?? 0) + 1);
166
+ }
167
+ return candidates.map((cand, idx) => ({ cand, idx })).sort((a, b) => {
168
+ const fa = fanIn.get(a.cand.filePath) ?? 0;
169
+ const fb = fanIn.get(b.cand.filePath) ?? 0;
170
+ if (fa !== fb) return fb - fa;
171
+ const da = pathDepth(a.cand.filePath);
172
+ const db = pathDepth(b.cand.filePath);
173
+ if (da !== db) return da - db;
174
+ return a.idx - b.idx;
175
+ }).map((x) => x.cand);
176
+ }
177
+ function pathDepth(p) {
178
+ return p.split(/[/\\]+/).filter(Boolean).length;
179
+ }
180
+ function extractSpecifiers(source) {
181
+ const found = [];
182
+ const re = /(?:from\s+['"`]|import\s+['"`]|require\s*\(\s*['"`])([^'"`]+)['"`]/g;
183
+ let m;
184
+ while ((m = re.exec(source)) !== null) {
185
+ if (m[1]) found.push(m[1]);
186
+ }
187
+ return [...new Set(found)];
188
+ }
189
+ function resolveLocal(importer, specifier) {
190
+ if (!/^[.~@]/.test(specifier)) return null;
191
+ if (specifier.startsWith("@/")) specifier = specifier.slice(2);
192
+ else if (specifier.startsWith("~")) specifier = specifier.slice(1);
193
+ const base = path.dirname(path.resolve(importer));
194
+ const p = path.resolve(base, specifier);
195
+ const candidates = [
196
+ p,
197
+ `${p}.ts`,
198
+ `${p}.tsx`,
199
+ `${p}.js`,
200
+ `${p}.jsx`,
201
+ `${p}.py`,
202
+ `${p}.go`,
203
+ `${p}.rs`,
204
+ `${p}.dart`,
205
+ `${p}.swift`,
206
+ `${p}.java`,
207
+ `${p}.kt`,
208
+ `${p}.c`,
209
+ `${p}.cpp`,
210
+ path.join(p, "index.ts"),
211
+ path.join(p, "index.js")
212
+ ];
213
+ for (const c of candidates) {
214
+ if (fs.existsSync(c) && fs.statSync(c).isFile()) return path.resolve(c);
215
+ }
216
+ return null;
217
+ }
218
+ function rel(p) {
219
+ try {
220
+ const cwd = process.cwd();
221
+ if (p.startsWith(cwd)) return "." + p.slice(cwd.length);
222
+ } catch {
223
+ }
224
+ return p;
225
+ }
226
+ function ext(p) {
227
+ return path.extname(p).replace(/^\./, "") || "text";
228
+ }
229
+ function approximateTokens(text) {
230
+ if (!text) return 0;
231
+ const tokens = text.match(/[A-Za-z0-9_$]+|[^A-Za-z0-9_\s$]/g);
232
+ return tokens ? tokens.length : 0;
233
+ }
234
+
235
+ // src/parser/wasm.ts
236
+ import fs2 from "fs";
237
+ import path2 from "path";
238
+ function resolveWasmDir() {
239
+ const candidates = [
240
+ path2.resolve(__dirname, "../../wasm"),
241
+ path2.resolve(__dirname, "../wasm"),
242
+ path2.resolve(process.cwd(), "wasm")
243
+ ];
244
+ for (const dir2 of candidates) {
245
+ try {
246
+ fs2.mkdirSync(dir2, { recursive: true });
247
+ return dir2;
248
+ } catch {
249
+ }
250
+ }
251
+ const dir = path2.resolve(process.cwd(), "wasm");
252
+ fs2.mkdirSync(dir, { recursive: true });
253
+ return dir;
254
+ }
255
+ var wasmDir = null;
256
+ function cacheDir() {
257
+ if (!wasmDir) wasmDir = resolveWasmDir();
258
+ return wasmDir;
259
+ }
260
+ function cachePath(spec) {
261
+ return path2.join(cacheDir(), spec.wasm);
262
+ }
263
+ function looksLikeWasm(bytes) {
264
+ return bytes.length >= 4 && bytes[0] === 0 && bytes[1] === 97 && bytes[2] === 115 && bytes[3] === 109;
265
+ }
266
+ var inflight = /* @__PURE__ */ new Map();
267
+ function download(spec) {
268
+ const pending = inflight.get(spec.wasm);
269
+ if (pending) return pending;
270
+ const job = (async () => {
271
+ const res = await fetch(spec.url, { redirect: "follow" });
272
+ if (!res.ok) {
273
+ throw new Error(
274
+ `Failed to download grammar "${spec.wasm}" from ${spec.url} (HTTP ${res.status})`
275
+ );
276
+ }
277
+ const bytes = new Uint8Array(await res.arrayBuffer());
278
+ if (!looksLikeWasm(bytes)) {
279
+ throw new Error(
280
+ `Downloaded grammar "${spec.wasm}" is not valid WebAssembly (got HTML/redirect).`
281
+ );
282
+ }
283
+ const target = cachePath(spec);
284
+ const tmp = `${target}.${process.pid}.tmp`;
285
+ fs2.writeFileSync(tmp, bytes);
286
+ fs2.renameSync(tmp, target);
287
+ inflight.delete(spec.wasm);
288
+ return bytes;
289
+ })().catch((err) => {
290
+ inflight.delete(spec.wasm);
291
+ throw err;
292
+ });
293
+ inflight.set(spec.wasm, job);
294
+ return job;
295
+ }
296
+ async function getGrammar(spec, force = false) {
297
+ if (!force && fs2.existsSync(cachePath(spec))) {
298
+ const cached = fs2.readFileSync(cachePath(spec));
299
+ if (looksLikeWasm(cached)) return cached;
300
+ }
301
+ return download(spec);
302
+ }
303
+ async function warmGrammars(specs = []) {
304
+ const registryModule = await import("./registry-JLP6X4QB.js");
305
+ const list = specs.length > 0 ? specs : Object.values(registryModule.registry);
306
+ const unique = /* @__PURE__ */ new Map();
307
+ for (const spec of list) unique.set(spec.wasm, spec);
308
+ const results = await Promise.allSettled(
309
+ [...unique.values()].map((spec) => getGrammar(spec))
310
+ );
311
+ return results.filter((r) => r.status === "fulfilled").length;
312
+ }
313
+
314
+ // src/parser/symbols.ts
315
+ var KIND_BY_TYPE = {
316
+ // TypeScript / JavaScript / TSX / JSX
317
+ function_declaration: "function",
318
+ generator_function_declaration: "function",
319
+ function_expression: "function",
320
+ function_signature_item: "function",
321
+ method_definition: "method",
322
+ method_signature: "method",
323
+ class_declaration: "class",
324
+ abstract_class_declaration: "class",
325
+ interface_declaration: "interface",
326
+ enum_declaration: "enum",
327
+ type_alias_declaration: "type",
328
+ // Python
329
+ function_definition: "function",
330
+ class_definition: "class",
331
+ // Go
332
+ method_declaration: "method",
333
+ type_spec: "type",
334
+ // Rust
335
+ function_item: "function",
336
+ struct_item: "class",
337
+ enum_item: "enum",
338
+ trait_item: "interface",
339
+ type_item: "type",
340
+ // Java / Kotlin / PHP / Dart / Swift (best-effort generic names)
341
+ constructor_declaration: "method",
342
+ module_declaration: "class",
343
+ protocol_declaration: "interface"
344
+ };
345
+ var SIGNATURE_MAX = 200;
346
+ function collectSymbols(root, source) {
347
+ const out = [];
348
+ const walk2 = (node) => {
349
+ const kind = KIND_BY_TYPE[node.type];
350
+ if (kind) {
351
+ const nameNode = node.childForFieldName("name");
352
+ if (nameNode && nameNode.text.trim()) {
353
+ out.push(symbolFrom(nameNode.text.trim(), kind, node.startIndex, node.endIndex, source));
354
+ }
355
+ } else if (node.type === "variable_declarator") {
356
+ const value = node.childForFieldName("value");
357
+ const valueType = value?.type;
358
+ if (valueType === "arrow_function" || valueType === "function_expression") {
359
+ const nameNode = node.childForFieldName("name");
360
+ if (nameNode && nameNode.text.trim()) {
361
+ out.push(
362
+ symbolFrom(nameNode.text.trim(), "arrow", node.startIndex, node.endIndex, source)
363
+ );
364
+ }
365
+ }
366
+ }
367
+ for (let i = 0; i < node.childCount; i++) {
368
+ const child = node.child(i);
369
+ if (child) walk2(child);
370
+ }
371
+ };
372
+ walk2(root);
373
+ return out;
374
+ }
375
+ function matchSymbols(symbols, query, kind) {
376
+ const q = query.trim();
377
+ if (!q) return [];
378
+ const pool = kind ? symbols.filter((s) => s.kind === kind) : symbols;
379
+ const byName = pool.filter((s) => s.name === q);
380
+ if (byName.length > 0) return byName;
381
+ const byNameCi = pool.filter((s) => s.name.toLowerCase() === q.toLowerCase());
382
+ if (byNameCi.length > 0) return byNameCi;
383
+ const lower = q.toLowerCase();
384
+ const bySubstring = pool.filter((s) => s.name.toLowerCase().includes(lower));
385
+ return bySubstring.length > 0 ? bySubstring : pool.filter((s) => (s.signature ?? "").toLowerCase().includes(lower));
386
+ }
387
+ function symbolFrom(name, kind, start, end, source) {
388
+ return {
389
+ name,
390
+ kind,
391
+ line: source.slice(0, start).split("\n").length,
392
+ start,
393
+ end,
394
+ signature: signaturePreview(source, start, end)
395
+ };
396
+ }
397
+ function signaturePreview(source, start, end) {
398
+ const nl = source.indexOf("\n", start);
399
+ const endOfLine = nl === -1 ? end : nl;
400
+ const first = source.slice(start, Math.min(endOfLine, end)).trim();
401
+ return first.length > SIGNATURE_MAX ? `${first.slice(0, SIGNATURE_MAX)}\u2026` : first;
402
+ }
403
+
404
+ // src/parser/analyze.ts
405
+ import Parser from "web-tree-sitter";
406
+ import { createRequire } from "module";
407
+ import path3 from "path";
408
+ var languageCache = /* @__PURE__ */ new Map();
409
+ var initPromise = null;
410
+ function ensureParserInit() {
411
+ if (!initPromise) {
412
+ initPromise = Parser.init({
413
+ locateFile: (file) => {
414
+ const require2 = createRequire(__filename);
415
+ try {
416
+ const pkgRoot = path3.dirname(require2.resolve("web-tree-sitter/package.json"));
417
+ return path3.join(pkgRoot, file);
418
+ } catch {
419
+ return file;
420
+ }
421
+ }
422
+ });
423
+ }
424
+ return initPromise;
425
+ }
426
+ function loadLanguage(spec, force = false) {
427
+ if (!force) {
428
+ const cached = languageCache.get(spec.wasm);
429
+ if (cached) return Promise.resolve(cached);
430
+ }
431
+ if (force) languageCache.delete(spec.wasm);
432
+ return (async () => {
433
+ await ensureParserInit();
434
+ const grammar = await getGrammar(spec, force);
435
+ const language = await Parser.Language.load(grammar);
436
+ languageCache.set(spec.wasm, language);
437
+ return language;
438
+ })();
439
+ }
440
+ function spliceRanges(source, ranges) {
441
+ let removed = 0;
442
+ const sorted = [...ranges].sort((a, b) => b.start - a.start);
443
+ let out = source;
444
+ for (const r of sorted) {
445
+ if (r.start < 0 || r.end > out.length || r.end < r.start) continue;
446
+ removed += r.end - r.start;
447
+ out = out.slice(0, r.start) + r.token + out.slice(r.end);
448
+ }
449
+ return { code: out, removed };
450
+ }
451
+ async function analyze(filePath, source, opts = {}) {
452
+ const spec = languageForFile(filePath);
453
+ if (!spec || spec.rules.length === 0) {
454
+ return { code: source, language: null, removed: 0, symbols: [] };
455
+ }
456
+ let language;
457
+ try {
458
+ language = await loadLanguage(spec, opts.forceDownload);
459
+ } catch {
460
+ return { code: source, language: null, removed: 0, symbols: [] };
461
+ }
462
+ const parser = new Parser();
463
+ parser.setLanguage(language);
464
+ const tree = parser.parse(source);
465
+ try {
466
+ const symbols = collectSymbols(tree.rootNode, source);
467
+ if (opts.skipPrune) {
468
+ return { code: source, language: spec.name, removed: 0, symbols };
469
+ }
470
+ const keepRanges = [
471
+ ...opts.keepBlocksInside ?? [],
472
+ ...opts.preserveAnnotations?.length ? annotatedKeepRanges(symbols, source, opts.preserveAnnotations) : []
473
+ ];
474
+ const isProtected = (start, end) => keepRanges.some((k) => k.start <= start && end <= k.end);
475
+ const ranges = [];
476
+ for (const rule of spec.rules) {
477
+ const query = language.query(rule.query);
478
+ const captures = query.captures(tree.rootNode);
479
+ for (const cap of captures) {
480
+ if (cap.node.startIndex === cap.node.endIndex) continue;
481
+ if (rule.replacement.keepIf?.test(cap.node.text)) continue;
482
+ if (isProtected(cap.node.startIndex, cap.node.endIndex)) 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
+ const { code, removed } = spliceRanges(source, ranges);
492
+ return { code, language: spec.name, removed, symbols };
493
+ } finally {
494
+ tree.delete();
495
+ parser.delete();
496
+ }
497
+ }
498
+ async function prune(filePath, source, opts = {}) {
499
+ const res = await analyze(filePath, source, opts);
500
+ return { code: res.code, language: res.language, removed: res.removed };
501
+ }
502
+ var ANNOTATION_WINDOW = 240;
503
+ function annotatedKeepRanges(symbols, source, annotations) {
504
+ if (annotations.length === 0) return [];
505
+ const markers = annotations.filter(Boolean).map(escapeRegExp);
506
+ if (markers.length === 0) return [];
507
+ const re = new RegExp(`@(${markers.join("|")})\\b`, "i");
508
+ return symbols.filter((s) => {
509
+ const windowStart = Math.max(0, s.start - ANNOTATION_WINDOW);
510
+ const tail = source.slice(windowStart, s.start);
511
+ const cut = Math.max(tail.lastIndexOf("}\n"), tail.lastIndexOf(";\n"));
512
+ const relevant = cut === -1 ? tail : tail.slice(cut + 1);
513
+ return re.test(relevant);
514
+ }).map((s) => ({ start: s.start, end: s.end }));
515
+ }
516
+ function escapeRegExp(text) {
517
+ return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
518
+ }
519
+
520
+ // src/config.ts
521
+ import fs3 from "fs";
522
+ import path4 from "path";
523
+ var CONFIG_FILE_NAME = ".tokenshrinkrc.json";
524
+ var EMPTY_CONFIG = {
525
+ ignorePatterns: [],
526
+ keepUnpruned: [],
527
+ preserveAnnotations: []
528
+ };
529
+ function configPathFor(root) {
530
+ return path4.join(root, CONFIG_FILE_NAME);
531
+ }
532
+ function loadConfig(root, warn) {
533
+ const file = configPathFor(root);
534
+ let raw;
535
+ try {
536
+ if (!fs3.existsSync(file)) return { ...EMPTY_CONFIG };
537
+ raw = JSON.parse(fs3.readFileSync(file, "utf8"));
538
+ } catch (err) {
539
+ warn?.(`Invalid ${CONFIG_FILE_NAME} at ${file}: ${err.message}`);
540
+ return { ...EMPTY_CONFIG };
541
+ }
542
+ return normalizeConfig(raw, warn, file);
543
+ }
544
+ function normalizeConfig(raw, warn, file) {
545
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
546
+ warn?.(`Invalid ${CONFIG_FILE_NAME}${file ? ` at ${file}` : ""}: expected a JSON object.`);
547
+ return { ...EMPTY_CONFIG };
548
+ }
549
+ const obj = raw;
550
+ const asStrings = (v) => Array.isArray(v) ? v.filter((x) => typeof x === "string") : [];
551
+ return {
552
+ ignorePatterns: asStrings(obj.ignorePatterns),
553
+ keepUnpruned: asStrings(obj.keepUnpruned),
554
+ preserveAnnotations: asStrings(obj.preserveAnnotations),
555
+ ...typeof obj.autoWorkflow === "boolean" ? { autoWorkflow: obj.autoWorkflow } : {}
556
+ };
557
+ }
558
+ function matchesAny(file, patterns) {
559
+ const rel2 = normalizeSlashes(file);
560
+ for (const raw of patterns) {
561
+ if (!raw) continue;
562
+ const pattern = normalizeSlashes(raw).replace(/^\.\//, "");
563
+ if (matchesGlob(rel2, pattern)) return true;
564
+ }
565
+ return false;
566
+ }
567
+ function matchesGlob(file, pattern) {
568
+ if (file === pattern) return true;
569
+ if (!/[?*[]/.test(pattern)) {
570
+ return file.endsWith(`/${pattern}`);
571
+ }
572
+ const re = globToRegExp(pattern);
573
+ return re.test(file);
574
+ }
575
+ function globToRegExp(glob) {
576
+ let re = "^";
577
+ for (let i = 0; i < glob.length; i++) {
578
+ const c = glob[i];
579
+ if (c === "*") {
580
+ if (glob[i + 1] === "*") {
581
+ i++;
582
+ if (glob[i + 1] === "/") {
583
+ i++;
584
+ re += "(?:.*/)?";
585
+ } else {
586
+ re += ".*";
587
+ }
588
+ } else {
589
+ re += "[^/]*";
590
+ }
591
+ } else if (c === "?") {
592
+ re += "[^/]";
593
+ } else if (c === "[") {
594
+ const close = glob.indexOf("]", i);
595
+ if (close === -1) {
596
+ re += "\\[";
597
+ } else {
598
+ const inner = glob.slice(i + 1, close).replace(/\\/g, "\\\\");
599
+ re += `[${inner}]`;
600
+ i = close;
601
+ }
602
+ } else {
603
+ re += c.replace(/[.+^${}()|\\]/g, "\\$&");
604
+ }
605
+ }
606
+ re += "$";
607
+ return new RegExp(re);
608
+ }
609
+ function normalizeSlashes(p) {
610
+ return p.split(path4.sep).join("/").replace(/^\.\//, "");
611
+ }
612
+
613
+ // src/watcher/sync.ts
614
+ import fs4 from "fs";
615
+ import path5 from "path";
616
+ import { createHash } from "crypto";
617
+ import chokidar from "chokidar";
618
+ var DEFAULT_IGNORED = [
619
+ /(^|[/\\])\.[^/\\]+/,
620
+ // dotfiles / dot-dirs (.git, .cursor, .env, ...)
621
+ /node_modules/,
622
+ /[/\\](build|dist|out|coverage|\.next|\.turbo|\.cache)[/\\]/
623
+ ];
624
+ var ContextCache = class {
625
+ entries = /* @__PURE__ */ new Map();
626
+ initPromise = null;
627
+ ensureInit() {
628
+ if (!this.initPromise) {
629
+ this.initPromise = ensureParserInit();
630
+ }
631
+ return this.initPromise;
632
+ }
633
+ /** Returns the cached skeleton for a file, if present. */
634
+ getSkeleton(filePath) {
635
+ return this.entries.get(path5.resolve(filePath)) ?? null;
636
+ }
637
+ get imports() {
638
+ return this.entries;
639
+ }
640
+ };
641
+ function hashOf(text) {
642
+ return createHash("sha1").update(text).digest("hex");
643
+ }
644
+ function extractImports(_filePath, source) {
645
+ const out = [];
646
+ const re = /(?:from\s+['"`]([^'"`]+)['"`]|import\s+['"`]([^'"`]+)['"`]|require\s*\(\s*['"]([^'"]+)['"]\s*\))/g;
647
+ let m;
648
+ while ((m = re.exec(source)) !== null) {
649
+ const spec = m[1] ?? m[2] ?? m[3];
650
+ if (spec) out.push(spec);
651
+ }
652
+ return [...new Set(out)].filter(Boolean);
653
+ }
654
+ function resolveImport(importer, specifier, root) {
655
+ if (!/^[.~@]/.test(specifier)) {
656
+ if (specifier.startsWith("@/")) {
657
+ return resolveCandidate(path5.join(root, specifier.slice(2)));
658
+ }
659
+ if (specifier.startsWith("@")) {
660
+ return null;
661
+ }
662
+ if (specifier.startsWith("~")) {
663
+ return resolveCandidate(path5.join(root, specifier.slice(1)));
664
+ }
665
+ return null;
666
+ }
667
+ const base = path5.dirname(path5.resolve(importer));
668
+ return resolveCandidate(path5.resolve(base, specifier));
669
+ }
670
+ function resolveCandidate(p) {
671
+ const candidates = [
672
+ p,
673
+ `${p}.ts`,
674
+ `${p}.tsx`,
675
+ `${p}.js`,
676
+ `${p}.jsx`,
677
+ `${p}.mjs`,
678
+ `${p}.cjs`,
679
+ `${p}.py`,
680
+ `${p}.go`,
681
+ `${p}.rs`,
682
+ `${p}.dart`,
683
+ `${p}.swift`,
684
+ `${p}.java`,
685
+ `${p}.kt`,
686
+ `${p}.c`,
687
+ `${p}.cpp`,
688
+ `${p}.h`,
689
+ `${p}.hpp`,
690
+ `${p}.php`,
691
+ path5.join(p, "index.ts"),
692
+ path5.join(p, "index.js"),
693
+ path5.join(p, "index.tsx"),
694
+ path5.join(p, "index.jsx"),
695
+ path5.join(p, "index.py")
696
+ ];
697
+ for (const c of candidates) {
698
+ if (fs4.existsSync(c) && fs4.statSync(c).isFile()) return path5.resolve(c);
699
+ }
700
+ return null;
701
+ }
702
+ function createWatcher(opts) {
703
+ const cache = new ContextCache();
704
+ const { root, ignored = DEFAULT_IGNORED, config, onIndexed, onRemoved } = opts;
705
+ const rel2 = (abs) => path5.relative(root, abs);
706
+ const effectiveIgnored = [
707
+ ...ignored,
708
+ ...config?.ignorePatterns?.length ? [(_abs) => matchesAny(rel2(String(_abs)), config.ignorePatterns)] : []
709
+ ];
710
+ const pending = /* @__PURE__ */ new Map();
711
+ const DEBOUNCE_MS = 100;
712
+ async function handle(filePath) {
713
+ const abs = path5.resolve(filePath);
714
+ let source;
715
+ try {
716
+ const st = fs4.statSync(abs);
717
+ if (!st.isFile()) return;
718
+ source = fs4.readFileSync(abs, "utf8");
719
+ } catch {
720
+ return;
721
+ }
722
+ const hash = hashOf(source);
723
+ const cached = cache.entries.get(abs);
724
+ if (cached && cached.hash === hash) return;
725
+ await cache.ensureInit();
726
+ const keepFull = config?.keepUnpruned?.length ? matchesAny(path5.relative(root, abs), config.keepUnpruned) : false;
727
+ const { code, language, symbols } = await analyze(abs, source, {
728
+ skipPrune: keepFull,
729
+ preserveAnnotations: config?.preserveAnnotations
730
+ });
731
+ const imports = extractImports(abs, source);
732
+ const entry = {
733
+ hash,
734
+ skeleton: code,
735
+ language,
736
+ imports: imports.map((spec) => resolveImport(abs, spec, root)).filter((p) => p !== null),
737
+ ...symbols.length > 0 ? { symbols } : {}
738
+ };
739
+ cache.entries.set(abs, entry);
740
+ onIndexed?.(abs, entry);
741
+ }
742
+ const watcher = chokidar.watch(root, {
743
+ // Combine path matchers with a stat check so non-regular files (e.g. unix
744
+ // sockets) are never opened with fs.watch (which raises UVException).
745
+ ignored(_p, stats) {
746
+ if (stats && !stats.isFile() && !stats.isDirectory()) return true;
747
+ return isIgnored(path5.resolve(String(_p)), effectiveIgnored);
748
+ },
749
+ alwaysStat: true,
750
+ ignoreInitial: true,
751
+ persistent: true,
752
+ awaitWriteFinish: { stabilityThreshold: 50, pollInterval: 10 }
753
+ });
754
+ function debounce(filePath) {
755
+ const abs = path5.resolve(filePath);
756
+ const existing = pending.get(abs);
757
+ if (existing) clearTimeout(existing);
758
+ if (!existing) void handle(abs);
759
+ pending.set(
760
+ abs,
761
+ setTimeout(() => pending.delete(abs), DEBOUNCE_MS)
762
+ );
763
+ }
764
+ watcher.on("add", debounce);
765
+ watcher.on("change", debounce);
766
+ watcher.on("unlink", (filePath) => {
767
+ const abs = path5.resolve(filePath);
768
+ cache.entries.delete(abs);
769
+ onRemoved?.(abs);
770
+ });
771
+ return {
772
+ cache,
773
+ watcher,
774
+ /** Index an existing file right now (bypasses the watcher). */
775
+ index: handle,
776
+ /**
777
+ * Index the whole tree once (cold start). Returns the number of files
778
+ * successfully indexed.
779
+ */
780
+ async indexAll() {
781
+ const files = [];
782
+ await walk(root, (f) => files.push(f), effectiveIgnored);
783
+ let ok = 0;
784
+ for (const f of files) {
785
+ try {
786
+ await handle(f);
787
+ ok++;
788
+ } catch {
789
+ }
790
+ }
791
+ return ok;
792
+ },
793
+ close: () => watcher.close()
794
+ };
795
+ }
796
+ async function walk(root, push, ignored) {
797
+ const entries = await fs4.promises.readdir(root, { withFileTypes: true });
798
+ for (const e of entries) {
799
+ const abs = path5.join(root, e.name);
800
+ if (isIgnored(abs, ignored)) continue;
801
+ if (e.isDirectory()) {
802
+ await walk(abs, push, ignored);
803
+ } else if (e.isFile()) {
804
+ push(abs);
805
+ }
806
+ }
807
+ }
808
+ function isIgnored(abs, ignored) {
809
+ for (const m of ignored) {
810
+ if (typeof m === "function") {
811
+ if (m(abs)) return true;
812
+ continue;
813
+ }
814
+ if (typeof m === "string" && abs.includes(m)) return true;
815
+ if (m instanceof RegExp && m.test(abs)) return true;
816
+ }
817
+ return false;
818
+ }
819
+
820
+ export {
821
+ assembleMany,
822
+ assemble,
823
+ extractSpecifiers,
824
+ approximateTokens,
825
+ getGrammar,
826
+ warmGrammars,
827
+ collectSymbols,
828
+ matchSymbols,
829
+ ensureParserInit,
830
+ loadLanguage,
831
+ spliceRanges,
832
+ analyze,
833
+ prune,
834
+ CONFIG_FILE_NAME,
835
+ EMPTY_CONFIG,
836
+ configPathFor,
837
+ loadConfig,
838
+ matchesAny,
839
+ matchesGlob,
840
+ globToRegExp,
841
+ DEFAULT_IGNORED,
842
+ hashOf,
843
+ extractImports,
844
+ resolveImport,
845
+ createWatcher
846
+ };
847
+ //# sourceMappingURL=chunk-FYCLLG7F.js.map