trace-mcp 1.0.11 → 1.1.0

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.js CHANGED
@@ -619,8 +619,8 @@ var require_utils = __commonJS({
619
619
  }
620
620
  return output;
621
621
  };
622
- exports.basename = (path82, { windows } = {}) => {
623
- const segs = path82.split(windows ? /[\\/]/ : "/");
622
+ exports.basename = (path83, { windows } = {}) => {
623
+ const segs = path83.split(windows ? /[\\/]/ : "/");
624
624
  const last = segs[segs.length - 1];
625
625
  if (last === "") {
626
626
  return segs[segs.length - 2];
@@ -2087,8 +2087,8 @@ var require_picomatch = __commonJS({
2087
2087
  try {
2088
2088
  const opts = options || {};
2089
2089
  return new RegExp(source, opts.flags || (opts.nocase ? "i" : ""));
2090
- } catch (err31) {
2091
- if (options && options.debug === true) throw err31;
2090
+ } catch (err32) {
2091
+ if (options && options.debug === true) throw err32;
2092
2092
  return /$^/;
2093
2093
  }
2094
2094
  };
@@ -2116,7 +2116,7 @@ var require_picomatch2 = __commonJS({
2116
2116
 
2117
2117
  // src/cli.ts
2118
2118
  import { Command as Command7 } from "commander";
2119
- import path81 from "path";
2119
+ import path82 from "path";
2120
2120
  import fs72 from "fs";
2121
2121
  import { createRequire as createRequire21 } from "module";
2122
2122
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
@@ -3149,14 +3149,14 @@ var Store = class {
3149
3149
  db;
3150
3150
  _stmts;
3151
3151
  // --- Files ---
3152
- insertFile(path82, language, contentHash, byteLength, workspace) {
3153
- const result = this._stmts.insertFile.run(path82, language, contentHash, byteLength, workspace ?? null);
3152
+ insertFile(path83, language, contentHash, byteLength, workspace) {
3153
+ const result = this._stmts.insertFile.run(path83, language, contentHash, byteLength, workspace ?? null);
3154
3154
  const fileId = Number(result.lastInsertRowid);
3155
3155
  this.createNode("file", fileId);
3156
3156
  return fileId;
3157
3157
  }
3158
- getFile(path82) {
3159
- return this._stmts.getFile.get(path82);
3158
+ getFile(path83) {
3159
+ return this._stmts.getFile.get(path83);
3160
3160
  }
3161
3161
  getFileById(id) {
3162
3162
  return this._stmts.getFileById.get(id);
@@ -6370,6 +6370,564 @@ var GitignoreMatcher = class {
6370
6370
  }
6371
6371
  };
6372
6372
 
6373
+ // src/tools/history.ts
6374
+ import { execFileSync as execFileSync2 } from "child_process";
6375
+
6376
+ // src/tools/git-analysis.ts
6377
+ import { execFileSync } from "child_process";
6378
+ function isGitRepo(cwd) {
6379
+ try {
6380
+ execFileSync("git", ["rev-parse", "--is-inside-work-tree"], {
6381
+ cwd,
6382
+ stdio: "pipe",
6383
+ timeout: 5e3
6384
+ });
6385
+ return true;
6386
+ } catch {
6387
+ return false;
6388
+ }
6389
+ }
6390
+ function getGitFileStats(cwd, sinceDays) {
6391
+ const args = [
6392
+ "log",
6393
+ "--pretty=format:__COMMIT__%H|%aI|%aN",
6394
+ "--name-only",
6395
+ "--no-merges",
6396
+ "--diff-filter=ACDMR"
6397
+ ];
6398
+ if (sinceDays !== void 0) {
6399
+ args.push(`--since=${sinceDays} days ago`);
6400
+ }
6401
+ let output;
6402
+ try {
6403
+ output = execFileSync("git", args, {
6404
+ cwd,
6405
+ stdio: "pipe",
6406
+ maxBuffer: 10 * 1024 * 1024,
6407
+ // 10 MB
6408
+ timeout: 3e4
6409
+ }).toString("utf-8");
6410
+ } catch (e) {
6411
+ logger.warn({ error: e }, "git log failed");
6412
+ return /* @__PURE__ */ new Map();
6413
+ }
6414
+ const fileStats = /* @__PURE__ */ new Map();
6415
+ let currentDate = null;
6416
+ let currentAuthor = null;
6417
+ for (const line of output.split("\n")) {
6418
+ if (line.startsWith("__COMMIT__")) {
6419
+ const parts = line.slice("__COMMIT__".length).split("|");
6420
+ currentDate = new Date(parts[1]);
6421
+ currentAuthor = parts[2];
6422
+ continue;
6423
+ }
6424
+ const trimmed = line.trim();
6425
+ if (!trimmed || !currentDate || !currentAuthor) continue;
6426
+ const existing = fileStats.get(trimmed);
6427
+ if (existing) {
6428
+ existing.commits++;
6429
+ existing.authors.add(currentAuthor);
6430
+ if (currentDate < existing.firstDate) existing.firstDate = currentDate;
6431
+ if (currentDate > existing.lastDate) existing.lastDate = currentDate;
6432
+ } else {
6433
+ fileStats.set(trimmed, {
6434
+ file: trimmed,
6435
+ commits: 1,
6436
+ authors: /* @__PURE__ */ new Set([currentAuthor]),
6437
+ firstDate: currentDate,
6438
+ lastDate: currentDate
6439
+ });
6440
+ }
6441
+ }
6442
+ return fileStats;
6443
+ }
6444
+ function getChurnRate(cwd, options = {}) {
6445
+ const { sinceDays, limit = 50, filePattern } = options;
6446
+ if (!isGitRepo(cwd)) {
6447
+ return [];
6448
+ }
6449
+ const stats = getGitFileStats(cwd, sinceDays);
6450
+ let entries = [];
6451
+ for (const [file, data] of stats) {
6452
+ if (filePattern && !file.includes(filePattern)) continue;
6453
+ const lifespanMs = data.lastDate.getTime() - data.firstDate.getTime();
6454
+ const lifespanWeeks = Math.max(lifespanMs / (7 * 24 * 60 * 60 * 1e3), 1);
6455
+ const churnPerWeek = Math.round(data.commits / lifespanWeeks * 100) / 100;
6456
+ let assessment;
6457
+ if (churnPerWeek <= 1) assessment = "stable";
6458
+ else if (churnPerWeek <= 3) assessment = "active";
6459
+ else assessment = "volatile";
6460
+ entries.push({
6461
+ file,
6462
+ commits: data.commits,
6463
+ unique_authors: data.authors.size,
6464
+ first_seen: data.firstDate.toISOString().split("T")[0],
6465
+ last_modified: data.lastDate.toISOString().split("T")[0],
6466
+ churn_per_week: churnPerWeek,
6467
+ assessment
6468
+ });
6469
+ }
6470
+ entries.sort((a, b) => b.commits - a.commits);
6471
+ return entries.slice(0, limit);
6472
+ }
6473
+ function getHotspots(store, cwd, options = {}) {
6474
+ const { sinceDays = 90, limit = 20, minCyclomatic = 3 } = options;
6475
+ if (!isGitRepo(cwd)) {
6476
+ return getComplexityOnlyHotspots(store, limit, minCyclomatic);
6477
+ }
6478
+ const gitStats = getGitFileStats(cwd, sinceDays);
6479
+ const fileComplexity = getMaxCyclomaticPerFile(store);
6480
+ const entries = [];
6481
+ for (const [file, maxCyclomatic] of fileComplexity) {
6482
+ if (maxCyclomatic < minCyclomatic) continue;
6483
+ const git = gitStats.get(file);
6484
+ const commits = git?.commits ?? 0;
6485
+ const score = Math.round(maxCyclomatic * Math.log(1 + commits) * 100) / 100;
6486
+ if (score <= 0) continue;
6487
+ let assessment;
6488
+ if (score <= 3) assessment = "low";
6489
+ else if (score <= 10) assessment = "medium";
6490
+ else assessment = "high";
6491
+ entries.push({
6492
+ file,
6493
+ max_cyclomatic: maxCyclomatic,
6494
+ commits,
6495
+ score,
6496
+ assessment
6497
+ });
6498
+ }
6499
+ entries.sort((a, b) => b.score - a.score);
6500
+ return entries.slice(0, limit);
6501
+ }
6502
+ function getMaxCyclomaticPerFile(store) {
6503
+ const rows = store.db.prepare(`
6504
+ SELECT f.path, MAX(s.cyclomatic) as max_cyclomatic
6505
+ FROM symbols s
6506
+ JOIN files f ON s.file_id = f.id
6507
+ WHERE s.cyclomatic IS NOT NULL
6508
+ GROUP BY f.path
6509
+ `).all();
6510
+ const result = /* @__PURE__ */ new Map();
6511
+ for (const row of rows) {
6512
+ result.set(row.path, row.max_cyclomatic);
6513
+ }
6514
+ return result;
6515
+ }
6516
+ function getComplexityOnlyHotspots(store, limit, minCyclomatic) {
6517
+ const fileComplexity = getMaxCyclomaticPerFile(store);
6518
+ const entries = [];
6519
+ for (const [file, maxCyclomatic] of fileComplexity) {
6520
+ if (maxCyclomatic < minCyclomatic) continue;
6521
+ entries.push({
6522
+ file,
6523
+ max_cyclomatic: maxCyclomatic,
6524
+ commits: 0,
6525
+ score: maxCyclomatic,
6526
+ // score = complexity alone
6527
+ assessment: maxCyclomatic <= 3 ? "low" : maxCyclomatic <= 10 ? "medium" : "high"
6528
+ });
6529
+ }
6530
+ entries.sort((a, b) => b.score - a.score);
6531
+ return entries.slice(0, limit);
6532
+ }
6533
+
6534
+ // src/tools/history.ts
6535
+ function sampleFileCommits(cwd, filePath, sinceDays, count) {
6536
+ const args = [
6537
+ "log",
6538
+ "--pretty=format:%H|%aI",
6539
+ "--follow",
6540
+ "--no-merges",
6541
+ `--max-count=${count * 3}`
6542
+ ];
6543
+ if (sinceDays !== void 0) {
6544
+ args.push(`--since=${sinceDays} days ago`);
6545
+ }
6546
+ args.push("--", filePath);
6547
+ let output;
6548
+ try {
6549
+ output = execFileSync2("git", args, {
6550
+ cwd,
6551
+ stdio: "pipe",
6552
+ timeout: 1e4
6553
+ }).toString("utf-8");
6554
+ } catch {
6555
+ return [];
6556
+ }
6557
+ const all = output.split("\n").filter(Boolean).map((line) => {
6558
+ const [hash, date] = line.split("|");
6559
+ return { hash, date: date.split("T")[0] };
6560
+ });
6561
+ if (all.length <= count) return all;
6562
+ const step = Math.max(1, Math.floor((all.length - 1) / (count - 1)));
6563
+ const sampled = [];
6564
+ for (let i = 0; i < all.length && sampled.length < count; i += step) {
6565
+ sampled.push(all[i]);
6566
+ }
6567
+ if (sampled[sampled.length - 1] !== all[all.length - 1]) {
6568
+ sampled.push(all[all.length - 1]);
6569
+ }
6570
+ return sampled;
6571
+ }
6572
+ function getFileAtCommit(cwd, filePath, commitHash) {
6573
+ try {
6574
+ return execFileSync2("git", ["show", `${commitHash}:${filePath}`], {
6575
+ cwd,
6576
+ stdio: "pipe",
6577
+ timeout: 1e4,
6578
+ maxBuffer: 5 * 1024 * 1024
6579
+ }).toString("utf-8");
6580
+ } catch {
6581
+ return null;
6582
+ }
6583
+ }
6584
+ function countImports(content) {
6585
+ let count = 0;
6586
+ for (const line of content.split("\n")) {
6587
+ const t = line.trim();
6588
+ if (/^import\s+/.test(t) && /['"]/.test(t)) {
6589
+ count++;
6590
+ continue;
6591
+ }
6592
+ if (/\brequire\s*\(\s*['"]/.test(t)) {
6593
+ count++;
6594
+ continue;
6595
+ }
6596
+ if (/^(?:from\s+\S+\s+import|import\s+\S+)/.test(t)) {
6597
+ count++;
6598
+ continue;
6599
+ }
6600
+ if (/^import\s+"/.test(t)) {
6601
+ count++;
6602
+ continue;
6603
+ }
6604
+ if (/^use\s+[A-Z\\]/.test(t)) {
6605
+ count++;
6606
+ continue;
6607
+ }
6608
+ }
6609
+ return count;
6610
+ }
6611
+ function countImportersAtCommit(cwd, filePath, commitHash) {
6612
+ const searchPattern = filePath.replace(/\.[^.]+$/, "").replace(/\/index$/, "");
6613
+ if (searchPattern.length < 8) return 0;
6614
+ try {
6615
+ const output = execFileSync2("git", [
6616
+ "grep",
6617
+ "-l",
6618
+ "--fixed-strings",
6619
+ searchPattern,
6620
+ commitHash,
6621
+ "--"
6622
+ ], {
6623
+ cwd,
6624
+ stdio: "pipe",
6625
+ timeout: 15e3,
6626
+ maxBuffer: 2 * 1024 * 1024
6627
+ }).toString("utf-8");
6628
+ let count = 0;
6629
+ for (const line of output.split("\n")) {
6630
+ if (!line.trim()) continue;
6631
+ const colonIdx = line.indexOf(":");
6632
+ const file = colonIdx >= 0 ? line.slice(colonIdx + 1) : line;
6633
+ if (file !== filePath) count++;
6634
+ }
6635
+ return count;
6636
+ } catch {
6637
+ return 0;
6638
+ }
6639
+ }
6640
+ function getCouplingTrend(store, cwd, filePath, options = {}) {
6641
+ const { sinceDays = 90, snapshots = 6 } = options;
6642
+ if (!isGitRepo(cwd)) return null;
6643
+ const file = store.getFile(filePath);
6644
+ if (!file) return null;
6645
+ const currentCoupling = getCurrentCoupling(store, file.id);
6646
+ const currentSnapshot = {
6647
+ date: (/* @__PURE__ */ new Date()).toISOString().split("T")[0],
6648
+ commit: "HEAD",
6649
+ ...currentCoupling
6650
+ };
6651
+ const commits = sampleFileCommits(cwd, filePath, sinceDays, snapshots);
6652
+ const historical = [];
6653
+ for (const { hash, date } of commits) {
6654
+ try {
6655
+ const content = getFileAtCommit(cwd, filePath, hash);
6656
+ if (!content) continue;
6657
+ const ce = countImports(content);
6658
+ const ca = countImportersAtCommit(cwd, filePath, hash);
6659
+ const total = ca + ce;
6660
+ const instability = total === 0 ? 0 : Math.round(ce / total * 1e3) / 1e3;
6661
+ historical.push({ date, commit: hash.slice(0, 8), ca, ce, instability });
6662
+ } catch (e) {
6663
+ logger.debug({ file: filePath, commit: hash, error: e }, "Coupling snapshot failed");
6664
+ }
6665
+ }
6666
+ let trend = "stable";
6667
+ let instabilityDelta = 0;
6668
+ let couplingDelta = 0;
6669
+ if (historical.length > 0) {
6670
+ const oldest = historical[historical.length - 1];
6671
+ instabilityDelta = Math.round((currentSnapshot.instability - oldest.instability) * 1e3) / 1e3;
6672
+ couplingDelta = currentSnapshot.ca + currentSnapshot.ce - (oldest.ca + oldest.ce);
6673
+ if (instabilityDelta > 0.1) trend = "destabilizing";
6674
+ else if (instabilityDelta < -0.1) trend = "stabilizing";
6675
+ }
6676
+ return {
6677
+ file: filePath,
6678
+ current: currentSnapshot,
6679
+ historical,
6680
+ trend,
6681
+ instability_delta: instabilityDelta,
6682
+ coupling_delta: couplingDelta
6683
+ };
6684
+ }
6685
+ function getCurrentCoupling(store, fileId) {
6686
+ const row = store.db.prepare(`
6687
+ WITH file_edges AS (
6688
+ SELECT
6689
+ CASE WHEN n1.node_type = 'file' THEN n1.ref_id ELSE s1.file_id END AS src_file,
6690
+ CASE WHEN n2.node_type = 'file' THEN n2.ref_id ELSE s2.file_id END AS tgt_file
6691
+ FROM edges e
6692
+ JOIN edge_types et ON e.edge_type_id = et.id
6693
+ JOIN nodes n1 ON e.source_node_id = n1.id
6694
+ JOIN nodes n2 ON e.target_node_id = n2.id
6695
+ LEFT JOIN symbols s1 ON n1.node_type = 'symbol' AND n1.ref_id = s1.id
6696
+ LEFT JOIN symbols s2 ON n2.node_type = 'symbol' AND n2.ref_id = s2.id
6697
+ WHERE et.name IN ('esm_imports', 'imports', 'py_imports', 'py_reexports')
6698
+ AND (
6699
+ (CASE WHEN n1.node_type = 'file' THEN n1.ref_id ELSE s1.file_id END) = ?
6700
+ OR (CASE WHEN n2.node_type = 'file' THEN n2.ref_id ELSE s2.file_id END) = ?
6701
+ )
6702
+ )
6703
+ SELECT
6704
+ COUNT(DISTINCT CASE WHEN src_file = ? AND tgt_file != ? THEN tgt_file END) AS ce,
6705
+ COUNT(DISTINCT CASE WHEN tgt_file = ? AND src_file != ? THEN src_file END) AS ca
6706
+ FROM file_edges
6707
+ `).get(fileId, fileId, fileId, fileId, fileId, fileId);
6708
+ const ca = row?.ca ?? 0;
6709
+ const ce = row?.ce ?? 0;
6710
+ const total = ca + ce;
6711
+ const instability = total === 0 ? 0 : Math.round(ce / total * 1e3) / 1e3;
6712
+ return { ca, ce, instability };
6713
+ }
6714
+ function stripStringsForBraces(source) {
6715
+ let s = source.replace(/\/\*[\s\S]*?\*\//g, "");
6716
+ s = s.replace(/\/\/.*$/gm, "");
6717
+ s = s.replace(/#.*$/gm, "");
6718
+ s = s.replace(/`[^`]*`/g, '""');
6719
+ s = s.replace(/"(?:[^"\\]|\\.)*"/g, '""');
6720
+ s = s.replace(/'(?:[^'\\]|\\.)*'/g, "''");
6721
+ return s;
6722
+ }
6723
+ function extractSymbolSource(content, symbolName, symbolKind) {
6724
+ const lines = content.split("\n");
6725
+ const patterns = [];
6726
+ const escaped = symbolName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
6727
+ if (symbolKind === "function" || symbolKind === "method") {
6728
+ patterns.push(
6729
+ new RegExp(`(?:export\\s+)?(?:async\\s+)?function\\s+${escaped}\\s*[(<]`),
6730
+ new RegExp(`(?:public|private|protected)\\s+(?:async\\s+)?(?:static\\s+)?${escaped}\\s*\\(`),
6731
+ new RegExp(`(?:export\\s+)?(?:const|let|var)\\s+${escaped}\\s*=\\s*(?:async\\s+)?(?:\\([^)]*\\)|\\w+)\\s*=>`),
6732
+ new RegExp(`\\b${escaped}\\s*:\\s*(?:async\\s+)?function`),
6733
+ new RegExp(`\\bdef\\s+${escaped}\\s*\\(`),
6734
+ new RegExp(`\\bfunc\\s+${escaped}\\s*\\(`)
6735
+ );
6736
+ } else if (symbolKind === "class") {
6737
+ patterns.push(
6738
+ new RegExp(`(?:export\\s+)?(?:abstract\\s+)?class\\s+${escaped}\\b`),
6739
+ new RegExp(`\\bclass\\s+${escaped}\\b`)
6740
+ );
6741
+ } else {
6742
+ patterns.push(new RegExp(`\\b${escaped}\\b`));
6743
+ }
6744
+ let startLine = -1;
6745
+ for (let i = 0; i < lines.length; i++) {
6746
+ if (patterns.some((p4) => p4.test(lines[i]))) {
6747
+ startLine = i;
6748
+ break;
6749
+ }
6750
+ }
6751
+ if (startLine === -1) return null;
6752
+ const cleanLines = stripStringsForBraces(content).split("\n");
6753
+ let depth = 0;
6754
+ let foundOpenBrace = false;
6755
+ let endLine = startLine;
6756
+ for (let i = startLine; i < cleanLines.length; i++) {
6757
+ for (const ch of cleanLines[i]) {
6758
+ if (ch === "{") {
6759
+ depth++;
6760
+ foundOpenBrace = true;
6761
+ } else if (ch === "}") {
6762
+ depth--;
6763
+ }
6764
+ }
6765
+ endLine = i;
6766
+ if (foundOpenBrace && depth <= 0) break;
6767
+ if (!foundOpenBrace && i > startLine && lines[i].trim() !== "") {
6768
+ const startIndent = lines[startLine].match(/^\s*/)?.[0].length ?? 0;
6769
+ const currentIndent = lines[i].match(/^\s*/)?.[0].length ?? 0;
6770
+ if (currentIndent <= startIndent && i > startLine + 1) {
6771
+ endLine = i - 1;
6772
+ break;
6773
+ }
6774
+ }
6775
+ }
6776
+ const source = lines.slice(startLine, endLine + 1).join("\n");
6777
+ const signature = lines[startLine].trim();
6778
+ return { source, signature };
6779
+ }
6780
+ function getSymbolComplexityTrend(store, cwd, symbolId, options = {}) {
6781
+ const { sinceDays, snapshots = 6 } = options;
6782
+ if (!isGitRepo(cwd)) return null;
6783
+ const sym = store.db.prepare(`
6784
+ SELECT s.symbol_id, s.name, s.kind, s.fqn, s.signature,
6785
+ s.cyclomatic, s.max_nesting, s.param_count,
6786
+ s.line_start, s.line_end,
6787
+ f.path
6788
+ FROM symbols s
6789
+ JOIN files f ON s.file_id = f.id
6790
+ WHERE s.symbol_id = ?
6791
+ `).get(symbolId);
6792
+ if (!sym) return null;
6793
+ const currentLines = sym.line_end && sym.line_start ? sym.line_end - sym.line_start + 1 : 0;
6794
+ const currentSnapshot = {
6795
+ date: (/* @__PURE__ */ new Date()).toISOString().split("T")[0],
6796
+ commit: "HEAD",
6797
+ cyclomatic: sym.cyclomatic ?? 1,
6798
+ max_nesting: sym.max_nesting ?? 0,
6799
+ param_count: sym.param_count ?? 0,
6800
+ lines: currentLines
6801
+ };
6802
+ const commits = sampleFileCommits(cwd, sym.path, sinceDays, snapshots);
6803
+ const historical = [];
6804
+ for (const { hash, date } of commits) {
6805
+ const content = getFileAtCommit(cwd, sym.path, hash);
6806
+ if (!content) continue;
6807
+ try {
6808
+ const extracted = extractSymbolSource(content, sym.name, sym.kind);
6809
+ if (!extracted) continue;
6810
+ const cyclomatic = computeCyclomatic(extracted.source);
6811
+ const maxNesting = computeMaxNesting(extracted.source);
6812
+ const paramCount = computeParamCount(extracted.signature);
6813
+ const lineCount = extracted.source.split("\n").length;
6814
+ historical.push({
6815
+ date,
6816
+ commit: hash.slice(0, 8),
6817
+ cyclomatic,
6818
+ max_nesting: maxNesting,
6819
+ param_count: paramCount,
6820
+ lines: lineCount
6821
+ });
6822
+ } catch (e) {
6823
+ logger.debug({ symbol: symbolId, commit: hash, error: e }, "Symbol complexity snapshot failed");
6824
+ }
6825
+ }
6826
+ let trend = "stable";
6827
+ let cyclomaticDelta = 0;
6828
+ if (historical.length > 0) {
6829
+ const oldest = historical[historical.length - 1];
6830
+ cyclomaticDelta = currentSnapshot.cyclomatic - oldest.cyclomatic;
6831
+ if (cyclomaticDelta >= 2) trend = "degrading";
6832
+ else if (cyclomaticDelta <= -2) trend = "improving";
6833
+ }
6834
+ return {
6835
+ symbol_id: sym.symbol_id,
6836
+ name: sym.name,
6837
+ file: sym.path,
6838
+ current: currentSnapshot,
6839
+ historical,
6840
+ trend,
6841
+ cyclomatic_delta: cyclomaticDelta
6842
+ };
6843
+ }
6844
+ function captureGraphSnapshots(store, cwd) {
6845
+ let commitHash;
6846
+ try {
6847
+ commitHash = execFileSync2("git", ["rev-parse", "--short", "HEAD"], {
6848
+ cwd,
6849
+ stdio: "pipe",
6850
+ timeout: 5e3
6851
+ }).toString("utf-8").trim();
6852
+ } catch {
6853
+ }
6854
+ if (commitHash) {
6855
+ const existing = store.db.prepare(
6856
+ "SELECT id FROM graph_snapshots WHERE commit_hash = ? AND snapshot_type = 'coupling_summary' LIMIT 1"
6857
+ ).get(commitHash);
6858
+ if (existing) return;
6859
+ }
6860
+ const allFiles = store.getAllFiles();
6861
+ if (allFiles.length === 0) return;
6862
+ const allFileIds = allFiles.map((f) => f.id);
6863
+ const fileNodeMap = /* @__PURE__ */ new Map();
6864
+ const CHUNK = 500;
6865
+ for (let i = 0; i < allFileIds.length; i += CHUNK) {
6866
+ const chunk = allFileIds.slice(i, i + CHUNK);
6867
+ for (const [k, v] of store.getNodeIdsBatch("file", chunk)) {
6868
+ fileNodeMap.set(k, v);
6869
+ }
6870
+ }
6871
+ const importsTypeRow = store.db.prepare(
6872
+ "SELECT id FROM edge_types WHERE name = 'imports'"
6873
+ ).get();
6874
+ if (!importsTypeRow) return;
6875
+ const allImportEdges = store.db.prepare(
6876
+ "SELECT source_node_id, target_node_id FROM edges WHERE edge_type_id = ?"
6877
+ ).all(importsTypeRow.id);
6878
+ const nodeToFileId = /* @__PURE__ */ new Map();
6879
+ for (const [fileId, nodeId] of fileNodeMap) {
6880
+ nodeToFileId.set(nodeId, fileId);
6881
+ }
6882
+ const fileCa = /* @__PURE__ */ new Map();
6883
+ const fileCe = /* @__PURE__ */ new Map();
6884
+ for (const edge of allImportEdges) {
6885
+ const srcFileId = nodeToFileId.get(edge.source_node_id);
6886
+ const tgtFileId = nodeToFileId.get(edge.target_node_id);
6887
+ if (srcFileId != null) fileCe.set(srcFileId, (fileCe.get(srcFileId) ?? 0) + 1);
6888
+ if (tgtFileId != null) fileCa.set(tgtFileId, (fileCa.get(tgtFileId) ?? 0) + 1);
6889
+ }
6890
+ const fileIdToPath = new Map(allFiles.map((f) => [f.id, f.path]));
6891
+ const insertStmt = store.db.prepare(
6892
+ "INSERT INTO graph_snapshots (commit_hash, snapshot_type, file_path, data) VALUES (?, ?, ?, ?)"
6893
+ );
6894
+ let count = 0;
6895
+ store.db.transaction(() => {
6896
+ for (const fileId of allFileIds) {
6897
+ const ca = fileCa.get(fileId) ?? 0;
6898
+ const ce = fileCe.get(fileId) ?? 0;
6899
+ if (ca === 0 && ce === 0) continue;
6900
+ const total = ca + ce;
6901
+ const instability = Math.round(ce / total * 1e3) / 1e3;
6902
+ const filePath = fileIdToPath.get(fileId);
6903
+ if (!filePath) continue;
6904
+ insertStmt.run(commitHash ?? null, "coupling", filePath, JSON.stringify({ ca, ce, instability }));
6905
+ count++;
6906
+ }
6907
+ if (count > 0) {
6908
+ const totalInstability = allFileIds.reduce((sum, fid) => {
6909
+ const ca = fileCa.get(fid) ?? 0;
6910
+ const ce = fileCe.get(fid) ?? 0;
6911
+ const total = ca + ce;
6912
+ return sum + (total === 0 ? 0 : ce / total);
6913
+ }, 0);
6914
+ insertStmt.run(
6915
+ commitHash ?? null,
6916
+ "coupling_summary",
6917
+ null,
6918
+ JSON.stringify({
6919
+ total_files: allFiles.length,
6920
+ files_with_edges: count,
6921
+ avg_instability: Math.round(totalInstability / allFiles.length * 1e3) / 1e3
6922
+ })
6923
+ );
6924
+ }
6925
+ })();
6926
+ if (count > 0) {
6927
+ logger.info({ files: count, commit: commitHash }, "Coupling snapshots captured");
6928
+ }
6929
+ }
6930
+
6373
6931
  // src/indexer/pipeline.ts
6374
6932
  var IndexingPipeline = class _IndexingPipeline {
6375
6933
  constructor(store, registry, config, rootPath) {
@@ -6463,9 +7021,9 @@ var IndexingPipeline = class _IndexingPipeline {
6463
7021
  this.registerFrameworkEdgeTypes();
6464
7022
  try {
6465
7023
  {
6466
- const BATCH_SIZE2 = 100;
6467
- for (let i = 0; i < relPaths.length; i += BATCH_SIZE2) {
6468
- const batch = relPaths.slice(i, i + BATCH_SIZE2);
7024
+ const BATCH_SIZE3 = 100;
7025
+ for (let i = 0; i < relPaths.length; i += BATCH_SIZE3) {
7026
+ const batch = relPaths.slice(i, i + BATCH_SIZE3);
6469
7027
  const extractions = [];
6470
7028
  for (const relPath of batch) {
6471
7029
  const ext = await this.extractFile(relPath, force);
@@ -7014,7 +7572,16 @@ var IndexingPipeline = class _IndexingPipeline {
7014
7572
  const absSource = path10.resolve(this.rootPath, file.path);
7015
7573
  const sourceNodeId = fileNodeMap.get(fileId);
7016
7574
  if (sourceNodeId == null) continue;
7575
+ const consolidated = /* @__PURE__ */ new Map();
7017
7576
  for (const { from, specifiers } of imports) {
7577
+ const existing = consolidated.get(from);
7578
+ if (existing) {
7579
+ existing.push(...specifiers);
7580
+ } else {
7581
+ consolidated.set(from, [...specifiers]);
7582
+ }
7583
+ }
7584
+ for (const [from, specifiers] of consolidated) {
7018
7585
  if (!from.startsWith(".") && !from.startsWith("/") && !from.startsWith("@/") && !from.startsWith("~")) continue;
7019
7586
  const resolved = resolver.resolve(from, absSource);
7020
7587
  if (!resolved) continue;
@@ -10248,6 +10815,7 @@ function isTestFile(filePath) {
10248
10815
 
10249
10816
  // src/tools/introspect.ts
10250
10817
  init_graph_analysis();
10818
+ var TEST_FIXTURE_RE = /(?:^|\/)(?:tests?|__tests__|spec)\/fixtures?\//;
10251
10819
  function safeParseMeta(raw) {
10252
10820
  if (!raw) return {};
10253
10821
  try {
@@ -10415,7 +10983,7 @@ function walkDescendants(store, name, result, visited, depth) {
10415
10983
  }
10416
10984
  }
10417
10985
  function getDeadExports(store, filePattern) {
10418
- const exported = store.getExportedSymbols(filePattern);
10986
+ const exported = store.getExportedSymbols(filePattern).filter((s) => !TEST_FIXTURE_RE.test(s.file_path));
10419
10987
  const importedNames = /* @__PURE__ */ new Set();
10420
10988
  const importEdges = store.getEdgesByType("imports");
10421
10989
  for (const edge of importEdges) {
@@ -10490,7 +11058,7 @@ function getDependencyGraph(store, filePath) {
10490
11058
  return { file: filePath, imports, imported_by: importedBy };
10491
11059
  }
10492
11060
  function getUntestedExports(store, filePattern) {
10493
- const exported = store.getExportedSymbols(filePattern).filter((s) => s.kind !== "method");
11061
+ const exported = store.getExportedSymbols(filePattern).filter((s) => s.kind !== "method").filter((s) => !TEST_FIXTURE_RE.test(s.file_path));
10494
11062
  const allFiles = store.getAllFiles();
10495
11063
  const testFiles = allFiles.filter((f) => /\.(test|spec)\.(ts|js|tsx|jsx)$/.test(f.path)).map((f) => f.path.toLowerCase());
10496
11064
  const untested = [];
@@ -10619,164 +11187,6 @@ function selfAudit(store) {
10619
11187
  // src/server.ts
10620
11188
  init_graph_analysis();
10621
11189
 
10622
- // src/tools/git-analysis.ts
10623
- import { execFileSync } from "child_process";
10624
- function isGitRepo(cwd) {
10625
- try {
10626
- execFileSync("git", ["rev-parse", "--is-inside-work-tree"], {
10627
- cwd,
10628
- stdio: "pipe",
10629
- timeout: 5e3
10630
- });
10631
- return true;
10632
- } catch {
10633
- return false;
10634
- }
10635
- }
10636
- function getGitFileStats(cwd, sinceDays) {
10637
- const args = [
10638
- "log",
10639
- "--pretty=format:__COMMIT__%H|%aI|%aN",
10640
- "--name-only",
10641
- "--no-merges",
10642
- "--diff-filter=ACDMR"
10643
- ];
10644
- if (sinceDays !== void 0) {
10645
- args.push(`--since=${sinceDays} days ago`);
10646
- }
10647
- let output;
10648
- try {
10649
- output = execFileSync("git", args, {
10650
- cwd,
10651
- stdio: "pipe",
10652
- maxBuffer: 10 * 1024 * 1024,
10653
- // 10 MB
10654
- timeout: 3e4
10655
- }).toString("utf-8");
10656
- } catch (e) {
10657
- logger.warn({ error: e }, "git log failed");
10658
- return /* @__PURE__ */ new Map();
10659
- }
10660
- const fileStats = /* @__PURE__ */ new Map();
10661
- let currentDate = null;
10662
- let currentAuthor = null;
10663
- for (const line of output.split("\n")) {
10664
- if (line.startsWith("__COMMIT__")) {
10665
- const parts = line.slice("__COMMIT__".length).split("|");
10666
- currentDate = new Date(parts[1]);
10667
- currentAuthor = parts[2];
10668
- continue;
10669
- }
10670
- const trimmed = line.trim();
10671
- if (!trimmed || !currentDate || !currentAuthor) continue;
10672
- const existing = fileStats.get(trimmed);
10673
- if (existing) {
10674
- existing.commits++;
10675
- existing.authors.add(currentAuthor);
10676
- if (currentDate < existing.firstDate) existing.firstDate = currentDate;
10677
- if (currentDate > existing.lastDate) existing.lastDate = currentDate;
10678
- } else {
10679
- fileStats.set(trimmed, {
10680
- file: trimmed,
10681
- commits: 1,
10682
- authors: /* @__PURE__ */ new Set([currentAuthor]),
10683
- firstDate: currentDate,
10684
- lastDate: currentDate
10685
- });
10686
- }
10687
- }
10688
- return fileStats;
10689
- }
10690
- function getChurnRate(cwd, options = {}) {
10691
- const { sinceDays, limit = 50, filePattern } = options;
10692
- if (!isGitRepo(cwd)) {
10693
- return [];
10694
- }
10695
- const stats = getGitFileStats(cwd, sinceDays);
10696
- let entries = [];
10697
- for (const [file, data] of stats) {
10698
- if (filePattern && !file.includes(filePattern)) continue;
10699
- const lifespanMs = data.lastDate.getTime() - data.firstDate.getTime();
10700
- const lifespanWeeks = Math.max(lifespanMs / (7 * 24 * 60 * 60 * 1e3), 1);
10701
- const churnPerWeek = Math.round(data.commits / lifespanWeeks * 100) / 100;
10702
- let assessment;
10703
- if (churnPerWeek <= 1) assessment = "stable";
10704
- else if (churnPerWeek <= 3) assessment = "active";
10705
- else assessment = "volatile";
10706
- entries.push({
10707
- file,
10708
- commits: data.commits,
10709
- unique_authors: data.authors.size,
10710
- first_seen: data.firstDate.toISOString().split("T")[0],
10711
- last_modified: data.lastDate.toISOString().split("T")[0],
10712
- churn_per_week: churnPerWeek,
10713
- assessment
10714
- });
10715
- }
10716
- entries.sort((a, b) => b.commits - a.commits);
10717
- return entries.slice(0, limit);
10718
- }
10719
- function getHotspots(store, cwd, options = {}) {
10720
- const { sinceDays = 90, limit = 20, minCyclomatic = 3 } = options;
10721
- if (!isGitRepo(cwd)) {
10722
- return getComplexityOnlyHotspots(store, limit, minCyclomatic);
10723
- }
10724
- const gitStats = getGitFileStats(cwd, sinceDays);
10725
- const fileComplexity = getMaxCyclomaticPerFile(store);
10726
- const entries = [];
10727
- for (const [file, maxCyclomatic] of fileComplexity) {
10728
- if (maxCyclomatic < minCyclomatic) continue;
10729
- const git = gitStats.get(file);
10730
- const commits = git?.commits ?? 0;
10731
- const score = Math.round(maxCyclomatic * Math.log(1 + commits) * 100) / 100;
10732
- if (score <= 0) continue;
10733
- let assessment;
10734
- if (score <= 3) assessment = "low";
10735
- else if (score <= 10) assessment = "medium";
10736
- else assessment = "high";
10737
- entries.push({
10738
- file,
10739
- max_cyclomatic: maxCyclomatic,
10740
- commits,
10741
- score,
10742
- assessment
10743
- });
10744
- }
10745
- entries.sort((a, b) => b.score - a.score);
10746
- return entries.slice(0, limit);
10747
- }
10748
- function getMaxCyclomaticPerFile(store) {
10749
- const rows = store.db.prepare(`
10750
- SELECT f.path, MAX(s.cyclomatic) as max_cyclomatic
10751
- FROM symbols s
10752
- JOIN files f ON s.file_id = f.id
10753
- WHERE s.cyclomatic IS NOT NULL
10754
- GROUP BY f.path
10755
- `).all();
10756
- const result = /* @__PURE__ */ new Map();
10757
- for (const row of rows) {
10758
- result.set(row.path, row.max_cyclomatic);
10759
- }
10760
- return result;
10761
- }
10762
- function getComplexityOnlyHotspots(store, limit, minCyclomatic) {
10763
- const fileComplexity = getMaxCyclomaticPerFile(store);
10764
- const entries = [];
10765
- for (const [file, maxCyclomatic] of fileComplexity) {
10766
- if (maxCyclomatic < minCyclomatic) continue;
10767
- entries.push({
10768
- file,
10769
- max_cyclomatic: maxCyclomatic,
10770
- commits: 0,
10771
- score: maxCyclomatic,
10772
- // score = complexity alone
10773
- assessment: maxCyclomatic <= 3 ? "low" : maxCyclomatic <= 10 ? "medium" : "high"
10774
- });
10775
- }
10776
- entries.sort((a, b) => b.score - a.score);
10777
- return entries.slice(0, limit);
10778
- }
10779
-
10780
11190
  // src/tools/dead-code.ts
10781
11191
  import path17 from "path";
10782
11192
  var BARREL_PATTERNS = [
@@ -10850,7 +11260,8 @@ function buildBarrelExportedNames(store) {
10850
11260
  }
10851
11261
  function getDeadCodeV2(store, options = {}) {
10852
11262
  const { filePattern, threshold = 0.5, limit = 50 } = options;
10853
- const exported = store.getExportedSymbols(filePattern).filter((s) => s.kind !== "method");
11263
+ const TEST_FIXTURE_RE2 = /(?:^|\/)(?:tests?|__tests__|spec)\/fixtures?\//;
11264
+ const exported = store.getExportedSymbols(filePattern).filter((s) => s.kind !== "method").filter((s) => !TEST_FIXTURE_RE2.test(s.file_path));
10854
11265
  const importedNames = buildImportedNamesSet(store);
10855
11266
  const referencedNodeIds = buildReferencedSymbolIds(store);
10856
11267
  const barrelExportedNames = buildBarrelExportedNames(store);
@@ -11484,13 +11895,13 @@ function getIndent(line) {
11484
11895
  init_layer_violations();
11485
11896
 
11486
11897
  // src/tools/git-ownership.ts
11487
- import { execFileSync as execFileSync2 } from "child_process";
11898
+ import { execFileSync as execFileSync3 } from "child_process";
11488
11899
  function getFileOwnership(cwd, filePaths) {
11489
11900
  if (!isGitRepo(cwd)) return [];
11490
11901
  const results = [];
11491
11902
  for (const filePath of filePaths) {
11492
11903
  try {
11493
- const output = execFileSync2("git", [
11904
+ const output = execFileSync3("git", [
11494
11905
  "shortlog",
11495
11906
  "-sn",
11496
11907
  "--no-merges",
@@ -11534,7 +11945,7 @@ function getSymbolOwnership(store, cwd, symbolId) {
11534
11945
  const file = store.getFileById(symbol.file_id);
11535
11946
  if (!file) return null;
11536
11947
  try {
11537
- const output = execFileSync2("git", [
11948
+ const output = execFileSync3("git", [
11538
11949
  "blame",
11539
11950
  "--porcelain",
11540
11951
  `-L${symbol.line_start},${symbol.line_end}`,
@@ -11578,10 +11989,10 @@ function getSymbolOwnership(store, cwd, symbolId) {
11578
11989
  }
11579
11990
 
11580
11991
  // src/tools/complexity-trend.ts
11581
- import { execFileSync as execFileSync3 } from "child_process";
11992
+ import { execFileSync as execFileSync4 } from "child_process";
11582
11993
  function getHistoricalCommits(cwd, filePath, count) {
11583
11994
  try {
11584
- const output = execFileSync3("git", [
11995
+ const output = execFileSync4("git", [
11585
11996
  "log",
11586
11997
  "--pretty=format:%H|%aI",
11587
11998
  "--follow",
@@ -11612,9 +12023,9 @@ function getHistoricalCommits(cwd, filePath, count) {
11612
12023
  return [];
11613
12024
  }
11614
12025
  }
11615
- function getFileAtCommit(cwd, filePath, commitHash) {
12026
+ function getFileAtCommit2(cwd, filePath, commitHash) {
11616
12027
  try {
11617
- return execFileSync3("git", ["show", `${commitHash}:${filePath}`], {
12028
+ return execFileSync4("git", ["show", `${commitHash}:${filePath}`], {
11618
12029
  cwd,
11619
12030
  stdio: "pipe",
11620
12031
  timeout: 1e4
@@ -11695,7 +12106,7 @@ function getComplexityTrend(store, cwd, filePath, options = {}) {
11695
12106
  const commits = getHistoricalCommits(cwd, filePath, snapshots);
11696
12107
  const historical = [];
11697
12108
  for (const { hash, date } of commits) {
11698
- const content = getFileAtCommit(cwd, filePath, hash);
12109
+ const content = getFileAtCommit2(cwd, filePath, hash);
11699
12110
  if (!content) continue;
11700
12111
  try {
11701
12112
  historical.push(computeSnapshot(content, hash, date));
@@ -11720,318 +12131,6 @@ function getComplexityTrend(store, cwd, filePath, options = {}) {
11720
12131
  };
11721
12132
  }
11722
12133
 
11723
- // src/tools/history.ts
11724
- import { execFileSync as execFileSync4 } from "child_process";
11725
- function sampleFileCommits(cwd, filePath, sinceDays, count) {
11726
- const args = [
11727
- "log",
11728
- "--pretty=format:%H|%aI",
11729
- "--follow",
11730
- "--no-merges",
11731
- `--max-count=${count * 3}`
11732
- ];
11733
- if (sinceDays !== void 0) {
11734
- args.push(`--since=${sinceDays} days ago`);
11735
- }
11736
- args.push("--", filePath);
11737
- let output;
11738
- try {
11739
- output = execFileSync4("git", args, {
11740
- cwd,
11741
- stdio: "pipe",
11742
- timeout: 1e4
11743
- }).toString("utf-8");
11744
- } catch {
11745
- return [];
11746
- }
11747
- const all = output.split("\n").filter(Boolean).map((line) => {
11748
- const [hash, date] = line.split("|");
11749
- return { hash, date: date.split("T")[0] };
11750
- });
11751
- if (all.length <= count) return all;
11752
- const step = Math.max(1, Math.floor((all.length - 1) / (count - 1)));
11753
- const sampled = [];
11754
- for (let i = 0; i < all.length && sampled.length < count; i += step) {
11755
- sampled.push(all[i]);
11756
- }
11757
- if (sampled[sampled.length - 1] !== all[all.length - 1]) {
11758
- sampled.push(all[all.length - 1]);
11759
- }
11760
- return sampled;
11761
- }
11762
- function getFileAtCommit2(cwd, filePath, commitHash) {
11763
- try {
11764
- return execFileSync4("git", ["show", `${commitHash}:${filePath}`], {
11765
- cwd,
11766
- stdio: "pipe",
11767
- timeout: 1e4,
11768
- maxBuffer: 5 * 1024 * 1024
11769
- }).toString("utf-8");
11770
- } catch {
11771
- return null;
11772
- }
11773
- }
11774
- function countImports(content) {
11775
- let count = 0;
11776
- for (const line of content.split("\n")) {
11777
- const t = line.trim();
11778
- if (/^import\s+/.test(t) && /['"]/.test(t)) {
11779
- count++;
11780
- continue;
11781
- }
11782
- if (/\brequire\s*\(\s*['"]/.test(t)) {
11783
- count++;
11784
- continue;
11785
- }
11786
- if (/^(?:from\s+\S+\s+import|import\s+\S+)/.test(t)) {
11787
- count++;
11788
- continue;
11789
- }
11790
- if (/^import\s+"/.test(t)) {
11791
- count++;
11792
- continue;
11793
- }
11794
- if (/^use\s+[A-Z\\]/.test(t)) {
11795
- count++;
11796
- continue;
11797
- }
11798
- }
11799
- return count;
11800
- }
11801
- function countImportersAtCommit(cwd, filePath, commitHash) {
11802
- const searchPattern = filePath.replace(/\.[^.]+$/, "").replace(/\/index$/, "");
11803
- if (searchPattern.length < 8) return 0;
11804
- try {
11805
- const output = execFileSync4("git", [
11806
- "grep",
11807
- "-l",
11808
- "--fixed-strings",
11809
- searchPattern,
11810
- commitHash,
11811
- "--"
11812
- ], {
11813
- cwd,
11814
- stdio: "pipe",
11815
- timeout: 15e3,
11816
- maxBuffer: 2 * 1024 * 1024
11817
- }).toString("utf-8");
11818
- let count = 0;
11819
- for (const line of output.split("\n")) {
11820
- if (!line.trim()) continue;
11821
- const colonIdx = line.indexOf(":");
11822
- const file = colonIdx >= 0 ? line.slice(colonIdx + 1) : line;
11823
- if (file !== filePath) count++;
11824
- }
11825
- return count;
11826
- } catch {
11827
- return 0;
11828
- }
11829
- }
11830
- function getCouplingTrend(store, cwd, filePath, options = {}) {
11831
- const { sinceDays = 90, snapshots = 6 } = options;
11832
- if (!isGitRepo(cwd)) return null;
11833
- const file = store.getFile(filePath);
11834
- if (!file) return null;
11835
- const currentCoupling = getCurrentCoupling(store, file.id);
11836
- const currentSnapshot = {
11837
- date: (/* @__PURE__ */ new Date()).toISOString().split("T")[0],
11838
- commit: "HEAD",
11839
- ...currentCoupling
11840
- };
11841
- const commits = sampleFileCommits(cwd, filePath, sinceDays, snapshots);
11842
- const historical = [];
11843
- for (const { hash, date } of commits) {
11844
- try {
11845
- const content = getFileAtCommit2(cwd, filePath, hash);
11846
- if (!content) continue;
11847
- const ce = countImports(content);
11848
- const ca = countImportersAtCommit(cwd, filePath, hash);
11849
- const total = ca + ce;
11850
- const instability = total === 0 ? 0 : Math.round(ce / total * 1e3) / 1e3;
11851
- historical.push({ date, commit: hash.slice(0, 8), ca, ce, instability });
11852
- } catch (e) {
11853
- logger.debug({ file: filePath, commit: hash, error: e }, "Coupling snapshot failed");
11854
- }
11855
- }
11856
- let trend = "stable";
11857
- let instabilityDelta = 0;
11858
- let couplingDelta = 0;
11859
- if (historical.length > 0) {
11860
- const oldest = historical[historical.length - 1];
11861
- instabilityDelta = Math.round((currentSnapshot.instability - oldest.instability) * 1e3) / 1e3;
11862
- couplingDelta = currentSnapshot.ca + currentSnapshot.ce - (oldest.ca + oldest.ce);
11863
- if (instabilityDelta > 0.1) trend = "destabilizing";
11864
- else if (instabilityDelta < -0.1) trend = "stabilizing";
11865
- }
11866
- return {
11867
- file: filePath,
11868
- current: currentSnapshot,
11869
- historical,
11870
- trend,
11871
- instability_delta: instabilityDelta,
11872
- coupling_delta: couplingDelta
11873
- };
11874
- }
11875
- function getCurrentCoupling(store, fileId) {
11876
- const row = store.db.prepare(`
11877
- WITH file_edges AS (
11878
- SELECT
11879
- CASE WHEN n1.node_type = 'file' THEN n1.ref_id ELSE s1.file_id END AS src_file,
11880
- CASE WHEN n2.node_type = 'file' THEN n2.ref_id ELSE s2.file_id END AS tgt_file
11881
- FROM edges e
11882
- JOIN edge_types et ON e.edge_type_id = et.id
11883
- JOIN nodes n1 ON e.source_node_id = n1.id
11884
- JOIN nodes n2 ON e.target_node_id = n2.id
11885
- LEFT JOIN symbols s1 ON n1.node_type = 'symbol' AND n1.ref_id = s1.id
11886
- LEFT JOIN symbols s2 ON n2.node_type = 'symbol' AND n2.ref_id = s2.id
11887
- WHERE et.name IN ('esm_imports', 'imports', 'py_imports', 'py_reexports')
11888
- AND (
11889
- (CASE WHEN n1.node_type = 'file' THEN n1.ref_id ELSE s1.file_id END) = ?
11890
- OR (CASE WHEN n2.node_type = 'file' THEN n2.ref_id ELSE s2.file_id END) = ?
11891
- )
11892
- )
11893
- SELECT
11894
- COUNT(DISTINCT CASE WHEN src_file = ? AND tgt_file != ? THEN tgt_file END) AS ce,
11895
- COUNT(DISTINCT CASE WHEN tgt_file = ? AND src_file != ? THEN src_file END) AS ca
11896
- FROM file_edges
11897
- `).get(fileId, fileId, fileId, fileId, fileId, fileId);
11898
- const ca = row?.ca ?? 0;
11899
- const ce = row?.ce ?? 0;
11900
- const total = ca + ce;
11901
- const instability = total === 0 ? 0 : Math.round(ce / total * 1e3) / 1e3;
11902
- return { ca, ce, instability };
11903
- }
11904
- function stripStringsForBraces(source) {
11905
- let s = source.replace(/\/\*[\s\S]*?\*\//g, "");
11906
- s = s.replace(/\/\/.*$/gm, "");
11907
- s = s.replace(/#.*$/gm, "");
11908
- s = s.replace(/`[^`]*`/g, '""');
11909
- s = s.replace(/"(?:[^"\\]|\\.)*"/g, '""');
11910
- s = s.replace(/'(?:[^'\\]|\\.)*'/g, "''");
11911
- return s;
11912
- }
11913
- function extractSymbolSource(content, symbolName, symbolKind) {
11914
- const lines = content.split("\n");
11915
- const patterns = [];
11916
- const escaped = symbolName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
11917
- if (symbolKind === "function" || symbolKind === "method") {
11918
- patterns.push(
11919
- new RegExp(`(?:export\\s+)?(?:async\\s+)?function\\s+${escaped}\\s*[(<]`),
11920
- new RegExp(`(?:public|private|protected)\\s+(?:async\\s+)?(?:static\\s+)?${escaped}\\s*\\(`),
11921
- new RegExp(`(?:export\\s+)?(?:const|let|var)\\s+${escaped}\\s*=\\s*(?:async\\s+)?(?:\\([^)]*\\)|\\w+)\\s*=>`),
11922
- new RegExp(`\\b${escaped}\\s*:\\s*(?:async\\s+)?function`),
11923
- new RegExp(`\\bdef\\s+${escaped}\\s*\\(`),
11924
- new RegExp(`\\bfunc\\s+${escaped}\\s*\\(`)
11925
- );
11926
- } else if (symbolKind === "class") {
11927
- patterns.push(
11928
- new RegExp(`(?:export\\s+)?(?:abstract\\s+)?class\\s+${escaped}\\b`),
11929
- new RegExp(`\\bclass\\s+${escaped}\\b`)
11930
- );
11931
- } else {
11932
- patterns.push(new RegExp(`\\b${escaped}\\b`));
11933
- }
11934
- let startLine = -1;
11935
- for (let i = 0; i < lines.length; i++) {
11936
- if (patterns.some((p4) => p4.test(lines[i]))) {
11937
- startLine = i;
11938
- break;
11939
- }
11940
- }
11941
- if (startLine === -1) return null;
11942
- const cleanLines = stripStringsForBraces(content).split("\n");
11943
- let depth = 0;
11944
- let foundOpenBrace = false;
11945
- let endLine = startLine;
11946
- for (let i = startLine; i < cleanLines.length; i++) {
11947
- for (const ch of cleanLines[i]) {
11948
- if (ch === "{") {
11949
- depth++;
11950
- foundOpenBrace = true;
11951
- } else if (ch === "}") {
11952
- depth--;
11953
- }
11954
- }
11955
- endLine = i;
11956
- if (foundOpenBrace && depth <= 0) break;
11957
- if (!foundOpenBrace && i > startLine && lines[i].trim() !== "") {
11958
- const startIndent = lines[startLine].match(/^\s*/)?.[0].length ?? 0;
11959
- const currentIndent = lines[i].match(/^\s*/)?.[0].length ?? 0;
11960
- if (currentIndent <= startIndent && i > startLine + 1) {
11961
- endLine = i - 1;
11962
- break;
11963
- }
11964
- }
11965
- }
11966
- const source = lines.slice(startLine, endLine + 1).join("\n");
11967
- const signature = lines[startLine].trim();
11968
- return { source, signature };
11969
- }
11970
- function getSymbolComplexityTrend(store, cwd, symbolId, options = {}) {
11971
- const { sinceDays, snapshots = 6 } = options;
11972
- if (!isGitRepo(cwd)) return null;
11973
- const sym = store.db.prepare(`
11974
- SELECT s.symbol_id, s.name, s.kind, s.fqn, s.signature,
11975
- s.cyclomatic, s.max_nesting, s.param_count,
11976
- s.line_start, s.line_end,
11977
- f.path
11978
- FROM symbols s
11979
- JOIN files f ON s.file_id = f.id
11980
- WHERE s.symbol_id = ?
11981
- `).get(symbolId);
11982
- if (!sym) return null;
11983
- const currentLines = sym.line_end && sym.line_start ? sym.line_end - sym.line_start + 1 : 0;
11984
- const currentSnapshot = {
11985
- date: (/* @__PURE__ */ new Date()).toISOString().split("T")[0],
11986
- commit: "HEAD",
11987
- cyclomatic: sym.cyclomatic ?? 1,
11988
- max_nesting: sym.max_nesting ?? 0,
11989
- param_count: sym.param_count ?? 0,
11990
- lines: currentLines
11991
- };
11992
- const commits = sampleFileCommits(cwd, sym.path, sinceDays, snapshots);
11993
- const historical = [];
11994
- for (const { hash, date } of commits) {
11995
- const content = getFileAtCommit2(cwd, sym.path, hash);
11996
- if (!content) continue;
11997
- try {
11998
- const extracted = extractSymbolSource(content, sym.name, sym.kind);
11999
- if (!extracted) continue;
12000
- const cyclomatic = computeCyclomatic(extracted.source);
12001
- const maxNesting = computeMaxNesting(extracted.source);
12002
- const paramCount = computeParamCount(extracted.signature);
12003
- const lineCount = extracted.source.split("\n").length;
12004
- historical.push({
12005
- date,
12006
- commit: hash.slice(0, 8),
12007
- cyclomatic,
12008
- max_nesting: maxNesting,
12009
- param_count: paramCount,
12010
- lines: lineCount
12011
- });
12012
- } catch (e) {
12013
- logger.debug({ symbol: symbolId, commit: hash, error: e }, "Symbol complexity snapshot failed");
12014
- }
12015
- }
12016
- let trend = "stable";
12017
- let cyclomaticDelta = 0;
12018
- if (historical.length > 0) {
12019
- const oldest = historical[historical.length - 1];
12020
- cyclomaticDelta = currentSnapshot.cyclomatic - oldest.cyclomatic;
12021
- if (cyclomaticDelta >= 2) trend = "degrading";
12022
- else if (cyclomaticDelta <= -2) trend = "improving";
12023
- }
12024
- return {
12025
- symbol_id: sym.symbol_id,
12026
- name: sym.name,
12027
- file: sym.path,
12028
- current: currentSnapshot,
12029
- historical,
12030
- trend,
12031
- cyclomatic_delta: cyclomaticDelta
12032
- };
12033
- }
12034
-
12035
12134
  // src/tools/suggest.ts
12036
12135
  init_graph_analysis();
12037
12136
  function suggestQueries(store) {
@@ -14305,16 +14404,16 @@ function findShortestPath(store, startNodeId, endNodeId, maxDepth) {
14305
14404
  parent.set(nodeId, { from, edgeType: edge.edge_type_name });
14306
14405
  nextFrontier.push(nodeId);
14307
14406
  if (nodeId === endNodeId) {
14308
- const path82 = [endNodeId];
14407
+ const path83 = [endNodeId];
14309
14408
  const edgeTypes = [];
14310
14409
  let cur = endNodeId;
14311
14410
  while (cur !== startNodeId) {
14312
14411
  const p4 = parent.get(cur);
14313
- path82.unshift(p4.from);
14412
+ path83.unshift(p4.from);
14314
14413
  edgeTypes.unshift(p4.edgeType);
14315
14414
  cur = p4.from;
14316
14415
  }
14317
- return { path: path82, edgeTypes };
14416
+ return { path: path83, edgeTypes };
14318
14417
  }
14319
14418
  }
14320
14419
  }
@@ -14769,9 +14868,9 @@ var OtlpReceiver = class {
14769
14868
  if (this.options.port === 0) return;
14770
14869
  return new Promise((resolve, reject) => {
14771
14870
  this.server = createServer((req, res) => this.handleRequest(req, res));
14772
- this.server.on("error", (err31) => {
14773
- logger.error({ error: err31 }, "OTLP receiver error");
14774
- reject(err31);
14871
+ this.server.on("error", (err32) => {
14872
+ logger.error({ error: err32 }, "OTLP receiver error");
14873
+ reject(err32);
14775
14874
  });
14776
14875
  this.server.listen(this.options.port, this.options.host, () => {
14777
14876
  logger.info(
@@ -17828,6 +17927,20 @@ var hintGenerators = {
17828
17927
  hints.push({ tool: "scan_security", args: { rules: '["all"]' }, why: "Also check for security vulnerabilities" });
17829
17928
  return hints;
17830
17929
  },
17930
+ scan_code_smells(r) {
17931
+ const hints = [];
17932
+ const summary = dig(r, "summary");
17933
+ if (summary?.empty_function) {
17934
+ hints.push({ tool: "get_dead_code", why: "Empty functions may also be dead code \u2014 cross-check with dead code analysis" });
17935
+ }
17936
+ if (summary?.todo_comment) {
17937
+ hints.push({ tool: "get_tech_debt", why: "See overall tech debt score for modules with many TODOs" });
17938
+ }
17939
+ if (summary?.hardcoded_value) {
17940
+ hints.push({ tool: "scan_security", args: { rules: '["hardcoded_secrets"]' }, why: "Some hardcoded values may be security-sensitive" });
17941
+ }
17942
+ return hints;
17943
+ },
17831
17944
  get_page_rank(r) {
17832
17945
  const hints = [];
17833
17946
  const ranked = arr(r);
@@ -18723,9 +18836,321 @@ function detectAntipatterns(store, _projectRoot, opts = {}) {
18723
18836
  });
18724
18837
  }
18725
18838
 
18726
- // src/tools/sbom.ts
18727
- import { readFileSync as readFileSync2, existsSync } from "fs";
18839
+ // src/tools/code-smells.ts
18840
+ import { readFileSync as readFileSync2 } from "fs";
18728
18841
  import path29 from "path";
18842
+ var ALL_CATEGORIES2 = [
18843
+ "todo_comment",
18844
+ "empty_function",
18845
+ "hardcoded_value"
18846
+ ];
18847
+ var TODO_TAGS = [
18848
+ { tag: "FIXME", priority: "high" },
18849
+ { tag: "HACK", priority: "high" },
18850
+ { tag: "XXX", priority: "medium" },
18851
+ { tag: "TODO", priority: "medium" },
18852
+ { tag: "TEMP", priority: "medium" },
18853
+ { tag: "WORKAROUND", priority: "medium" },
18854
+ { tag: "BUG", priority: "high" },
18855
+ { tag: "REFACTOR", priority: "low" },
18856
+ { tag: "OPTIMIZE", priority: "low" },
18857
+ { tag: "NOTE", priority: "low" }
18858
+ ];
18859
+ var TODO_TAG_NAMES = TODO_TAGS.map((t) => t.tag).join("|");
18860
+ var TODO_REGEX = new RegExp(
18861
+ `(?://|#|/\\*|\\*|--|%|;|'|REM\\b)\\s*\\b(${TODO_TAG_NAMES})\\b[:\\s]?(.*)`,
18862
+ "i"
18863
+ );
18864
+ var TODO_TAG_PRIORITY = new Map(
18865
+ TODO_TAGS.map((t) => [t.tag, t.priority])
18866
+ );
18867
+ function detectTodoComments(lines, filePath) {
18868
+ const findings = [];
18869
+ for (let i = 0; i < lines.length; i++) {
18870
+ const line = lines[i];
18871
+ const m = TODO_REGEX.exec(line);
18872
+ if (!m) continue;
18873
+ const tag = m[1].toUpperCase();
18874
+ const message = (m[2] ?? "").trim();
18875
+ const priority = TODO_TAG_PRIORITY.get(tag) ?? "medium";
18876
+ findings.push({
18877
+ category: "todo_comment",
18878
+ priority,
18879
+ tag,
18880
+ file: filePath,
18881
+ line: i + 1,
18882
+ snippet: line.trim().slice(0, 200),
18883
+ description: message || `${tag} comment without description`
18884
+ });
18885
+ }
18886
+ return findings;
18887
+ }
18888
+ var STUB_BODY_PATTERNS = [
18889
+ // completely empty (just whitespace / braces)
18890
+ /^\s*$/,
18891
+ // Python pass
18892
+ /^\s*pass\s*$/,
18893
+ // bare return / return null / return undefined / return nil / return None
18894
+ /^\s*return(?:\s+(?:null|undefined|nil|None|false|0|''))?\s*;?\s*$/,
18895
+ // throw not-implemented
18896
+ /^\s*throw\s+new\s+(?:Error|NotImplementedError|UnsupportedOperationException)\s*\(\s*(['"`].*?['"`])?\s*\)\s*;?\s*$/,
18897
+ // Python raise
18898
+ /^\s*raise\s+(?:NotImplementedError|NotImplemented)\s*(?:\(.*\))?\s*$/,
18899
+ // Single-line TODO/FIXME comment only
18900
+ /^\s*(?:\/\/|#|--|%)\s*(?:TODO|FIXME|HACK|XXX)\b.*$/i,
18901
+ // Ellipsis (Python stub)
18902
+ /^\s*\.\.\.\s*$/
18903
+ ];
18904
+ var CALLABLE_KINDS = /* @__PURE__ */ new Set([
18905
+ "function",
18906
+ "method",
18907
+ "arrow_function",
18908
+ "closure",
18909
+ "constructor",
18910
+ "static_method",
18911
+ "class_method",
18912
+ "generator",
18913
+ "async_function",
18914
+ "async_method"
18915
+ ]);
18916
+ function detectEmptyFunctions(content, lines, symbols, filePath, language) {
18917
+ const findings = [];
18918
+ for (const sym of symbols) {
18919
+ if (!CALLABLE_KINDS.has(sym.kind)) continue;
18920
+ if (sym.line_start == null || sym.line_end == null) continue;
18921
+ const rawBody = content.slice(sym.byte_start, sym.byte_end);
18922
+ let body;
18923
+ if (language === "python") {
18924
+ const bodyLines = rawBody.split("\n");
18925
+ let startIdx = 0;
18926
+ for (let i = 0; i < bodyLines.length; i++) {
18927
+ if (/:\s*(?:#.*)?$/.test(bodyLines[i].trimEnd())) {
18928
+ startIdx = i + 1;
18929
+ break;
18930
+ }
18931
+ }
18932
+ body = bodyLines.slice(startIdx).join("\n");
18933
+ } else {
18934
+ const openBrace = rawBody.indexOf("{");
18935
+ const closeBrace = rawBody.lastIndexOf("}");
18936
+ if (openBrace === -1 || closeBrace === -1 || closeBrace <= openBrace) {
18937
+ continue;
18938
+ }
18939
+ body = rawBody.slice(openBrace + 1, closeBrace);
18940
+ }
18941
+ const strippedLines = body.split("\n").map((l) => l.trim()).filter((l) => l.length > 0).filter((l) => !/^(?:\/\/|#|--|\/\*|\*\/|\*)\s*$/.test(l));
18942
+ const joined = strippedLines.join("\n");
18943
+ const isEmpty = strippedLines.length === 0;
18944
+ const isStub = !isEmpty && strippedLines.length <= 2 && STUB_BODY_PATTERNS.some((p4) => p4.test(joined));
18945
+ if (isEmpty || isStub) {
18946
+ const description = isEmpty ? `Empty ${sym.kind} '${sym.name}' \u2014 no implementation` : `Stub ${sym.kind} '${sym.name}' \u2014 placeholder implementation`;
18947
+ findings.push({
18948
+ category: "empty_function",
18949
+ priority: isEmpty ? "medium" : "low",
18950
+ file: filePath,
18951
+ line: sym.line_start,
18952
+ snippet: (sym.signature ?? lines[sym.line_start - 1]?.trim() ?? sym.name).slice(0, 200),
18953
+ description,
18954
+ symbol: sym.name
18955
+ });
18956
+ }
18957
+ }
18958
+ return findings;
18959
+ }
18960
+ var HARDCODE_PATTERNS = [
18961
+ // Hardcoded IP addresses (not 127.0.0.1 or 0.0.0.0 which are often intentional)
18962
+ {
18963
+ name: "hardcoded_ip",
18964
+ regex: /(?<![.\d])(?!(?:127\.0\.0\.1|0\.0\.0\.0|255\.255\.255\.\d+)\b)(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})(?![\d.])/g,
18965
+ priority: "medium",
18966
+ description: "Hardcoded IP address \u2014 use configuration or environment variable",
18967
+ falsePositives: [
18968
+ /version|semver|v\d/i,
18969
+ /(?:\/\/|#|--|\/\*)\s*(?:example|e\.g\.|i\.e\.|see|cf\.|docs)/i,
18970
+ /(?:test|spec|mock|fixture|seed|sample)/i,
18971
+ /0\.0\.0\.0|127\.0\.0\.1|localhost/
18972
+ ]
18973
+ },
18974
+ // Hardcoded port numbers in connection strings
18975
+ {
18976
+ name: "hardcoded_port",
18977
+ regex: /(?:port\s*[:=]\s*|:\s*)(\d{4,5})\b/g,
18978
+ priority: "low",
18979
+ description: "Hardcoded port number \u2014 use configuration or environment variable",
18980
+ falsePositives: [
18981
+ /(?:test|spec|mock|fixture)/i,
18982
+ /process\.env|os\.environ|getenv|ENV\[|config\./i,
18983
+ /(?:\/\/|#)\s*default/i,
18984
+ /\.env|\.ya?ml|\.toml|\.ini|\.cfg|Dockerfile|docker-compose/
18985
+ ]
18986
+ },
18987
+ // Hardcoded URLs (http/https) — likely should be configurable
18988
+ {
18989
+ name: "hardcoded_url",
18990
+ regex: /(?:['"`])(https?:\/\/(?!(?:localhost|127\.0\.0\.1|example\.com|schemas?\.))[\w.-]+\.\w+[^'"`\s]*?)(?:['"`])/g,
18991
+ priority: "medium",
18992
+ description: "Hardcoded URL \u2014 use configuration or environment variable",
18993
+ falsePositives: [
18994
+ /(?:test|spec|mock|fixture|__tests__)/i,
18995
+ /(?:\/\/|#)\s*(?:see|docs|ref|link|source|from|via|credit)/i,
18996
+ /schema\.org|json-schema|w3\.org|ietf\.org|creativecommons|spdx\.org/i,
18997
+ /swagger|openapi|license|readme|changelog/i,
18998
+ /(?:npmjs?|pypi|rubygems|crates|maven|nuget|packagist)\.(?:org|io|dev)/i,
18999
+ /github\.com|gitlab\.com|bitbucket\.org/i
19000
+ ]
19001
+ },
19002
+ // Magic numbers in comparisons, assignments, or returns
19003
+ {
19004
+ name: "magic_number",
19005
+ regex: /(?:===?\s*|!==?\s*|[<>]=?\s*|return\s+|=\s+)(-?\d{2,}(?:\.\d+)?)\b/g,
19006
+ priority: "low",
19007
+ description: "Magic number \u2014 extract to a named constant for readability",
19008
+ falsePositives: [
19009
+ /(?:test|spec|mock|fixture|__tests__)/i,
19010
+ // Common non-magic values
19011
+ /(?:===?\s*|!==?\s*|return\s+|=\s+)(?:0|1|2|10|100|1000|200|201|204|301|302|400|401|403|404|409|422|429|500|502|503)\b/,
19012
+ /(?:status|code|http|statusCode|response)\s*(?:===?|!==?)/i,
19013
+ /(?:length|size|count|index|offset|width|height|margin|padding)\s*(?:===?|!==?|[<>]=?)/i,
19014
+ /\.(?:status|code|length|size|indexOf|charCodeAt)\s*(?:===?|!==?)/i,
19015
+ /(?:Math\.|parseInt|parseFloat|Number\()/,
19016
+ /(?:timeout|delay|interval|duration|ttl|retry|retries|max_|min_|limit)/i
19017
+ ]
19018
+ },
19019
+ // Hardcoded credentials / secrets patterns (not already caught by security scanner)
19020
+ {
19021
+ name: "hardcoded_credential",
19022
+ regex: /(?:password|passwd|pwd|secret|api_?key|token|auth)\s*[:=]\s*['"`](?![\s'"`)]{0,2}$)[^'"`\n]{3,}['"`]/gi,
19023
+ priority: "high",
19024
+ description: "Hardcoded credential \u2014 use environment variable or secret manager",
19025
+ falsePositives: [
19026
+ /(?:test|spec|mock|fixture|__tests__|example|sample|dummy|placeholder)/i,
19027
+ /process\.env|os\.environ|getenv|ENV\[|config\./i,
19028
+ /TODO|FIXME|CHANGEME|REPLACE|PLACEHOLDER|YOUR_/i,
19029
+ /(?:schema|type|validation|interface|swagger|openapi)/i
19030
+ ]
19031
+ },
19032
+ // Hardcoded feature flags / toggles
19033
+ {
19034
+ name: "hardcoded_feature_flag",
19035
+ regex: /(?:feature|flag|toggle|experiment|beta|enable|disable)[\w_]*\s*[:=]\s*(?:true|false|1|0)\b/gi,
19036
+ priority: "low",
19037
+ description: "Hardcoded feature flag \u2014 use a feature flag service or configuration",
19038
+ falsePositives: [
19039
+ /(?:test|spec|mock|fixture|__tests__)/i,
19040
+ /(?:default|fallback|config|schema|type|interface)/i,
19041
+ /process\.env|os\.environ|getenv|ENV\[/i
19042
+ ]
19043
+ }
19044
+ ];
19045
+ function detectHardcodedValues(lines, filePath) {
19046
+ const findings = [];
19047
+ for (let i = 0; i < lines.length; i++) {
19048
+ const line = lines[i];
19049
+ for (const pattern of HARDCODE_PATTERNS) {
19050
+ const re = new RegExp(pattern.regex.source, pattern.regex.flags);
19051
+ re.lastIndex = 0;
19052
+ const m = re.exec(line);
19053
+ if (!m) continue;
19054
+ let isFP = false;
19055
+ for (const fp of pattern.falsePositives) {
19056
+ if (fp.test(line) || fp.test(filePath)) {
19057
+ isFP = true;
19058
+ break;
19059
+ }
19060
+ }
19061
+ if (isFP) continue;
19062
+ findings.push({
19063
+ category: "hardcoded_value",
19064
+ priority: pattern.priority,
19065
+ tag: pattern.name,
19066
+ file: filePath,
19067
+ line: i + 1,
19068
+ snippet: line.trim().slice(0, 200),
19069
+ description: pattern.description
19070
+ });
19071
+ }
19072
+ }
19073
+ return findings;
19074
+ }
19075
+ var PRIORITY_ORDER = { high: 0, medium: 1, low: 2 };
19076
+ function priorityRank(p4) {
19077
+ return PRIORITY_ORDER[p4];
19078
+ }
19079
+ var BATCH_SIZE2 = 100;
19080
+ var MAX_FILE_SIZE2 = 512 * 1024;
19081
+ function scanCodeSmells(store, projectRoot, opts = {}) {
19082
+ const categories = new Set(opts.category ?? ALL_CATEGORIES2);
19083
+ const thresholdRank = priorityRank(opts.priority_threshold ?? "low");
19084
+ const includeTests = opts.include_tests ?? false;
19085
+ const tagFilter = opts.tags ? new Set(opts.tags.map((t) => t.toUpperCase())) : null;
19086
+ const limit = opts.limit ?? 200;
19087
+ const scope = opts.scope?.replace(/\/+$/, "");
19088
+ const files = scope ? store.db.prepare("SELECT id, path, language FROM files WHERE path LIKE ? AND (status = 'ok' OR status IS NULL)").all(`${scope}%`) : store.db.prepare("SELECT id, path, language FROM files WHERE status = 'ok' OR status IS NULL").all();
19089
+ const allFindings = [];
19090
+ let scanned = 0;
19091
+ const needsEmptyFn = categories.has("empty_function");
19092
+ for (let i = 0; i < files.length; i += BATCH_SIZE2) {
19093
+ const batch = files.slice(i, i + BATCH_SIZE2);
19094
+ for (const file of batch) {
19095
+ if (!includeTests && /\.(?:test|spec)\.|__tests__|\/tests?\//i.test(file.path)) continue;
19096
+ const absPath = path29.resolve(projectRoot, file.path);
19097
+ let content;
19098
+ try {
19099
+ const buf = readFileSync2(absPath);
19100
+ if (buf.length > MAX_FILE_SIZE2) continue;
19101
+ content = buf.toString("utf-8");
19102
+ } catch {
19103
+ continue;
19104
+ }
19105
+ scanned++;
19106
+ const lines = content.split("\n");
19107
+ if (categories.has("todo_comment")) {
19108
+ const todos = detectTodoComments(lines, file.path);
19109
+ for (const f of todos) {
19110
+ if (priorityRank(f.priority) > thresholdRank) continue;
19111
+ if (tagFilter && f.tag && !tagFilter.has(f.tag)) continue;
19112
+ allFindings.push(f);
19113
+ }
19114
+ }
19115
+ if (needsEmptyFn) {
19116
+ const symbols = store.getSymbolsByFile(file.id);
19117
+ const empties = detectEmptyFunctions(content, lines, symbols, file.path, file.language ?? "");
19118
+ for (const f of empties) {
19119
+ if (priorityRank(f.priority) > thresholdRank) continue;
19120
+ allFindings.push(f);
19121
+ }
19122
+ }
19123
+ if (categories.has("hardcoded_value")) {
19124
+ const hardcoded = detectHardcodedValues(lines, file.path);
19125
+ for (const f of hardcoded) {
19126
+ if (priorityRank(f.priority) > thresholdRank) continue;
19127
+ allFindings.push(f);
19128
+ }
19129
+ }
19130
+ }
19131
+ }
19132
+ allFindings.sort(
19133
+ (a, b) => priorityRank(a.priority) - priorityRank(b.priority) || a.file.localeCompare(b.file) || a.line - b.line
19134
+ );
19135
+ const summary = {
19136
+ todo_comment: 0,
19137
+ empty_function: 0,
19138
+ hardcoded_value: 0
19139
+ };
19140
+ for (const f of allFindings) {
19141
+ summary[f.category]++;
19142
+ }
19143
+ return ok({
19144
+ files_scanned: scanned,
19145
+ findings: allFindings.slice(0, limit),
19146
+ summary,
19147
+ total: allFindings.length
19148
+ });
19149
+ }
19150
+
19151
+ // src/tools/sbom.ts
19152
+ import { readFileSync as readFileSync3, existsSync } from "fs";
19153
+ import path30 from "path";
18729
19154
  var COPYLEFT_LICENSES = /* @__PURE__ */ new Set([
18730
19155
  "GPL-2.0",
18731
19156
  "GPL-2.0-only",
@@ -18760,21 +19185,21 @@ function checkLicenseWarning(name, version, license) {
18760
19185
  }
18761
19186
  function readJson(filePath) {
18762
19187
  try {
18763
- return JSON.parse(readFileSync2(filePath, "utf-8"));
19188
+ return JSON.parse(readFileSync3(filePath, "utf-8"));
18764
19189
  } catch {
18765
19190
  return null;
18766
19191
  }
18767
19192
  }
18768
19193
  function readLines2(filePath) {
18769
19194
  try {
18770
- return readFileSync2(filePath, "utf-8").split("\n");
19195
+ return readFileSync3(filePath, "utf-8").split("\n");
18771
19196
  } catch {
18772
19197
  return [];
18773
19198
  }
18774
19199
  }
18775
19200
  function parseNpm(root, includeDev, includeTransitive) {
18776
- const pkgPath = path29.join(root, "package.json");
18777
- const lockPath = path29.join(root, "package-lock.json");
19201
+ const pkgPath = path30.join(root, "package.json");
19202
+ const lockPath = path30.join(root, "package-lock.json");
18778
19203
  const pkg = readJson(pkgPath);
18779
19204
  if (!pkg) return [];
18780
19205
  const deps = pkg.dependencies ?? {};
@@ -18814,8 +19239,8 @@ function parseNpm(root, includeDev, includeTransitive) {
18814
19239
  return components;
18815
19240
  }
18816
19241
  function parseComposer(root, includeDev, includeTransitive) {
18817
- const lockPath = path29.join(root, "composer.lock");
18818
- const manifestPath = path29.join(root, "composer.json");
19242
+ const lockPath = path30.join(root, "composer.lock");
19243
+ const manifestPath = path30.join(root, "composer.json");
18819
19244
  const manifest = readJson(manifestPath);
18820
19245
  const directNames = /* @__PURE__ */ new Set();
18821
19246
  if (manifest) {
@@ -18858,7 +19283,7 @@ function parseComposer(root, includeDev, includeTransitive) {
18858
19283
  }
18859
19284
  function parsePip(root, includeDev, includeTransitive) {
18860
19285
  const components = [];
18861
- const reqPath = path29.join(root, "requirements.txt");
19286
+ const reqPath = path30.join(root, "requirements.txt");
18862
19287
  if (existsSync(reqPath)) {
18863
19288
  for (const line of readLines2(reqPath)) {
18864
19289
  const trimmed = line.trim();
@@ -18874,9 +19299,9 @@ function parsePip(root, includeDev, includeTransitive) {
18874
19299
  }
18875
19300
  }
18876
19301
  }
18877
- const poetryLock = path29.join(root, "poetry.lock");
19302
+ const poetryLock = path30.join(root, "poetry.lock");
18878
19303
  if (existsSync(poetryLock) && includeTransitive) {
18879
- const content = readFileSync2(poetryLock, "utf-8");
19304
+ const content = readFileSync3(poetryLock, "utf-8");
18880
19305
  const directNames = new Set(components.map((c) => c.name.toLowerCase()));
18881
19306
  let currentName = "";
18882
19307
  let currentVersion = "";
@@ -18905,7 +19330,7 @@ function parsePip(root, includeDev, includeTransitive) {
18905
19330
  return components;
18906
19331
  }
18907
19332
  function parseGo(root, _includeDev, includeTransitive) {
18908
- const modPath = path29.join(root, "go.mod");
19333
+ const modPath = path30.join(root, "go.mod");
18909
19334
  if (!existsSync(modPath)) return [];
18910
19335
  const components = [];
18911
19336
  const directNames = /* @__PURE__ */ new Set();
@@ -18938,9 +19363,9 @@ function parseGo(root, _includeDev, includeTransitive) {
18938
19363
  return components;
18939
19364
  }
18940
19365
  function parseCargo(root, _includeDev, includeTransitive) {
18941
- const lockPath = path29.join(root, "Cargo.lock");
19366
+ const lockPath = path30.join(root, "Cargo.lock");
18942
19367
  if (!existsSync(lockPath)) {
18943
- const tomlPath = path29.join(root, "Cargo.toml");
19368
+ const tomlPath = path30.join(root, "Cargo.toml");
18944
19369
  if (!existsSync(tomlPath)) return [];
18945
19370
  const components2 = [];
18946
19371
  let inDeps = false;
@@ -19005,7 +19430,7 @@ function parseCargo(root, _includeDev, includeTransitive) {
19005
19430
  });
19006
19431
  }
19007
19432
  if (!includeTransitive) {
19008
- const tomlPath = path29.join(root, "Cargo.toml");
19433
+ const tomlPath = path30.join(root, "Cargo.toml");
19009
19434
  if (existsSync(tomlPath)) {
19010
19435
  const directNames = /* @__PURE__ */ new Set();
19011
19436
  let inDeps = false;
@@ -19030,7 +19455,7 @@ function parseCargo(root, _includeDev, includeTransitive) {
19030
19455
  return components;
19031
19456
  }
19032
19457
  function parseBundler(root, _includeDev, includeTransitive) {
19033
- const lockPath = path29.join(root, "Gemfile.lock");
19458
+ const lockPath = path30.join(root, "Gemfile.lock");
19034
19459
  if (!existsSync(lockPath)) return [];
19035
19460
  const components = [];
19036
19461
  let inSpecs = false;
@@ -19069,9 +19494,9 @@ function parseBundler(root, _includeDev, includeTransitive) {
19069
19494
  return components;
19070
19495
  }
19071
19496
  function parseMaven(root, _includeDev, _includeTransitive) {
19072
- const pomPath = path29.join(root, "pom.xml");
19497
+ const pomPath = path30.join(root, "pom.xml");
19073
19498
  if (!existsSync(pomPath)) return [];
19074
- const content = readFileSync2(pomPath, "utf-8");
19499
+ const content = readFileSync3(pomPath, "utf-8");
19075
19500
  const components = [];
19076
19501
  const depRegex = /<dependency>\s*<groupId>([^<]+)<\/groupId>\s*<artifactId>([^<]+)<\/artifactId>\s*(?:<version>([^<]+)<\/version>)?/gs;
19077
19502
  let match;
@@ -19376,8 +19801,8 @@ function getArtifacts(store, opts) {
19376
19801
 
19377
19802
  // src/tools/zero-index.ts
19378
19803
  import { execFileSync as execFileSync6 } from "child_process";
19379
- import { readFileSync as readFileSync3, readdirSync, statSync } from "fs";
19380
- import path30 from "path";
19804
+ import { readFileSync as readFileSync4, readdirSync, statSync } from "fs";
19805
+ import path31 from "path";
19381
19806
  var RG_ARGS_BASE = [
19382
19807
  "--json",
19383
19808
  "--max-count=200",
@@ -19408,7 +19833,7 @@ function searchWithRipgrep(projectRoot, query, opts) {
19408
19833
  const parsed = JSON.parse(line);
19409
19834
  if (parsed.type !== "match") continue;
19410
19835
  const data = parsed.data;
19411
- const relPath = path30.relative(projectRoot, data.path.text);
19836
+ const relPath = path31.relative(projectRoot, data.path.text);
19412
19837
  matches.push({
19413
19838
  file: relPath,
19414
19839
  line: data.line_number,
@@ -19444,7 +19869,7 @@ function manualSearch(projectRoot, query, opts) {
19444
19869
  for (const entry of entries) {
19445
19870
  if (matches.length >= limit) return;
19446
19871
  if (skipDirs.has(entry)) continue;
19447
- const full = path30.join(dir, entry);
19872
+ const full = path31.join(dir, entry);
19448
19873
  let stat;
19449
19874
  try {
19450
19875
  stat = statSync(full);
@@ -19455,14 +19880,14 @@ function manualSearch(projectRoot, query, opts) {
19455
19880
  walk2(full);
19456
19881
  } else if (stat.isFile() && stat.size < 512 * 1024) {
19457
19882
  try {
19458
- const content = readFileSync3(full, "utf-8");
19883
+ const content = readFileSync4(full, "utf-8");
19459
19884
  const lines = content.split("\n");
19460
19885
  for (let i = 0; i < lines.length; i++) {
19461
19886
  regex.lastIndex = 0;
19462
19887
  const m = regex.exec(lines[i]);
19463
19888
  if (m) {
19464
19889
  matches.push({
19465
- file: path30.relative(projectRoot, full),
19890
+ file: path31.relative(projectRoot, full),
19466
19891
  line: i + 1,
19467
19892
  column: m.index + 1,
19468
19893
  text: lines[i].trimEnd()
@@ -19529,7 +19954,7 @@ SYMBOL_PATTERNS["tsx"] = SYMBOL_PATTERNS["typescript"];
19529
19954
  SYMBOL_PATTERNS["jsx"] = SYMBOL_PATTERNS["javascript"];
19530
19955
  SYMBOL_PATTERNS["kotlin"] = SYMBOL_PATTERNS["java"];
19531
19956
  function detectLanguage2(filePath) {
19532
- const ext = path30.extname(filePath).toLowerCase();
19957
+ const ext = path31.extname(filePath).toLowerCase();
19533
19958
  const map = {
19534
19959
  ".ts": "typescript",
19535
19960
  ".tsx": "typescript",
@@ -19580,8 +20005,8 @@ function fallbackSearch(projectRoot, query, opts = {}) {
19580
20005
  };
19581
20006
  }
19582
20007
  function fallbackOutline(projectRoot, filePath) {
19583
- const absPath = path30.resolve(projectRoot, filePath);
19584
- const content = readFileSync3(absPath, "utf-8");
20008
+ const absPath = path31.resolve(projectRoot, filePath);
20009
+ const content = readFileSync4(absPath, "utf-8");
19585
20010
  const language = detectLanguage2(filePath);
19586
20011
  const symbols = extractSymbolsFromContent(content, language);
19587
20012
  return {
@@ -20062,8 +20487,8 @@ function compareBranches(store, rootPath, opts) {
20062
20487
 
20063
20488
  // src/savings.ts
20064
20489
  import fs21 from "fs";
20065
- import path31 from "path";
20066
- var SAVINGS_PATH = path31.join(TRACE_MCP_HOME, "savings.json");
20490
+ import path32 from "path";
20491
+ var SAVINGS_PATH = path32.join(TRACE_MCP_HOME, "savings.json");
20067
20492
  var RAW_COST_ESTIMATES = {
20068
20493
  get_symbol: 800,
20069
20494
  search: 600,
@@ -20195,7 +20620,7 @@ function savePersistentSavings(data) {
20195
20620
 
20196
20621
  // src/tools/audit-config.ts
20197
20622
  import fs22 from "fs";
20198
- import path32 from "path";
20623
+ import path33 from "path";
20199
20624
  var CONFIG_PATTERNS = [
20200
20625
  "CLAUDE.md",
20201
20626
  ".claude/CLAUDE.md",
@@ -20232,7 +20657,7 @@ function auditConfig(store, projectRoot, options = {}) {
20232
20657
  const pathMatches = line.match(/(?:src|lib|app|routes|tests?|components?|pages?)\/[\w/.-]+\.\w+/g);
20233
20658
  if (pathMatches) {
20234
20659
  for (const ref of pathMatches) {
20235
- const refPath = path32.join(projectRoot, ref);
20660
+ const refPath = path33.join(projectRoot, ref);
20236
20661
  if (!fs22.existsSync(refPath) && !store.getFile(ref)) {
20237
20662
  issues.push({
20238
20663
  file,
@@ -20345,7 +20770,7 @@ function checkSymbol(store, name, file, line, fixSuggestions, issues) {
20345
20770
  function findConfigFiles(projectRoot) {
20346
20771
  const found = [];
20347
20772
  for (const pattern of CONFIG_PATTERNS) {
20348
- const absPath = path32.join(projectRoot, pattern);
20773
+ const absPath = path33.join(projectRoot, pattern);
20349
20774
  if (fs22.existsSync(absPath)) found.push(pattern);
20350
20775
  }
20351
20776
  for (const pattern of GLOBAL_CONFIG_PATTERNS) {
@@ -20356,8 +20781,8 @@ function findConfigFiles(projectRoot) {
20356
20781
  }
20357
20782
  function resolveConfigPath(file, projectRoot) {
20358
20783
  if (file.startsWith("~")) return file.replace("~", process.env.HOME ?? "");
20359
- if (path32.isAbsolute(file)) return file;
20360
- return path32.join(projectRoot, file);
20784
+ if (path33.isAbsolute(file)) return file;
20785
+ return path33.join(projectRoot, file);
20361
20786
  }
20362
20787
  function isGlobalConfig(file) {
20363
20788
  return file.startsWith("~") || file.includes(".claude/CLAUDE.md");
@@ -20946,7 +21371,7 @@ Review this pre-merge checklist. Flag any risks and recommend whether it's safe
20946
21371
  // src/tools/pack-context.ts
20947
21372
  init_graph_analysis();
20948
21373
  import fs23 from "fs";
20949
- import path33 from "path";
21374
+ import path34 from "path";
20950
21375
  function estimateTokens2(text) {
20951
21376
  return Math.ceil(text.length / 4);
20952
21377
  }
@@ -21023,7 +21448,7 @@ ${outlineLines.join("\n")}
21023
21448
  const budgetForSource = Math.floor((maxTokens - tokenCount) * 0.9);
21024
21449
  let sourceTokens = 0;
21025
21450
  for (const f of files) {
21026
- const absPath = path33.join(projectRoot, f.path);
21451
+ const absPath = path34.join(projectRoot, f.path);
21027
21452
  if (!fs23.existsSync(absPath)) continue;
21028
21453
  let content;
21029
21454
  try {
@@ -21184,7 +21609,7 @@ function buildFileTree(paths) {
21184
21609
  for (const p4 of sorted.slice(0, 200)) {
21185
21610
  const depth = p4.split("/").length - 1;
21186
21611
  const indent = " ".repeat(Math.min(depth, 6));
21187
- lines.push(`${indent}${path33.basename(p4)}`);
21612
+ lines.push(`${indent}${path34.basename(p4)}`);
21188
21613
  }
21189
21614
  if (sorted.length > 200) {
21190
21615
  lines.push(` ... and ${sorted.length - 200} more files`);
@@ -21457,7 +21882,7 @@ function escapeHtml(s) {
21457
21882
 
21458
21883
  // src/tools/package-deps.ts
21459
21884
  import fs24 from "fs";
21460
- import path34 from "path";
21885
+ import path35 from "path";
21461
21886
  function loadRegistry() {
21462
21887
  if (!fs24.existsSync(REGISTRY_PATH)) return {};
21463
21888
  try {
@@ -21469,7 +21894,7 @@ function loadRegistry() {
21469
21894
  }
21470
21895
  function readManifest(repoPath) {
21471
21896
  const result = { name: void 0, deps: /* @__PURE__ */ new Map(), publishes: [] };
21472
- const pkgJsonPath = path34.join(repoPath, "package.json");
21897
+ const pkgJsonPath = path35.join(repoPath, "package.json");
21473
21898
  if (fs24.existsSync(pkgJsonPath)) {
21474
21899
  try {
21475
21900
  const pkg = JSON.parse(fs24.readFileSync(pkgJsonPath, "utf-8"));
@@ -21484,7 +21909,7 @@ function readManifest(repoPath) {
21484
21909
  } catch {
21485
21910
  }
21486
21911
  }
21487
- const composerPath = path34.join(repoPath, "composer.json");
21912
+ const composerPath = path35.join(repoPath, "composer.json");
21488
21913
  if (fs24.existsSync(composerPath)) {
21489
21914
  try {
21490
21915
  const composer = JSON.parse(fs24.readFileSync(composerPath, "utf-8"));
@@ -21502,7 +21927,7 @@ function readManifest(repoPath) {
21502
21927
  } catch {
21503
21928
  }
21504
21929
  }
21505
- const pyprojectPath = path34.join(repoPath, "pyproject.toml");
21930
+ const pyprojectPath = path35.join(repoPath, "pyproject.toml");
21506
21931
  if (fs24.existsSync(pyprojectPath)) {
21507
21932
  try {
21508
21933
  const content = fs24.readFileSync(pyprojectPath, "utf-8");
@@ -21535,7 +21960,7 @@ function getPackageDeps(options) {
21535
21960
  repoManifests.set(repoPath, manifest);
21536
21961
  const allPublishes = [...entry.publishes ?? [], ...manifest.publishes];
21537
21962
  for (const p4 of new Set(allPublishes)) {
21538
- publishMap.set(p4, { repo: entry.name ?? path34.basename(repoPath), repoPath });
21963
+ publishMap.set(p4, { repo: entry.name ?? path35.basename(repoPath), repoPath });
21539
21964
  }
21540
21965
  }
21541
21966
  const results = [];
@@ -21545,7 +21970,7 @@ function getPackageDeps(options) {
21545
21970
  } else if (project) {
21546
21971
  for (const [repoPath, manifest] of repoManifests) {
21547
21972
  const entry = registry[repoPath];
21548
- if (entry?.name === project || path34.basename(repoPath) === project) {
21973
+ if (entry?.name === project || path35.basename(repoPath) === project) {
21549
21974
  for (const p4 of manifest.publishes) {
21550
21975
  targetPackages.add(p4);
21551
21976
  }
@@ -21564,7 +21989,7 @@ function getPackageDeps(options) {
21564
21989
  if (direction === "dependents" || direction === "both") {
21565
21990
  for (const [repoPath, manifest] of repoManifests) {
21566
21991
  const entry = registry[repoPath];
21567
- const repoName = entry?.name ?? path34.basename(repoPath);
21992
+ const repoName = entry?.name ?? path35.basename(repoPath);
21568
21993
  for (const targetPkg of targetPackages) {
21569
21994
  const dep = manifest.deps.get(targetPkg);
21570
21995
  if (dep) {
@@ -21583,7 +22008,7 @@ function getPackageDeps(options) {
21583
22008
  if (direction === "dependencies" || direction === "both") {
21584
22009
  for (const [repoPath, manifest] of repoManifests) {
21585
22010
  const entry = registry[repoPath];
21586
- const repoName = entry?.name ?? path34.basename(repoPath);
22011
+ const repoName = entry?.name ?? path35.basename(repoPath);
21587
22012
  const isTarget = manifest.publishes.some((p4) => targetPackages.has(p4));
21588
22013
  if (!isTarget && targetPackages.size > 0) continue;
21589
22014
  for (const [dep, info] of manifest.deps) {
@@ -21839,7 +22264,7 @@ function cfgToAscii(cfg) {
21839
22264
 
21840
22265
  // src/tools/control-flow.ts
21841
22266
  import fs25 from "fs";
21842
- import path35 from "path";
22267
+ import path36 from "path";
21843
22268
  function getControlFlow(store, projectRoot, options) {
21844
22269
  const { symbolId, fqn, format, simplify } = options;
21845
22270
  let symbol = null;
@@ -21862,7 +22287,7 @@ function getControlFlow(store, projectRoot, options) {
21862
22287
  if (!file) {
21863
22288
  return err(notFound(`file for symbol`));
21864
22289
  }
21865
- const absPath = path35.isAbsolute(file.path) ? file.path : path35.join(projectRoot, file.path);
22290
+ const absPath = path36.isAbsolute(file.path) ? file.path : path36.join(projectRoot, file.path);
21866
22291
  if (!fs25.existsSync(absPath)) {
21867
22292
  return err(validationError(`File not on disk: ${file.path}`));
21868
22293
  }
@@ -21948,19 +22373,39 @@ function createServer2(store, registry, config, rootPath) {
21948
22373
  instructions: [
21949
22374
  `trace-mcp is a framework-aware code intelligence server for this project. Detected frameworks: ${detectedFrameworks}.`,
21950
22375
  "",
21951
- "WHEN TO USE trace-mcp tools (instead of Read/Grep/Glob):",
21952
- "- Finding a function/class/method \u2192 `search` (understands symbol kinds, FQNs, language filters)",
21953
- "- Understanding a file before editing \u2192 `outline_file` (signatures only \u2014 cheaper than Read)",
22376
+ "IMPORTANT: For ANY code exploration task, ALWAYS use trace-mcp tools first. NEVER fall back to Read/Grep/Glob/Bash(ls,find) for navigating source code \u2014 trace-mcp gives semantic, structured results that are cheaper in tokens and more accurate.",
22377
+ "",
22378
+ "WHEN TO USE trace-mcp tools:",
22379
+ "",
22380
+ "Navigation & search:",
22381
+ "- Finding a function/class/method \u2192 `search` (understands symbol kinds, FQNs, language filters; use `implements`/`extends` to filter by interface)",
22382
+ "- Understanding a file before editing \u2192 `get_outline` (signatures only \u2014 cheaper than Read)",
21954
22383
  "- Reading one symbol's source \u2192 `get_symbol` (returns only the symbol, not the whole file)",
22384
+ "- Quick keyword context \u2192 `get_feature_context` (NL query \u2192 relevant symbols + source)",
22385
+ "- Starting work on a task \u2192 `get_task_context` (NL task \u2192 full execution context with tests)",
22386
+ "",
22387
+ "Relationships & impact:",
21955
22388
  "- What breaks if I change X \u2192 `get_change_impact` (reverse dependency graph)",
21956
22389
  "- Who calls this / what does it call \u2192 `get_call_graph` (bidirectional)",
21957
- "- All usages of a symbol \u2192 `trace_usages` (semantic: imports, calls, renders, dispatches)",
21958
- "- Starting work on a task \u2192 `get_task_context` (NL task \u2192 full execution context with tests, adapts to bugfix/feature/refactor)",
21959
- "- Quick keyword context \u2192 `get_feature_context` (NL query \u2192 relevant symbols + source)",
22390
+ "- All usages of a symbol \u2192 `find_usages` (semantic: imports, calls, renders, dispatches)",
21960
22391
  "- Tests for a symbol/file \u2192 `get_tests_for` (understands test-to-source mapping)",
22392
+ "",
22393
+ "Architecture & meta-analysis:",
22394
+ "- All implementations of an interface \u2192 `get_type_hierarchy` (walks extends/implements tree)",
22395
+ "- All classes implementing X \u2192 `search` with `implements` or `extends` filter",
22396
+ "- Project health / coverage gaps \u2192 `self_audit` (dead exports, untested code, hotspots)",
22397
+ "- Module dependency graph \u2192 `get_module_graph` (NestJS) or `get_import_graph`",
22398
+ "- Dead code / dead exports \u2192 `get_dead_code` / `get_dead_exports`",
22399
+ "- Circular dependencies \u2192 `get_circular_imports`",
22400
+ "- Coupling analysis \u2192 `get_coupling`",
22401
+ "",
22402
+ "Framework-specific:",
21961
22403
  "- HTTP request flow \u2192 `get_request_flow` (route \u2192 middleware \u2192 controller \u2192 service)",
21962
22404
  "- DB model details \u2192 `get_model_context` (relationships, schema, metadata)",
21963
22405
  "- Database schema \u2192 `get_schema` (from migrations/ORM definitions)",
22406
+ "- Component tree \u2192 `get_component_tree` (React/Vue/Angular)",
22407
+ "- State stores \u2192 `get_state_stores` (Zustand/Redux/Pinia)",
22408
+ "- Event graph \u2192 `get_event_graph` (event emitters/listeners)",
21964
22409
  "",
21965
22410
  "WHEN TO USE native tools (Read/Grep/Glob):",
21966
22411
  "- Non-code files (.md, .json, .yaml, .toml, config) \u2192 Read/Grep",
@@ -22865,6 +23310,40 @@ function createServer2(store, registry, config, rootPath) {
22865
23310
  return { content: [{ type: "text", text: jh("detect_antipatterns", result.value) }] };
22866
23311
  }
22867
23312
  );
23313
+ server.tool(
23314
+ "scan_code_smells",
23315
+ "Find deferred work and shortcuts: TODO/FIXME/HACK/XXX comments, empty functions & stubs, hardcoded values (IPs, URLs, credentials, magic numbers, feature flags). Surfaces technical debt that grep alone misses by combining comment scanning, symbol body analysis, and context-aware false-positive filtering.",
23316
+ {
23317
+ category: z4.array(z4.enum([
23318
+ "todo_comment",
23319
+ "empty_function",
23320
+ "hardcoded_value"
23321
+ ])).optional().describe("Categories to scan (default: all)"),
23322
+ scope: z4.string().max(512).optional().describe("Directory to scan (default: whole project)"),
23323
+ priority_threshold: z4.enum(["high", "medium", "low"]).optional().describe("Minimum priority to report (default: low)"),
23324
+ include_tests: z4.boolean().optional().describe("Include test files in scan (default: false)"),
23325
+ tags: z4.array(z4.string().max(64)).optional().describe('Filter TODO comments by tag (e.g. ["FIXME","HACK"]). Only applies to todo_comment category'),
23326
+ limit: z4.number().int().min(1).max(1e3).optional().describe("Max findings to return (default: 200)")
23327
+ },
23328
+ async ({ category, scope, priority_threshold, include_tests, tags, limit }) => {
23329
+ if (scope) {
23330
+ const blocked = guardPath(scope);
23331
+ if (blocked) return blocked;
23332
+ }
23333
+ const result = scanCodeSmells(store, projectRoot, {
23334
+ category,
23335
+ scope,
23336
+ priority_threshold,
23337
+ include_tests,
23338
+ tags,
23339
+ limit
23340
+ });
23341
+ if (result.isErr()) {
23342
+ return { content: [{ type: "text", text: j2(formatToolError(result.error)) }], isError: true };
23343
+ }
23344
+ return { content: [{ type: "text", text: jh("scan_code_smells", result.value) }] };
23345
+ }
23346
+ );
22868
23347
  server.tool(
22869
23348
  "generate_sbom",
22870
23349
  "Generate a Software Bill of Materials (SBOM) from package manifests and lockfiles. Supports npm, Composer, pip, Go, Cargo, Bundler, Maven. Outputs CycloneDX, SPDX, or plain JSON. Includes license compliance warnings for copyleft licenses.",
@@ -23878,7 +24357,7 @@ function createServer2(store, registry, config, rootPath) {
23878
24357
 
23879
24358
  // src/indexer/plugins/language/php/index.ts
23880
24359
  import { createRequire as createRequire2 } from "module";
23881
- import { ok as ok9, err as err11 } from "neverthrow";
24360
+ import { ok as ok9, err as err12 } from "neverthrow";
23882
24361
 
23883
24362
  // src/indexer/plugins/language/php/helpers.ts
23884
24363
  function extractNamespace(rootNode) {
@@ -24194,7 +24673,7 @@ var PhpLanguagePlugin = class {
24194
24673
  });
24195
24674
  } catch (e) {
24196
24675
  const msg = e instanceof Error ? e.message : String(e);
24197
- return err11(parseError(filePath, `PHP parse failed: ${msg}`));
24676
+ return err12(parseError(filePath, `PHP parse failed: ${msg}`));
24198
24677
  }
24199
24678
  }
24200
24679
  walkTopLevel(root, filePath, namespace, symbols) {
@@ -24400,7 +24879,7 @@ var PhpLanguagePlugin = class {
24400
24879
 
24401
24880
  // src/indexer/plugins/language/typescript/index.ts
24402
24881
  import { createRequire as createRequire3 } from "module";
24403
- import { ok as ok10, err as err12 } from "neverthrow";
24882
+ import { ok as ok10, err as err13 } from "neverthrow";
24404
24883
 
24405
24884
  // src/indexer/plugins/language/typescript/helpers.ts
24406
24885
  function makeSymbolId2(relativePath, name, kind, parentName) {
@@ -24783,7 +25262,7 @@ var TypeScriptLanguagePlugin = class {
24783
25262
  });
24784
25263
  } catch (e) {
24785
25264
  const msg = e instanceof Error ? e.message : String(e);
24786
- return err12(parseError(filePath, `TypeScript parse failed: ${msg}`));
25265
+ return err13(parseError(filePath, `TypeScript parse failed: ${msg}`));
24787
25266
  }
24788
25267
  }
24789
25268
  walkTopLevel(root, filePath, symbols) {
@@ -25007,7 +25486,7 @@ var TypeScriptLanguagePlugin = class {
25007
25486
  // src/indexer/plugins/language/vue/index.ts
25008
25487
  import { createRequire as createRequire4 } from "module";
25009
25488
  import { parse as parseSFC } from "@vue/compiler-sfc";
25010
- import { ok as ok11, err as err13 } from "neverthrow";
25489
+ import { ok as ok11, err as err14 } from "neverthrow";
25011
25490
 
25012
25491
  // src/indexer/plugins/language/vue/helpers.ts
25013
25492
  var HTML_ELEMENTS = /* @__PURE__ */ new Set([
@@ -25403,7 +25882,7 @@ var VueLanguagePlugin = class {
25403
25882
  });
25404
25883
  } catch (e) {
25405
25884
  const msg = e instanceof Error ? e.message : String(e);
25406
- return err13(parseError(filePath, `Vue SFC parse failed: ${msg}`));
25885
+ return err14(parseError(filePath, `Vue SFC parse failed: ${msg}`));
25407
25886
  }
25408
25887
  }
25409
25888
  /**
@@ -25486,7 +25965,7 @@ var VueLanguagePlugin = class {
25486
25965
 
25487
25966
  // src/indexer/plugins/language/python/index.ts
25488
25967
  import { createRequire as createRequire5 } from "module";
25489
- import { ok as ok12, err as err14 } from "neverthrow";
25968
+ import { ok as ok12, err as err15 } from "neverthrow";
25490
25969
 
25491
25970
  // src/indexer/plugins/language/python/helpers.ts
25492
25971
  function collectNodeTypes3(node) {
@@ -25877,7 +26356,7 @@ var PythonLanguagePlugin = class {
25877
26356
  });
25878
26357
  } catch (e) {
25879
26358
  const msg = e instanceof Error ? e.message : String(e);
25880
- return err14(parseError(filePath, `Python parse failed: ${msg}`));
26359
+ return err15(parseError(filePath, `Python parse failed: ${msg}`));
25881
26360
  }
25882
26361
  }
25883
26362
  walkTopLevel(root, filePath, modulePath, symbols) {
@@ -26127,7 +26606,7 @@ var PythonLanguagePlugin = class {
26127
26606
 
26128
26607
  // src/indexer/plugins/language/java/index.ts
26129
26608
  import { createRequire as createRequire6 } from "module";
26130
- import { ok as ok13, err as err15 } from "neverthrow";
26609
+ import { ok as ok13, err as err16 } from "neverthrow";
26131
26610
 
26132
26611
  // src/indexer/plugins/language/java/version-features.ts
26133
26612
  var JAVA_SOURCE_PATTERNS = [
@@ -26474,7 +26953,7 @@ var JavaLanguagePlugin = class {
26474
26953
  });
26475
26954
  } catch (e) {
26476
26955
  const msg = e instanceof Error ? e.message : String(e);
26477
- return err15(parseError(filePath, `Java parse failed: ${msg}`));
26956
+ return err16(parseError(filePath, `Java parse failed: ${msg}`));
26478
26957
  }
26479
26958
  }
26480
26959
  walkTopLevel(root, filePath, packageName, symbols) {
@@ -26601,7 +27080,7 @@ var JavaLanguagePlugin = class {
26601
27080
  };
26602
27081
 
26603
27082
  // src/indexer/plugins/language/kotlin/index.ts
26604
- import { ok as ok14, err as err16 } from "neverthrow";
27083
+ import { ok as ok14, err as err17 } from "neverthrow";
26605
27084
 
26606
27085
  // src/indexer/plugins/language/kotlin/version-features.ts
26607
27086
  var KOTLIN_SOURCE_PATTERNS = [
@@ -26784,14 +27263,14 @@ var KotlinLanguagePlugin = class {
26784
27263
  });
26785
27264
  } catch (e) {
26786
27265
  const msg = e instanceof Error ? e.message : String(e);
26787
- return err16(parseError(filePath, `Kotlin parse failed: ${msg}`));
27266
+ return err17(parseError(filePath, `Kotlin parse failed: ${msg}`));
26788
27267
  }
26789
27268
  }
26790
27269
  };
26791
27270
 
26792
27271
  // src/indexer/plugins/language/ruby/index.ts
26793
27272
  import { createRequire as createRequire7 } from "module";
26794
- import { ok as ok15, err as err17 } from "neverthrow";
27273
+ import { ok as ok15, err as err18 } from "neverthrow";
26795
27274
 
26796
27275
  // src/indexer/plugins/language/ruby/version-features.ts
26797
27276
  var RUBY_SOURCE_PATTERNS = [
@@ -27087,7 +27566,7 @@ var RubyLanguagePlugin = class {
27087
27566
  });
27088
27567
  } catch (e) {
27089
27568
  const msg = e instanceof Error ? e.message : String(e);
27090
- return err17(parseError(filePath, `Ruby parse failed: ${msg}`));
27569
+ return err18(parseError(filePath, `Ruby parse failed: ${msg}`));
27091
27570
  }
27092
27571
  }
27093
27572
  walkNode(node, filePath, namespaceParts, symbols) {
@@ -27200,7 +27679,7 @@ var RubyLanguagePlugin = class {
27200
27679
 
27201
27680
  // src/indexer/plugins/language/go/index.ts
27202
27681
  import { createRequire as createRequire8 } from "module";
27203
- import { ok as ok16, err as err18 } from "neverthrow";
27682
+ import { ok as ok16, err as err19 } from "neverthrow";
27204
27683
 
27205
27684
  // src/indexer/plugins/language/go/version-features.ts
27206
27685
  var GO_SOURCE_PATTERNS = [
@@ -27435,7 +27914,7 @@ var GoLanguagePlugin = class {
27435
27914
  });
27436
27915
  } catch (e) {
27437
27916
  const msg = e instanceof Error ? e.message : String(e);
27438
- return err18(parseError(filePath, `Go parse failed: ${msg}`));
27917
+ return err19(parseError(filePath, `Go parse failed: ${msg}`));
27439
27918
  }
27440
27919
  }
27441
27920
  extractFunction(node, filePath, pkg, symbols) {
@@ -27942,7 +28421,7 @@ function lineAt2(source, offset) {
27942
28421
 
27943
28422
  // src/indexer/plugins/language/rust/index.ts
27944
28423
  import { createRequire as createRequire9 } from "module";
27945
- import { ok as ok19, err as err19 } from "neverthrow";
28424
+ import { ok as ok19, err as err20 } from "neverthrow";
27946
28425
 
27947
28426
  // src/indexer/plugins/language/rust/helpers.ts
27948
28427
  function makeSymbolId7(filePath, name, kind, parentName) {
@@ -28186,7 +28665,7 @@ var RustLanguagePlugin = class {
28186
28665
  });
28187
28666
  } catch (e) {
28188
28667
  const msg = e instanceof Error ? e.message : String(e);
28189
- return err19(parseError(filePath, `Rust parse failed: ${msg}`));
28668
+ return err20(parseError(filePath, `Rust parse failed: ${msg}`));
28190
28669
  }
28191
28670
  }
28192
28671
  walkTopLevel(root, filePath, symbols) {
@@ -28448,7 +28927,7 @@ var RustLanguagePlugin = class {
28448
28927
 
28449
28928
  // src/indexer/plugins/language/c/index.ts
28450
28929
  import { createRequire as createRequire10 } from "module";
28451
- import { ok as ok20, err as err20 } from "neverthrow";
28930
+ import { ok as ok20, err as err21 } from "neverthrow";
28452
28931
 
28453
28932
  // src/indexer/plugins/language/c/helpers.ts
28454
28933
  function makeSymbolId8(filePath, name, kind, parentName) {
@@ -28619,7 +29098,7 @@ var CLanguagePlugin = class {
28619
29098
  });
28620
29099
  } catch (e) {
28621
29100
  const msg = e instanceof Error ? e.message : String(e);
28622
- return err20(parseError(filePath, `C parse failed: ${msg}`));
29101
+ return err21(parseError(filePath, `C parse failed: ${msg}`));
28623
29102
  }
28624
29103
  }
28625
29104
  walkTopLevel(root, filePath, symbols) {
@@ -28866,7 +29345,7 @@ var CLanguagePlugin = class {
28866
29345
 
28867
29346
  // src/indexer/plugins/language/cpp/index.ts
28868
29347
  import { createRequire as createRequire11 } from "module";
28869
- import { ok as ok21, err as err21 } from "neverthrow";
29348
+ import { ok as ok21, err as err22 } from "neverthrow";
28870
29349
 
28871
29350
  // src/indexer/plugins/language/cpp/helpers.ts
28872
29351
  function makeSymbolId9(filePath, name, kind, parentName) {
@@ -29088,7 +29567,7 @@ var CppLanguagePlugin = class {
29088
29567
  });
29089
29568
  } catch (e) {
29090
29569
  const msg = e instanceof Error ? e.message : String(e);
29091
- return err21(parseError(filePath, `C++ parse failed: ${msg}`));
29570
+ return err22(parseError(filePath, `C++ parse failed: ${msg}`));
29092
29571
  }
29093
29572
  }
29094
29573
  /**
@@ -29485,7 +29964,7 @@ var CppLanguagePlugin = class {
29485
29964
 
29486
29965
  // src/indexer/plugins/language/csharp/index.ts
29487
29966
  import { createRequire as createRequire12 } from "module";
29488
- import { ok as ok22, err as err22 } from "neverthrow";
29967
+ import { ok as ok22, err as err23 } from "neverthrow";
29489
29968
 
29490
29969
  // src/indexer/plugins/language/csharp/helpers.ts
29491
29970
  function makeSymbolId10(relativePath, name, kind, parentName) {
@@ -29890,7 +30369,7 @@ var CSharpLanguagePlugin = class {
29890
30369
  });
29891
30370
  } catch (e) {
29892
30371
  const msg = e instanceof Error ? e.message : String(e);
29893
- return err22(parseError(filePath, `C# parse failed: ${msg}`));
30372
+ return err23(parseError(filePath, `C# parse failed: ${msg}`));
29894
30373
  }
29895
30374
  }
29896
30375
  /** Walk children of a node, dispatching to extraction methods by node type. */
@@ -30494,7 +30973,7 @@ var DartLanguagePlugin = class {
30494
30973
 
30495
30974
  // src/indexer/plugins/language/scala/index.ts
30496
30975
  import { createRequire as createRequire13 } from "module";
30497
- import { ok as ok25, err as err23 } from "neverthrow";
30976
+ import { ok as ok25, err as err24 } from "neverthrow";
30498
30977
 
30499
30978
  // src/indexer/plugins/language/scala/helpers.ts
30500
30979
  function makeSymbolId12(filePath, name, kind, parentName) {
@@ -30921,7 +31400,7 @@ var ScalaLanguagePlugin = class {
30921
31400
  });
30922
31401
  } catch (e) {
30923
31402
  const msg = e instanceof Error ? e.message : String(e);
30924
- return err23(parseError(filePath, `Scala parse failed: ${msg}`));
31403
+ return err24(parseError(filePath, `Scala parse failed: ${msg}`));
30925
31404
  }
30926
31405
  }
30927
31406
  walkTopLevel(node, filePath, fqnParts, symbols) {
@@ -32509,7 +32988,7 @@ var XmlLanguagePlugin = class {
32509
32988
 
32510
32989
  // src/indexer/plugins/integration/orm/prisma/index.ts
32511
32990
  import fs26 from "fs";
32512
- import path36 from "path";
32991
+ import path37 from "path";
32513
32992
  import { ok as ok28 } from "neverthrow";
32514
32993
  var PrismaLanguagePlugin = class {
32515
32994
  manifest = {
@@ -32543,8 +33022,8 @@ var PrismaPlugin = class {
32543
33022
  if ("@prisma/client" in deps || "prisma" in deps) return true;
32544
33023
  try {
32545
33024
  const candidates = [
32546
- path36.join(ctx.rootPath, "prisma", "schema.prisma"),
32547
- path36.join(ctx.rootPath, "schema.prisma")
33025
+ path37.join(ctx.rootPath, "prisma", "schema.prisma"),
33026
+ path37.join(ctx.rootPath, "schema.prisma")
32548
33027
  ];
32549
33028
  return candidates.some((p4) => fs26.existsSync(p4));
32550
33029
  } catch {
@@ -35476,7 +35955,7 @@ function createAllLanguagePlugins() {
35476
35955
 
35477
35956
  // src/indexer/plugins/integration/framework/laravel/index.ts
35478
35957
  import fs28 from "fs";
35479
- import path37 from "path";
35958
+ import path38 from "path";
35480
35959
  import { ok as ok34 } from "neverthrow";
35481
35960
 
35482
35961
  // src/indexer/plugins/integration/framework/laravel/routes.ts
@@ -37278,7 +37757,7 @@ var LaravelPlugin = class {
37278
37757
  deps = ctx.composerJson.require;
37279
37758
  } else {
37280
37759
  try {
37281
- const composerPath = path37.join(ctx.rootPath, "composer.json");
37760
+ const composerPath = path38.join(ctx.rootPath, "composer.json");
37282
37761
  const content = fs28.readFileSync(composerPath, "utf-8");
37283
37762
  const json = JSON.parse(content);
37284
37763
  deps = json.require;
@@ -37952,7 +38431,7 @@ var LaravelPlugin = class {
37952
38431
 
37953
38432
  // src/indexer/plugins/integration/framework/django/index.ts
37954
38433
  import fs29 from "fs";
37955
- import path38 from "path";
38434
+ import path39 from "path";
37956
38435
  import { ok as ok35 } from "neverthrow";
37957
38436
 
37958
38437
  // src/indexer/plugins/integration/framework/django/models.ts
@@ -38455,16 +38934,16 @@ var DjangoPlugin = class {
38455
38934
  dependencies: []
38456
38935
  };
38457
38936
  detect(ctx) {
38458
- if (ctx.configFiles.some((f) => path38.basename(f) === "manage.py")) {
38937
+ if (ctx.configFiles.some((f) => path39.basename(f) === "manage.py")) {
38459
38938
  return true;
38460
38939
  }
38461
38940
  try {
38462
- fs29.accessSync(path38.join(ctx.rootPath, "manage.py"), fs29.constants.F_OK);
38941
+ fs29.accessSync(path39.join(ctx.rootPath, "manage.py"), fs29.constants.F_OK);
38463
38942
  return true;
38464
38943
  } catch {
38465
38944
  }
38466
38945
  try {
38467
- const pyprojectPath = path38.join(ctx.rootPath, "pyproject.toml");
38946
+ const pyprojectPath = path39.join(ctx.rootPath, "pyproject.toml");
38468
38947
  const content = fs29.readFileSync(pyprojectPath, "utf-8");
38469
38948
  if (/django/i.test(content) && /dependencies|requires/i.test(content)) {
38470
38949
  return true;
@@ -38472,7 +38951,7 @@ var DjangoPlugin = class {
38472
38951
  } catch {
38473
38952
  }
38474
38953
  try {
38475
- const reqPath = path38.join(ctx.rootPath, "requirements.txt");
38954
+ const reqPath = path39.join(ctx.rootPath, "requirements.txt");
38476
38955
  const content = fs29.readFileSync(reqPath, "utf-8");
38477
38956
  if (/^django(?:==|>=|<=|~=|!=|\[|\s|$)/im.test(content)) {
38478
38957
  return true;
@@ -38480,7 +38959,7 @@ var DjangoPlugin = class {
38480
38959
  } catch {
38481
38960
  }
38482
38961
  try {
38483
- const setupPath = path38.join(ctx.rootPath, "setup.py");
38962
+ const setupPath = path39.join(ctx.rootPath, "setup.py");
38484
38963
  const content = fs29.readFileSync(setupPath, "utf-8");
38485
38964
  if (/['"]django['"]/i.test(content)) {
38486
38965
  return true;
@@ -38556,7 +39035,7 @@ var DjangoPlugin = class {
38556
39035
  let source;
38557
39036
  try {
38558
39037
  source = fs29.readFileSync(
38559
- path38.resolve(ctx.rootPath, file.path),
39038
+ path39.resolve(ctx.rootPath, file.path),
38560
39039
  "utf-8"
38561
39040
  );
38562
39041
  } catch {
@@ -39150,8 +39629,8 @@ var SpringPlugin = class {
39150
39629
  const re = new RegExp(`@${annotation}\\s*(?:\\(\\s*(?:value\\s*=\\s*)?["']([^"']*)["']\\s*\\))?`, "g");
39151
39630
  let m;
39152
39631
  while ((m = re.exec(source)) !== null) {
39153
- const path82 = m[1] ?? "";
39154
- const uri = normalizePath(classPrefix + "/" + path82);
39632
+ const path83 = m[1] ?? "";
39633
+ const uri = normalizePath(classPrefix + "/" + path83);
39155
39634
  result.routes.push({ method, uri, line: source.substring(0, m.index).split("\n").length });
39156
39635
  }
39157
39636
  }
@@ -39211,13 +39690,13 @@ var SpringPlugin = class {
39211
39690
  }
39212
39691
  }
39213
39692
  };
39214
- function normalizePath(path82) {
39215
- return "/" + path82.replace(/\/+/g, "/").replace(/^\/|\/$/g, "");
39693
+ function normalizePath(path83) {
39694
+ return "/" + path83.replace(/\/+/g, "/").replace(/^\/|\/$/g, "");
39216
39695
  }
39217
39696
 
39218
39697
  // src/indexer/plugins/integration/framework/express/index.ts
39219
39698
  import fs30 from "fs";
39220
- import path39 from "path";
39699
+ import path40 from "path";
39221
39700
  var ROUTE_RE = /(?:app|router)\s*\.\s*(get|post|put|delete|patch|head|options)\s*\(\s*['"`]([^'"`]+)['"`]/g;
39222
39701
  var MIDDLEWARE_RE = /(?:app|router)\s*\.\s*use\s*\(\s*['"`]([^'"`]+)['"`]/g;
39223
39702
  var GLOBAL_MIDDLEWARE_RE = /(?:app|router)\s*\.\s*use\s*\(\s*([A-Za-z][\w.]*(?:\s*\(\s*[^)]*\))?)\s*[,)]/g;
@@ -39287,7 +39766,7 @@ var ExpressPlugin = class {
39287
39766
  if ("express" in deps) return true;
39288
39767
  }
39289
39768
  try {
39290
- const pkgPath = path39.join(ctx.rootPath, "package.json");
39769
+ const pkgPath = path40.join(ctx.rootPath, "package.json");
39291
39770
  const content = fs30.readFileSync(pkgPath, "utf-8");
39292
39771
  const pkg = JSON.parse(content);
39293
39772
  const deps = {
@@ -39349,8 +39828,8 @@ var ExpressPlugin = class {
39349
39828
  // src/indexer/plugins/integration/framework/fastapi/index.ts
39350
39829
  import { createRequire as createRequire14 } from "module";
39351
39830
  import fs31 from "fs";
39352
- import path40 from "path";
39353
- import { ok as ok38, err as err24 } from "neverthrow";
39831
+ import path41 from "path";
39832
+ import { ok as ok38, err as err25 } from "neverthrow";
39354
39833
  var require15 = createRequire14(import.meta.url);
39355
39834
  var Parser13 = require15("tree-sitter");
39356
39835
  var PythonGrammar2 = require15("tree-sitter-python");
@@ -39363,14 +39842,14 @@ function hasPythonDep(ctx, pkg) {
39363
39842
  }
39364
39843
  if (ctx.requirementsTxt?.includes(lowerPkg)) return true;
39365
39844
  try {
39366
- const pyprojectPath = path40.join(ctx.rootPath, "pyproject.toml");
39845
+ const pyprojectPath = path41.join(ctx.rootPath, "pyproject.toml");
39367
39846
  const content = fs31.readFileSync(pyprojectPath, "utf-8");
39368
39847
  const re = new RegExp(`["']${escapeRegExp(pkg)}[>=<\\[!~\\s"']`, "i");
39369
39848
  if (re.test(content)) return true;
39370
39849
  } catch {
39371
39850
  }
39372
39851
  try {
39373
- const reqPath = path40.join(ctx.rootPath, "requirements.txt");
39852
+ const reqPath = path41.join(ctx.rootPath, "requirements.txt");
39374
39853
  const content = fs31.readFileSync(reqPath, "utf-8");
39375
39854
  const re = new RegExp(`^${escapeRegExp(pkg)}\\b`, "im");
39376
39855
  if (re.test(content)) return true;
@@ -39423,7 +39902,7 @@ var FastAPIPlugin = class {
39423
39902
  this.extractRoutes(root, source, filePath, result);
39424
39903
  this.extractRouterMounts(root, source, filePath, result);
39425
39904
  } catch (e) {
39426
- return err24(parseError(filePath, `FastAPI parse error: ${e instanceof Error ? e.message : String(e)}`));
39905
+ return err25(parseError(filePath, `FastAPI parse error: ${e instanceof Error ? e.message : String(e)}`));
39427
39906
  }
39428
39907
  if (result.routes.length > 0 || result.edges.length > 0) {
39429
39908
  result.frameworkRole = "fastapi_routes";
@@ -39666,8 +40145,8 @@ var FastAPIPlugin = class {
39666
40145
  // src/indexer/plugins/integration/framework/flask/index.ts
39667
40146
  import { createRequire as createRequire15 } from "module";
39668
40147
  import fs32 from "fs";
39669
- import path41 from "path";
39670
- import { ok as ok39, err as err25 } from "neverthrow";
40148
+ import path42 from "path";
40149
+ import { ok as ok39, err as err26 } from "neverthrow";
39671
40150
  var require16 = createRequire15(import.meta.url);
39672
40151
  var Parser14 = require16("tree-sitter");
39673
40152
  var PythonGrammar3 = require16("tree-sitter-python");
@@ -39680,14 +40159,14 @@ function hasPythonDep2(ctx, pkg) {
39680
40159
  }
39681
40160
  if (ctx.requirementsTxt?.includes(lowerPkg)) return true;
39682
40161
  try {
39683
- const pyprojectPath = path41.join(ctx.rootPath, "pyproject.toml");
40162
+ const pyprojectPath = path42.join(ctx.rootPath, "pyproject.toml");
39684
40163
  const content = fs32.readFileSync(pyprojectPath, "utf-8");
39685
40164
  const re = new RegExp(`["']${escapeRegExp(pkg)}[>=<\\[!~\\s"']`, "i");
39686
40165
  if (re.test(content)) return true;
39687
40166
  } catch {
39688
40167
  }
39689
40168
  try {
39690
- const reqPath = path41.join(ctx.rootPath, "requirements.txt");
40169
+ const reqPath = path42.join(ctx.rootPath, "requirements.txt");
39691
40170
  const content = fs32.readFileSync(reqPath, "utf-8");
39692
40171
  const re = new RegExp(`^${escapeRegExp(pkg)}\\b`, "im");
39693
40172
  if (re.test(content)) return true;
@@ -39741,7 +40220,7 @@ var FlaskPlugin = class {
39741
40220
  this.extractBeforeRequestHooks(root, source, filePath, result);
39742
40221
  this.extractErrorHandlers(root, source, filePath, result);
39743
40222
  } catch (e) {
39744
- return err25(parseError(filePath, `Flask parse error: ${e instanceof Error ? e.message : String(e)}`));
40223
+ return err26(parseError(filePath, `Flask parse error: ${e instanceof Error ? e.message : String(e)}`));
39745
40224
  }
39746
40225
  if (result.routes.length > 0 || result.edges.length > 0) {
39747
40226
  result.frameworkRole = "flask_routes";
@@ -39965,7 +40444,7 @@ var FlaskPlugin = class {
39965
40444
 
39966
40445
  // src/indexer/plugins/integration/framework/hono/index.ts
39967
40446
  import fs33 from "fs";
39968
- import path42 from "path";
40447
+ import path43 from "path";
39969
40448
  var ROUTE_RE2 = /(?:app|router|hono)\s*\.\s*(get|post|put|delete|patch|head|options|all)\s*\(\s*['"`]([^'"`]+)['"`]/g;
39970
40449
  var ON_RE = /(?:app|router|hono)\s*\.\s*on\s*\(\s*['"`]([^'"`]+)['"`]\s*,\s*['"`]([^'"`]+)['"`]/g;
39971
40450
  var ROUTE_GROUP_RE = /(?:app|router|hono)\s*\.\s*route\s*\(\s*['"`]([^'"`]+)['"`]\s*,\s*([A-Za-z][\w]*)/g;
@@ -40035,7 +40514,7 @@ var HonoPlugin = class {
40035
40514
  if ("hono" in deps) return true;
40036
40515
  }
40037
40516
  try {
40038
- const pkgPath = path42.join(ctx.rootPath, "package.json");
40517
+ const pkgPath = path43.join(ctx.rootPath, "package.json");
40039
40518
  const content = fs33.readFileSync(pkgPath, "utf-8");
40040
40519
  const pkg = JSON.parse(content);
40041
40520
  const deps = {
@@ -40091,7 +40570,7 @@ var HonoPlugin = class {
40091
40570
 
40092
40571
  // src/indexer/plugins/integration/framework/fastify/index.ts
40093
40572
  import fs34 from "fs";
40094
- import path43 from "path";
40573
+ import path44 from "path";
40095
40574
  var SHORTHAND_ROUTE_RE = /(?:fastify|app|server|instance)\s*\.\s*(get|post|put|delete|patch|head|options)\s*\(\s*['"`]([^'"`]+)['"`]/g;
40096
40575
  var ROUTE_OBJECT_RE = /\.route\s*\(\s*\{[^}]*?method\s*:\s*['"`]([^'"`]+)['"`][^}]*?url\s*:\s*['"`]([^'"`]+)['"`]/g;
40097
40576
  var HOOK_RE = /\.addHook\s*\(\s*['"`]([^'"`]+)['"`]/g;
@@ -40150,7 +40629,7 @@ var FastifyPlugin = class {
40150
40629
  if ("fastify" in deps) return true;
40151
40630
  }
40152
40631
  try {
40153
- const pkgPath = path43.join(ctx.rootPath, "package.json");
40632
+ const pkgPath = path44.join(ctx.rootPath, "package.json");
40154
40633
  const content = fs34.readFileSync(pkgPath, "utf-8");
40155
40634
  const pkg = JSON.parse(content);
40156
40635
  const deps = {
@@ -40205,7 +40684,7 @@ var FastifyPlugin = class {
40205
40684
 
40206
40685
  // src/indexer/plugins/integration/framework/nuxt/index.ts
40207
40686
  import fs35 from "fs";
40208
- import path44 from "path";
40687
+ import path45 from "path";
40209
40688
  function filePathToRoute(filePath, srcDir = ".") {
40210
40689
  const pagesPrefix = srcDir === "." ? "pages/" : `${srcDir}/pages/`;
40211
40690
  let route = filePath.replace(new RegExp(`^${pagesPrefix.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}`), "").replace(/\.vue$/, "");
@@ -40272,7 +40751,7 @@ var NuxtPlugin = class {
40272
40751
  }
40273
40752
  }
40274
40753
  try {
40275
- const configPath = path44.join(ctx.rootPath, "nuxt.config.ts");
40754
+ const configPath = path45.join(ctx.rootPath, "nuxt.config.ts");
40276
40755
  const configContent = fs35.readFileSync(configPath, "utf-8");
40277
40756
  if (/compatibilityVersion\s*:\s*4/.test(configContent)) {
40278
40757
  return true;
@@ -40280,7 +40759,7 @@ var NuxtPlugin = class {
40280
40759
  } catch {
40281
40760
  }
40282
40761
  try {
40283
- const appPagesDir = path44.join(ctx.rootPath, "app", "pages");
40762
+ const appPagesDir = path45.join(ctx.rootPath, "app", "pages");
40284
40763
  fs35.accessSync(appPagesDir);
40285
40764
  return true;
40286
40765
  } catch {
@@ -40304,7 +40783,7 @@ var NuxtPlugin = class {
40304
40783
  }
40305
40784
  }
40306
40785
  try {
40307
- const configPath = path44.join(ctx.rootPath, "nuxt.config.ts");
40786
+ const configPath = path45.join(ctx.rootPath, "nuxt.config.ts");
40308
40787
  fs35.accessSync(configPath);
40309
40788
  this.nuxt4 = this.isNuxt4(ctx);
40310
40789
  this.srcDir = this.nuxt4 ? "app" : ".";
@@ -40312,7 +40791,7 @@ var NuxtPlugin = class {
40312
40791
  } catch {
40313
40792
  }
40314
40793
  try {
40315
- const pkgPath = path44.join(ctx.rootPath, "package.json");
40794
+ const pkgPath = path45.join(ctx.rootPath, "package.json");
40316
40795
  const content = fs35.readFileSync(pkgPath, "utf-8");
40317
40796
  const pkg = JSON.parse(content);
40318
40797
  const deps = {
@@ -40414,7 +40893,7 @@ var NuxtPlugin = class {
40414
40893
  for (const file of vueFiles) {
40415
40894
  let source;
40416
40895
  try {
40417
- source = fs35.readFileSync(path44.resolve(ctx.rootPath, file.path), "utf-8");
40896
+ source = fs35.readFileSync(path45.resolve(ctx.rootPath, file.path), "utf-8");
40418
40897
  } catch {
40419
40898
  continue;
40420
40899
  }
@@ -40452,7 +40931,7 @@ var NuxtPlugin = class {
40452
40931
 
40453
40932
  // src/indexer/plugins/integration/framework/nextjs/index.ts
40454
40933
  import fs36 from "fs";
40455
- import path45 from "path";
40934
+ import path46 from "path";
40456
40935
  var PAGE_EXTENSIONS = /\.(tsx|ts|jsx|js)$/;
40457
40936
  var APP_ROUTER_FILES = ["page", "layout", "loading", "error", "not-found", "route", "template", "default"];
40458
40937
  var API_ROUTE_EXPORTS_RE = /export\s+(?:async\s+)?function\s+(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)\b/g;
@@ -40491,7 +40970,7 @@ function pagesRouterPathToRoute(filePath) {
40491
40970
  return "/" + routeSegments.join("/");
40492
40971
  }
40493
40972
  function getAppRouterFileType(filePath) {
40494
- const basename = path45.basename(filePath).replace(PAGE_EXTENSIONS, "");
40973
+ const basename = path46.basename(filePath).replace(PAGE_EXTENSIONS, "");
40495
40974
  if (APP_ROUTER_FILES.includes(basename)) {
40496
40975
  return basename;
40497
40976
  }
@@ -40556,7 +41035,7 @@ var NextJSPlugin = class {
40556
41035
  if ("next" in deps) return true;
40557
41036
  }
40558
41037
  try {
40559
- const pkgPath = path45.join(ctx.rootPath, "package.json");
41038
+ const pkgPath = path46.join(ctx.rootPath, "package.json");
40560
41039
  const content = fs36.readFileSync(pkgPath, "utf-8");
40561
41040
  const pkg = JSON.parse(content);
40562
41041
  const deps = {
@@ -40663,15 +41142,15 @@ var NextJSPlugin = class {
40663
41142
  const edges = [];
40664
41143
  const allFiles = ctx.getAllFiles();
40665
41144
  const layouts = allFiles.filter((f) => {
40666
- const basename = path45.basename(f.path).replace(PAGE_EXTENSIONS, "");
41145
+ const basename = path46.basename(f.path).replace(PAGE_EXTENSIONS, "");
40667
41146
  return f.path.startsWith("app/") && basename === "layout";
40668
41147
  });
40669
41148
  const pages = allFiles.filter((f) => {
40670
- const basename = path45.basename(f.path).replace(PAGE_EXTENSIONS, "");
41149
+ const basename = path46.basename(f.path).replace(PAGE_EXTENSIONS, "");
40671
41150
  return f.path.startsWith("app/") && basename === "page";
40672
41151
  });
40673
41152
  for (const layout of layouts) {
40674
- const layoutDir = path45.dirname(layout.path);
41153
+ const layoutDir = path46.dirname(layout.path);
40675
41154
  const layoutSymbols = ctx.getSymbolsByFile(layout.id);
40676
41155
  const layoutSym = layoutSymbols.find((s) => s.kind === "function" || s.kind === "class");
40677
41156
  if (!layoutSym) continue;
@@ -40701,7 +41180,7 @@ var NextJSPlugin = class {
40701
41180
  if (slotIdx < 1) continue;
40702
41181
  const parentDir = segments.slice(0, slotIdx).join("/");
40703
41182
  const parentLayout = allFiles.find(
40704
- (f) => f.path.startsWith(parentDir + "/") && !f.path.includes("@") && /layout\.(tsx|ts|jsx|js)$/.test(path45.basename(f.path))
41183
+ (f) => f.path.startsWith(parentDir + "/") && !f.path.includes("@") && /layout\.(tsx|ts|jsx|js)$/.test(path46.basename(f.path))
40705
41184
  );
40706
41185
  if (parentLayout) {
40707
41186
  const layoutSymbols = ctx.getSymbolsByFile(parentLayout.id);
@@ -40753,11 +41232,11 @@ var NextJSPlugin = class {
40753
41232
  }
40754
41233
  }
40755
41234
  const templates = allFiles.filter((f) => {
40756
- const basename = path45.basename(f.path).replace(PAGE_EXTENSIONS, "");
41235
+ const basename = path46.basename(f.path).replace(PAGE_EXTENSIONS, "");
40757
41236
  return f.path.startsWith("app/") && basename === "template";
40758
41237
  });
40759
41238
  for (const template of templates) {
40760
- const templateDir = path45.dirname(template.path);
41239
+ const templateDir = path46.dirname(template.path);
40761
41240
  const templateSymbols = ctx.getSymbolsByFile(template.id);
40762
41241
  const templateSym = templateSymbols.find((s) => s.kind === "function" || s.kind === "class");
40763
41242
  if (!templateSym) continue;
@@ -40782,7 +41261,7 @@ var NextJSPlugin = class {
40782
41261
  for (const file of pagesRouterFiles) {
40783
41262
  let source;
40784
41263
  try {
40785
- source = fs36.readFileSync(path45.resolve(ctx.rootPath, file.path), "utf-8");
41264
+ source = fs36.readFileSync(path46.resolve(ctx.rootPath, file.path), "utf-8");
40786
41265
  } catch {
40787
41266
  continue;
40788
41267
  }
@@ -40820,7 +41299,7 @@ var NextJSPlugin = class {
40820
41299
 
40821
41300
  // src/indexer/plugins/integration/framework/gin/index.ts
40822
41301
  import fs37 from "fs";
40823
- import path46 from "path";
41302
+ import path47 from "path";
40824
41303
  var ROUTE_RE3 = /\b(\w+)\s*\.\s*(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS|Any)\s*\(\s*"([^"]+)"/g;
40825
41304
  var GROUP_RE = /\b(\w+)\s*\.\s*Group\s*\(\s*"([^"]+)"/g;
40826
41305
  var MIDDLEWARE_RE2 = /\b(\w+)\s*\.\s*Use\s*\(\s*(\w[\w.]*)/g;
@@ -40876,13 +41355,13 @@ var GinPlugin = class {
40876
41355
  };
40877
41356
  detect(ctx) {
40878
41357
  try {
40879
- const goModPath = path46.join(ctx.rootPath, "go.mod");
41358
+ const goModPath = path47.join(ctx.rootPath, "go.mod");
40880
41359
  const content = fs37.readFileSync(goModPath, "utf-8");
40881
41360
  if (content.includes("github.com/gin-gonic/gin")) return true;
40882
41361
  } catch {
40883
41362
  }
40884
41363
  try {
40885
- const goSumPath = path46.join(ctx.rootPath, "go.sum");
41364
+ const goSumPath = path47.join(ctx.rootPath, "go.sum");
40886
41365
  const content = fs37.readFileSync(goSumPath, "utf-8");
40887
41366
  return content.includes("github.com/gin-gonic/gin");
40888
41367
  } catch {
@@ -40945,7 +41424,7 @@ var GinPlugin = class {
40945
41424
 
40946
41425
  // src/indexer/plugins/integration/framework/echo/index.ts
40947
41426
  import fs38 from "fs";
40948
- import path47 from "path";
41427
+ import path48 from "path";
40949
41428
  var ROUTE_RE4 = /\b(\w+)\s*\.\s*(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS|CONNECT|TRACE)\s*\(\s*"([^"]+)"/g;
40950
41429
  var GROUP_RE2 = /\b(\w+)\s*\.\s*Group\s*\(\s*"([^"]+)"/g;
40951
41430
  var MIDDLEWARE_RE3 = /\b(\w+)\s*\.\s*(?:Use|Pre)\s*\(\s*(\w[\w.]*)/g;
@@ -41001,13 +41480,13 @@ var EchoPlugin = class {
41001
41480
  };
41002
41481
  detect(ctx) {
41003
41482
  try {
41004
- const goModPath = path47.join(ctx.rootPath, "go.mod");
41483
+ const goModPath = path48.join(ctx.rootPath, "go.mod");
41005
41484
  const content = fs38.readFileSync(goModPath, "utf-8");
41006
41485
  if (content.includes("github.com/labstack/echo")) return true;
41007
41486
  } catch {
41008
41487
  }
41009
41488
  try {
41010
- const goSumPath = path47.join(ctx.rootPath, "go.sum");
41489
+ const goSumPath = path48.join(ctx.rootPath, "go.sum");
41011
41490
  const content = fs38.readFileSync(goSumPath, "utf-8");
41012
41491
  return content.includes("github.com/labstack/echo");
41013
41492
  } catch {
@@ -41176,7 +41655,7 @@ function extractTypeORMEntity(source, filePath) {
41176
41655
 
41177
41656
  // src/indexer/plugins/integration/orm/sequelize/index.ts
41178
41657
  import fs39 from "fs";
41179
- import path48 from "path";
41658
+ import path49 from "path";
41180
41659
  import { ok as ok41 } from "neverthrow";
41181
41660
  var SequelizePlugin = class {
41182
41661
  manifest = {
@@ -41193,7 +41672,7 @@ var SequelizePlugin = class {
41193
41672
  };
41194
41673
  if ("sequelize" in deps || "sequelize-typescript" in deps) return true;
41195
41674
  try {
41196
- const pkgPath = path48.join(ctx.rootPath, "package.json");
41675
+ const pkgPath = path49.join(ctx.rootPath, "package.json");
41197
41676
  const content = fs39.readFileSync(pkgPath, "utf-8");
41198
41677
  const json = JSON.parse(content);
41199
41678
  const allDeps = { ...json.dependencies, ...json.devDependencies };
@@ -41246,7 +41725,7 @@ var SequelizePlugin = class {
41246
41725
  return ok41([]);
41247
41726
  }
41248
41727
  isMigrationFile(filePath) {
41249
- return /migrations?\//.test(filePath) && /\d/.test(path48.basename(filePath));
41728
+ return /migrations?\//.test(filePath) && /\d/.test(path49.basename(filePath));
41250
41729
  }
41251
41730
  };
41252
41731
  function extractSequelizeModel(source, filePath) {
@@ -41515,7 +41994,7 @@ function extractScopes2(source, className) {
41515
41994
 
41516
41995
  // src/indexer/plugins/integration/orm/mongoose/index.ts
41517
41996
  import fs40 from "fs";
41518
- import path49 from "path";
41997
+ import path50 from "path";
41519
41998
  import { ok as ok42 } from "neverthrow";
41520
41999
  var MongoosePlugin = class {
41521
42000
  manifest = {
@@ -41532,7 +42011,7 @@ var MongoosePlugin = class {
41532
42011
  };
41533
42012
  if ("mongoose" in deps) return true;
41534
42013
  try {
41535
- const pkgPath = path49.join(ctx.rootPath, "package.json");
42014
+ const pkgPath = path50.join(ctx.rootPath, "package.json");
41536
42015
  const content = fs40.readFileSync(pkgPath, "utf-8");
41537
42016
  const json = JSON.parse(content);
41538
42017
  const allDeps = { ...json.dependencies, ...json.devDependencies };
@@ -41824,8 +42303,8 @@ function extractRefs(fields, sourceModelName) {
41824
42303
  // src/indexer/plugins/integration/orm/sqlalchemy/index.ts
41825
42304
  import { createRequire as createRequire16 } from "module";
41826
42305
  import fs41 from "fs";
41827
- import path50 from "path";
41828
- import { ok as ok43, err as err26 } from "neverthrow";
42306
+ import path51 from "path";
42307
+ import { ok as ok43, err as err27 } from "neverthrow";
41829
42308
  var require17 = createRequire16(import.meta.url);
41830
42309
  var Parser15 = require17("tree-sitter");
41831
42310
  var PythonGrammar4 = require17("tree-sitter-python");
@@ -41844,14 +42323,14 @@ function hasPythonDep3(ctx, pkg) {
41844
42323
  }
41845
42324
  if (ctx.requirementsTxt?.includes(lowerPkg)) return true;
41846
42325
  try {
41847
- const pyprojectPath = path50.join(ctx.rootPath, "pyproject.toml");
42326
+ const pyprojectPath = path51.join(ctx.rootPath, "pyproject.toml");
41848
42327
  const content = fs41.readFileSync(pyprojectPath, "utf-8");
41849
42328
  const re = new RegExp(`["']${escapeRegExp(pkg)}[>=<\\[!~\\s"']`, "i");
41850
42329
  if (re.test(content)) return true;
41851
42330
  } catch {
41852
42331
  }
41853
42332
  try {
41854
- const reqPath = path50.join(ctx.rootPath, "requirements.txt");
42333
+ const reqPath = path51.join(ctx.rootPath, "requirements.txt");
41855
42334
  const content = fs41.readFileSync(reqPath, "utf-8");
41856
42335
  const re = new RegExp(`^${escapeRegExp(pkg)}\\b`, "im");
41857
42336
  if (re.test(content)) return true;
@@ -41902,7 +42381,7 @@ var SQLAlchemyPlugin = class {
41902
42381
  const tree = parser.parse(source);
41903
42382
  this.extractAlembicMigrations(tree.rootNode, source, filePath, result);
41904
42383
  } catch (e) {
41905
- return err26(parseError(filePath, `SQLAlchemy migration parse error: ${e instanceof Error ? e.message : String(e)}`));
42384
+ return err27(parseError(filePath, `SQLAlchemy migration parse error: ${e instanceof Error ? e.message : String(e)}`));
41906
42385
  }
41907
42386
  result.frameworkRole = "alembic_migration";
41908
42387
  return ok43(result);
@@ -41919,7 +42398,7 @@ var SQLAlchemyPlugin = class {
41919
42398
  const root = tree.rootNode;
41920
42399
  this.extractModels(root, source, filePath, result);
41921
42400
  } catch (e) {
41922
- return err26(parseError(filePath, `SQLAlchemy parse error: ${e instanceof Error ? e.message : String(e)}`));
42401
+ return err27(parseError(filePath, `SQLAlchemy parse error: ${e instanceof Error ? e.message : String(e)}`));
41923
42402
  }
41924
42403
  return ok43(result);
41925
42404
  }
@@ -42009,7 +42488,7 @@ var SQLAlchemyPlugin = class {
42009
42488
  */
42010
42489
  extractAlembicMigrations(root, source, filePath, result) {
42011
42490
  const calls = this.findAllByType(root, "call");
42012
- const fileBaseName = path50.basename(filePath);
42491
+ const fileBaseName = path51.basename(filePath);
42013
42492
  const timestampMatch = fileBaseName.match(/^(\d+)_/);
42014
42493
  const timestamp = timestampMatch ? timestampMatch[1] : void 0;
42015
42494
  for (const call of calls) {
@@ -42410,7 +42889,7 @@ function toModelName(varName) {
42410
42889
 
42411
42890
  // src/indexer/plugins/integration/view/react/index.ts
42412
42891
  import { createRequire as createRequire17 } from "module";
42413
- import { ok as ok45, err as err27 } from "neverthrow";
42892
+ import { ok as ok45, err as err28 } from "neverthrow";
42414
42893
  var require18 = createRequire17(import.meta.url);
42415
42894
  var Parser16 = require18("tree-sitter");
42416
42895
  var TsGrammar3 = require18("tree-sitter-typescript");
@@ -42548,7 +43027,7 @@ var ReactPlugin = class {
42548
43027
  }
42549
43028
  }
42550
43029
  } catch (e) {
42551
- return err27(parseError(filePath, `tree-sitter parse failed: ${e}`));
43030
+ return err28(parseError(filePath, `tree-sitter parse failed: ${e}`));
42552
43031
  }
42553
43032
  return ok45(result);
42554
43033
  }
@@ -42662,7 +43141,7 @@ var ReactPlugin = class {
42662
43141
 
42663
43142
  // src/indexer/plugins/integration/view/vue/index.ts
42664
43143
  import fs42 from "fs";
42665
- import path51 from "path";
43144
+ import path52 from "path";
42666
43145
 
42667
43146
  // src/indexer/plugins/integration/view/vue/resolver.ts
42668
43147
  function toKebabCase(name) {
@@ -42701,7 +43180,7 @@ var VueFrameworkPlugin = class {
42701
43180
  return "vue" in deps;
42702
43181
  }
42703
43182
  try {
42704
- const pkgPath = path51.join(ctx.rootPath, "package.json");
43183
+ const pkgPath = path52.join(ctx.rootPath, "package.json");
42705
43184
  const content = fs42.readFileSync(pkgPath, "utf-8");
42706
43185
  const pkg = JSON.parse(content);
42707
43186
  const deps = {
@@ -42807,7 +43286,7 @@ var VueFrameworkPlugin = class {
42807
43286
 
42808
43287
  // src/indexer/plugins/integration/view/react-native/index.ts
42809
43288
  import fs43 from "fs";
42810
- import path52 from "path";
43289
+ import path53 from "path";
42811
43290
  import { ok as ok46 } from "neverthrow";
42812
43291
  function expoFileToRoute(filePath) {
42813
43292
  const normalized = filePath.replace(/\\/g, "/");
@@ -42845,7 +43324,7 @@ var ReactNativePlugin = class {
42845
43324
  return true;
42846
43325
  }
42847
43326
  try {
42848
- const pkgPath = path52.join(ctx.rootPath, "package.json");
43327
+ const pkgPath = path53.join(ctx.rootPath, "package.json");
42849
43328
  const content = fs43.readFileSync(pkgPath, "utf-8");
42850
43329
  const json = JSON.parse(content);
42851
43330
  const allDeps = { ...json.dependencies, ...json.devDependencies };
@@ -43079,8 +43558,8 @@ function extractExpoNavigationCalls(source) {
43079
43558
  }
43080
43559
  const templateRegex = /router\.(push|replace|navigate)\s*\(\s*`([^`]+)`/g;
43081
43560
  while ((match = templateRegex.exec(source)) !== null) {
43082
- const path82 = match[2].replace(/\$\{[^}]+\}/g, ":param");
43083
- paths.push(path82);
43561
+ const path83 = match[2].replace(/\$\{[^}]+\}/g, ":param");
43562
+ paths.push(path83);
43084
43563
  }
43085
43564
  const linkRegex = /<Link\s+[^>]*href\s*=\s*(?:\{?\s*)?['"]([^'"]+)['"]/g;
43086
43565
  while ((match = linkRegex.exec(source)) !== null) {
@@ -43092,9 +43571,9 @@ function extractExpoNavigationCalls(source) {
43092
43571
  }
43093
43572
  return [...new Set(paths)];
43094
43573
  }
43095
- function matchExpoRoute(path82, routePattern) {
43096
- if (path82 === routePattern) return true;
43097
- const pathParts = path82.split("/").filter(Boolean);
43574
+ function matchExpoRoute(path83, routePattern) {
43575
+ if (path83 === routePattern) return true;
43576
+ const pathParts = path83.split("/").filter(Boolean);
43098
43577
  const routeParts = routePattern.split("/").filter(Boolean);
43099
43578
  if (pathParts.length !== routeParts.length) {
43100
43579
  if (routeParts[routeParts.length - 1] === "*" && pathParts.length >= routeParts.length - 1) {
@@ -43153,7 +43632,7 @@ function extractNativeModuleNames(source) {
43153
43632
 
43154
43633
  // src/indexer/plugins/integration/view/blade/index.ts
43155
43634
  import fs44 from "fs";
43156
- import path53 from "path";
43635
+ import path54 from "path";
43157
43636
  var EXTENDS_RE = /@extends\(\s*['"]([\w.-]+)['"]\s*\)/g;
43158
43637
  var INCLUDE_RE = /@include(?:If|When|Unless|First)?\(\s*['"]([\w.-]+)['"]/g;
43159
43638
  var COMPONENT_RE = /@component\(\s*['"]([\w.-]+)['"]/g;
@@ -43211,7 +43690,7 @@ var BladePlugin = class {
43211
43690
  };
43212
43691
  detect(ctx) {
43213
43692
  try {
43214
- const viewsDir = path53.join(ctx.rootPath, "resources", "views");
43693
+ const viewsDir = path54.join(ctx.rootPath, "resources", "views");
43215
43694
  const stat = fs44.statSync(viewsDir);
43216
43695
  if (!stat.isDirectory()) return false;
43217
43696
  return this.hasBlade(viewsDir);
@@ -43298,7 +43777,7 @@ var BladePlugin = class {
43298
43777
  for (const entry of entries) {
43299
43778
  if (entry.isFile() && entry.name.endsWith(".blade.php")) return true;
43300
43779
  if (entry.isDirectory()) {
43301
- if (this.hasBlade(path53.join(dir, entry.name))) return true;
43780
+ if (this.hasBlade(path54.join(dir, entry.name))) return true;
43302
43781
  }
43303
43782
  }
43304
43783
  } catch {
@@ -43309,7 +43788,7 @@ var BladePlugin = class {
43309
43788
 
43310
43789
  // src/indexer/plugins/integration/view/inertia/index.ts
43311
43790
  import fs45 from "fs";
43312
- import path54 from "path";
43791
+ import path55 from "path";
43313
43792
  var INERTIA_RENDER_RE = /(?:Inertia::render|inertia)\(\s*['"]([\w/.-]+)['"]\s*(?:,\s*\[([^\]]*)\])?\s*\)/g;
43314
43793
  var ARRAY_KEY_RE = /['"](\w+)['"]\s*=>/g;
43315
43794
  function extractInertiaRenders(source) {
@@ -43355,7 +43834,7 @@ var InertiaPlugin = class {
43355
43834
  if ("@inertiajs/vue3" in deps || "@inertiajs/react" in deps) return true;
43356
43835
  }
43357
43836
  try {
43358
- const composerPath = path54.join(ctx.rootPath, "composer.json");
43837
+ const composerPath = path55.join(ctx.rootPath, "composer.json");
43359
43838
  const content = fs45.readFileSync(composerPath, "utf-8");
43360
43839
  const json = JSON.parse(content);
43361
43840
  const req = json.require;
@@ -43363,7 +43842,7 @@ var InertiaPlugin = class {
43363
43842
  } catch {
43364
43843
  }
43365
43844
  try {
43366
- const pkgPath = path54.join(ctx.rootPath, "package.json");
43845
+ const pkgPath = path55.join(ctx.rootPath, "package.json");
43367
43846
  const content = fs45.readFileSync(pkgPath, "utf-8");
43368
43847
  const pkg = JSON.parse(content);
43369
43848
  const deps = {
@@ -43405,7 +43884,7 @@ var InertiaPlugin = class {
43405
43884
  if (file.language !== "php") continue;
43406
43885
  let source;
43407
43886
  try {
43408
- source = fs45.readFileSync(path54.resolve(ctx.rootPath, file.path), "utf-8");
43887
+ source = fs45.readFileSync(path55.resolve(ctx.rootPath, file.path), "utf-8");
43409
43888
  } catch {
43410
43889
  continue;
43411
43890
  }
@@ -43457,7 +43936,7 @@ var InertiaPlugin = class {
43457
43936
 
43458
43937
  // src/indexer/plugins/integration/view/shadcn/index.ts
43459
43938
  import fs46 from "fs";
43460
- import path55 from "path";
43939
+ import path56 from "path";
43461
43940
  import { ok as ok47 } from "neverthrow";
43462
43941
  var CVA_RE = /(?:export\s+(?:default\s+)?)?(?:const|let)\s+(\w+)\s*=\s*cva\s*\(/g;
43463
43942
  function extractCvaDefinitions(source) {
@@ -43644,7 +44123,7 @@ function extractShadcnComponents(source, filePath) {
43644
44123
  return components;
43645
44124
  }
43646
44125
  function extractShadcnVueComponent(source, filePath) {
43647
- const fileName = path55.basename(filePath, path55.extname(filePath));
44126
+ const fileName = path56.basename(filePath, path56.extname(filePath));
43648
44127
  const name = toPascalCase2(fileName);
43649
44128
  const hasRadixVue = /from\s+['"]radix-vue['"]/.test(source) || /from\s+['"]reka-ui['"]/.test(source);
43650
44129
  const hasCn = /\bcn\s*\(/.test(source);
@@ -43781,7 +44260,7 @@ function scanInstalledComponents(rootPath, config) {
43781
44260
  "app/components/ui"
43782
44261
  ].filter(Boolean);
43783
44262
  for (const relDir of possibleDirs) {
43784
- const absDir = path55.resolve(rootPath, relDir);
44263
+ const absDir = path56.resolve(rootPath, relDir);
43785
44264
  try {
43786
44265
  const entries = fs46.readdirSync(absDir, { withFileTypes: true });
43787
44266
  for (const entry of entries) {
@@ -43790,17 +44269,17 @@ function scanInstalledComponents(rootPath, config) {
43790
44269
  components.push({
43791
44270
  name: toPascalCase2(baseName),
43792
44271
  fileName: entry.name,
43793
- relativePath: path55.join(relDir, entry.name)
44272
+ relativePath: path56.join(relDir, entry.name)
43794
44273
  });
43795
44274
  } else if (entry.isDirectory()) {
43796
44275
  try {
43797
- const subEntries = fs46.readdirSync(path55.join(absDir, entry.name));
44276
+ const subEntries = fs46.readdirSync(path56.join(absDir, entry.name));
43798
44277
  const indexFile = subEntries.find((f) => /^index\.(tsx|vue|ts|jsx)$/.test(f));
43799
44278
  if (indexFile) {
43800
44279
  components.push({
43801
44280
  name: toPascalCase2(entry.name),
43802
44281
  fileName: indexFile,
43803
- relativePath: path55.join(relDir, entry.name, indexFile)
44282
+ relativePath: path56.join(relDir, entry.name, indexFile)
43804
44283
  });
43805
44284
  }
43806
44285
  for (const sub of subEntries) {
@@ -43809,7 +44288,7 @@ function scanInstalledComponents(rootPath, config) {
43809
44288
  components.push({
43810
44289
  name: toPascalCase2(baseName),
43811
44290
  fileName: sub,
43812
- relativePath: path55.join(relDir, entry.name, sub)
44291
+ relativePath: path56.join(relDir, entry.name, sub)
43813
44292
  });
43814
44293
  }
43815
44294
  }
@@ -43842,7 +44321,7 @@ var ShadcnPlugin = class {
43842
44321
  detect(ctx) {
43843
44322
  this.rootPath = ctx.rootPath;
43844
44323
  try {
43845
- const configPath = path55.join(ctx.rootPath, "components.json");
44324
+ const configPath = path56.join(ctx.rootPath, "components.json");
43846
44325
  const raw = fs46.readFileSync(configPath, "utf-8");
43847
44326
  this.config = JSON.parse(raw);
43848
44327
  this.scanComponents(ctx);
@@ -44720,7 +45199,7 @@ var HeadlessUiPlugin = class {
44720
45199
 
44721
45200
  // src/indexer/plugins/integration/view/nuxt-ui/index.ts
44722
45201
  import fs47 from "fs";
44723
- import path56 from "path";
45202
+ import path57 from "path";
44724
45203
  import { ok as ok51 } from "neverthrow";
44725
45204
  var NUXT_UI_V3_COMPONENTS = /* @__PURE__ */ new Set([
44726
45205
  "UAccordion",
@@ -44976,7 +45455,7 @@ var NuxtUiPlugin = class {
44976
45455
  this.hasPro = "@nuxt/ui-pro" in deps;
44977
45456
  if (!hasNuxtUi) {
44978
45457
  try {
44979
- const configPath = path56.join(ctx.rootPath, "nuxt.config.ts");
45458
+ const configPath = path57.join(ctx.rootPath, "nuxt.config.ts");
44980
45459
  const configContent = fs47.readFileSync(configPath, "utf-8");
44981
45460
  if (/@nuxt\/ui/.test(configContent)) return true;
44982
45461
  } catch {
@@ -45177,7 +45656,7 @@ function extractBraceBody4(source, pos) {
45177
45656
 
45178
45657
  // src/indexer/plugins/integration/view/angular/index.ts
45179
45658
  import fs48 from "fs";
45180
- import path57 from "path";
45659
+ import path58 from "path";
45181
45660
  var COMPONENT_RE2 = /@Component\s*\(\s*\{[^}]*selector\s*:\s*['"]([^'"]+)['"]/gs;
45182
45661
  var INJECTABLE_RE2 = /@Injectable\s*\(/g;
45183
45662
  var NGMODULE_RE = /@NgModule\s*\(/g;
@@ -45225,7 +45704,7 @@ var AngularPlugin = class {
45225
45704
  if ("@angular/core" in deps) return true;
45226
45705
  }
45227
45706
  try {
45228
- const pkgPath = path57.join(ctx.rootPath, "package.json");
45707
+ const pkgPath = path58.join(ctx.rootPath, "package.json");
45229
45708
  const content = fs48.readFileSync(pkgPath, "utf-8");
45230
45709
  const pkg = JSON.parse(content);
45231
45710
  const deps = {
@@ -45546,7 +46025,7 @@ function isHtmlTag(tag) {
45546
46025
 
45547
46026
  // src/indexer/plugins/integration/view/svelte/index.ts
45548
46027
  import fs49 from "fs";
45549
- import path58 from "path";
46028
+ import path59 from "path";
45550
46029
  var EXPORT_LET_RE = /export\s+let\s+(\w+)/g;
45551
46030
  var SLOT_RE = /<slot(?:\s+name\s*=\s*['"]([^'"]+)['"])?\s*\/?>/g;
45552
46031
  var DISPATCH_RE2 = /dispatch\s*\(\s*['"]([^'"]+)['"]/g;
@@ -45564,11 +46043,11 @@ function extractTemplate(source) {
45564
46043
  return source.replace(/<script[^>]*>[\s\S]*?<\/script>/g, "").replace(/<style[^>]*>[\s\S]*?<\/style>/g, "");
45565
46044
  }
45566
46045
  function isSvelteKitRouteFile(filePath) {
45567
- const base = path58.basename(filePath);
46046
+ const base = path59.basename(filePath);
45568
46047
  return /^\+(page|layout|server|error)/.test(base);
45569
46048
  }
45570
46049
  function isSvelteKitHooksFile(filePath) {
45571
- const base = path58.basename(filePath);
46050
+ const base = path59.basename(filePath);
45572
46051
  return /^hooks\.(server|client)\.(ts|js)$/.test(base);
45573
46052
  }
45574
46053
  function extractRouteUri(filePath) {
@@ -45576,11 +46055,11 @@ function extractRouteUri(filePath) {
45576
46055
  const routesIdx = normalized.indexOf("/routes/");
45577
46056
  if (routesIdx === -1) return "/";
45578
46057
  const afterRoutes = normalized.substring(routesIdx + "/routes".length);
45579
- const dir = path58.posix.dirname(afterRoutes);
46058
+ const dir = path59.posix.dirname(afterRoutes);
45580
46059
  return dir === "." ? "/" : dir;
45581
46060
  }
45582
46061
  function componentNameFromPath2(filePath) {
45583
- const base = path58.basename(filePath, ".svelte");
46062
+ const base = path59.basename(filePath, ".svelte");
45584
46063
  if (base.startsWith("+")) return base;
45585
46064
  return base;
45586
46065
  }
@@ -45607,7 +46086,7 @@ var SveltePlugin = class {
45607
46086
  if ("svelte" in deps || "@sveltejs/kit" in deps) return true;
45608
46087
  }
45609
46088
  try {
45610
- const pkgPath = path58.join(ctx.rootPath, "package.json");
46089
+ const pkgPath = path59.join(ctx.rootPath, "package.json");
45611
46090
  const content = fs49.readFileSync(pkgPath, "utf-8");
45612
46091
  const pkg = JSON.parse(content);
45613
46092
  const deps = {
@@ -45683,7 +46162,7 @@ var SveltePlugin = class {
45683
46162
  const name = componentNameFromPath2(filePath);
45684
46163
  const isRouteFile = isSvelteKitRouteFile(filePath);
45685
46164
  let kind = "component";
45686
- const base = path58.basename(filePath);
46165
+ const base = path59.basename(filePath);
45687
46166
  if (base === "+page.svelte") kind = "page";
45688
46167
  else if (base === "+layout.svelte") kind = "layout";
45689
46168
  else if (base === "+error.svelte") kind = "component";
@@ -45764,7 +46243,7 @@ var SveltePlugin = class {
45764
46243
  }
45765
46244
  }
45766
46245
  extractSvelteKitServerFile(filePath, source, result) {
45767
- const base = path58.basename(filePath);
46246
+ const base = path59.basename(filePath);
45768
46247
  if (!isSvelteKitRouteFile(filePath) && !isSvelteKitHooksFile(filePath)) {
45769
46248
  return;
45770
46249
  }
@@ -45856,7 +46335,7 @@ var SveltePlugin = class {
45856
46335
 
45857
46336
  // src/indexer/plugins/integration/api/trpc/index.ts
45858
46337
  import fs50 from "fs";
45859
- import path59 from "path";
46338
+ import path60 from "path";
45860
46339
  var PROCEDURE_RE = /(\w+)\s*:\s*\w*[Pp]rocedure[\s\S]{0,500}?\.(query|mutation|subscription)\s*\(/g;
45861
46340
  var ROUTER_RE = /(?:t\.router|router)\s*\(\s*\{/g;
45862
46341
  function extractTrpcProcedures(source) {
@@ -45888,7 +46367,7 @@ var TrpcPlugin = class {
45888
46367
  if ("@trpc/server" in deps) return true;
45889
46368
  }
45890
46369
  try {
45891
- const pkgPath = path59.join(ctx.rootPath, "package.json");
46370
+ const pkgPath = path60.join(ctx.rootPath, "package.json");
45892
46371
  const content = fs50.readFileSync(pkgPath, "utf-8");
45893
46372
  const pkg = JSON.parse(content);
45894
46373
  const deps = {
@@ -45937,8 +46416,8 @@ var TrpcPlugin = class {
45937
46416
  // src/indexer/plugins/integration/api/drf/index.ts
45938
46417
  import { createRequire as createRequire18 } from "module";
45939
46418
  import fs51 from "fs";
45940
- import path60 from "path";
45941
- import { ok as ok52, err as err28 } from "neverthrow";
46419
+ import path61 from "path";
46420
+ import { ok as ok52, err as err29 } from "neverthrow";
45942
46421
  var require19 = createRequire18(import.meta.url);
45943
46422
  var Parser17 = require19("tree-sitter");
45944
46423
  var PythonGrammar5 = require19("tree-sitter-python");
@@ -45953,25 +46432,25 @@ function getParser14() {
45953
46432
  function hasPythonDep4(rootPath, depName) {
45954
46433
  for (const reqFile of ["requirements.txt", "requirements/base.txt", "requirements/prod.txt"]) {
45955
46434
  try {
45956
- const content = fs51.readFileSync(path60.join(rootPath, reqFile), "utf-8");
46435
+ const content = fs51.readFileSync(path61.join(rootPath, reqFile), "utf-8");
45957
46436
  if (new RegExp(`^${escapeRegExp(depName)}\\b`, "m").test(content)) return true;
45958
46437
  } catch {
45959
46438
  }
45960
46439
  }
45961
46440
  try {
45962
- const content = fs51.readFileSync(path60.join(rootPath, "pyproject.toml"), "utf-8");
46441
+ const content = fs51.readFileSync(path61.join(rootPath, "pyproject.toml"), "utf-8");
45963
46442
  if (content.includes(depName)) return true;
45964
46443
  } catch {
45965
46444
  }
45966
46445
  for (const f of ["setup.py", "setup.cfg"]) {
45967
46446
  try {
45968
- const content = fs51.readFileSync(path60.join(rootPath, f), "utf-8");
46447
+ const content = fs51.readFileSync(path61.join(rootPath, f), "utf-8");
45969
46448
  if (content.includes(depName)) return true;
45970
46449
  } catch {
45971
46450
  }
45972
46451
  }
45973
46452
  try {
45974
- const content = fs51.readFileSync(path60.join(rootPath, "Pipfile"), "utf-8");
46453
+ const content = fs51.readFileSync(path61.join(rootPath, "Pipfile"), "utf-8");
45975
46454
  if (content.includes(depName)) return true;
45976
46455
  } catch {
45977
46456
  }
@@ -46219,7 +46698,7 @@ var DRFPlugin = class {
46219
46698
  const parser = getParser14();
46220
46699
  tree = parser.parse(source);
46221
46700
  } catch (e) {
46222
- return err28(parseError(filePath, `tree-sitter parse failed: ${e}`));
46701
+ return err29(parseError(filePath, `tree-sitter parse failed: ${e}`));
46223
46702
  }
46224
46703
  const root = tree.rootNode;
46225
46704
  const serializers = extractSerializers(root);
@@ -46286,7 +46765,7 @@ var DRFPlugin = class {
46286
46765
 
46287
46766
  // src/indexer/plugins/integration/validation/zod/index.ts
46288
46767
  import fs52 from "fs";
46289
- import path61 from "path";
46768
+ import path62 from "path";
46290
46769
  var ZOD_OBJECT_RE = /(?:export\s+(?:default\s+)?)?(?:const|let|var)\s+(\w+)\s*=\s*z\.object\s*\(\s*\{([^]*?)\}\s*\)/g;
46291
46770
  var ZOD_FIELD_RE = /(\w+)\s*:\s*z\.(\w+)\s*\(([^)]*)\)([.\w()]*)/g;
46292
46771
  function resolveFieldType(baseType, chain) {
@@ -46341,7 +46820,7 @@ var ZodPlugin = class {
46341
46820
  if ("zod" in deps) return true;
46342
46821
  }
46343
46822
  try {
46344
- const pkgPath = path61.join(ctx.rootPath, "package.json");
46823
+ const pkgPath = path62.join(ctx.rootPath, "package.json");
46345
46824
  const content = fs52.readFileSync(pkgPath, "utf-8");
46346
46825
  const pkg = JSON.parse(content);
46347
46826
  const deps = {
@@ -46387,8 +46866,8 @@ var ZodPlugin = class {
46387
46866
  // src/indexer/plugins/integration/validation/pydantic/index.ts
46388
46867
  import { createRequire as createRequire19 } from "module";
46389
46868
  import fs53 from "fs";
46390
- import path62 from "path";
46391
- import { ok as ok53, err as err29 } from "neverthrow";
46869
+ import path63 from "path";
46870
+ import { ok as ok53, err as err30 } from "neverthrow";
46392
46871
  var require20 = createRequire19(import.meta.url);
46393
46872
  var Parser18 = require20("tree-sitter");
46394
46873
  var PythonGrammar6 = require20("tree-sitter-python");
@@ -46403,25 +46882,25 @@ function getParser15() {
46403
46882
  function hasPythonDep5(rootPath, depName) {
46404
46883
  for (const reqFile of ["requirements.txt", "requirements/base.txt", "requirements/prod.txt"]) {
46405
46884
  try {
46406
- const content = fs53.readFileSync(path62.join(rootPath, reqFile), "utf-8");
46885
+ const content = fs53.readFileSync(path63.join(rootPath, reqFile), "utf-8");
46407
46886
  if (new RegExp(`^${escapeRegExp(depName)}\\b`, "m").test(content)) return true;
46408
46887
  } catch {
46409
46888
  }
46410
46889
  }
46411
46890
  try {
46412
- const content = fs53.readFileSync(path62.join(rootPath, "pyproject.toml"), "utf-8");
46891
+ const content = fs53.readFileSync(path63.join(rootPath, "pyproject.toml"), "utf-8");
46413
46892
  if (content.includes(depName)) return true;
46414
46893
  } catch {
46415
46894
  }
46416
46895
  for (const f of ["setup.py", "setup.cfg"]) {
46417
46896
  try {
46418
- const content = fs53.readFileSync(path62.join(rootPath, f), "utf-8");
46897
+ const content = fs53.readFileSync(path63.join(rootPath, f), "utf-8");
46419
46898
  if (content.includes(depName)) return true;
46420
46899
  } catch {
46421
46900
  }
46422
46901
  }
46423
46902
  try {
46424
- const content = fs53.readFileSync(path62.join(rootPath, "Pipfile"), "utf-8");
46903
+ const content = fs53.readFileSync(path63.join(rootPath, "Pipfile"), "utf-8");
46425
46904
  if (content.includes(depName)) return true;
46426
46905
  } catch {
46427
46906
  }
@@ -46734,7 +47213,7 @@ var PydanticPlugin = class {
46734
47213
  const parser = getParser15();
46735
47214
  tree = parser.parse(source);
46736
47215
  } catch (e) {
46737
- return err29(parseError(filePath, `tree-sitter parse failed: ${e}`));
47216
+ return err30(parseError(filePath, `tree-sitter parse failed: ${e}`));
46738
47217
  }
46739
47218
  const root = tree.rootNode;
46740
47219
  const models = extractPydanticModels(root);
@@ -46965,7 +47444,7 @@ function extractBraceBody5(source, pos) {
46965
47444
 
46966
47445
  // src/indexer/plugins/integration/realtime/socketio/index.ts
46967
47446
  import fs54 from "fs";
46968
- import path63 from "path";
47447
+ import path64 from "path";
46969
47448
  var LISTENER_RE = /(?:socket|io|server|namespace)\s*\.\s*on\s*\(\s*['"`]([^'"`]+)['"`]/g;
46970
47449
  var EMITTER_RE = /(?:socket|io|server|namespace)(?:\.broadcast)?\s*\.\s*emit\s*\(\s*['"`]([^'"`]+)['"`]/g;
46971
47450
  var NAMESPACE_RE6 = /(?:io|server)\s*\.\s*of\s*\(\s*['"`]([^'"`]+)['"`]/g;
@@ -47008,7 +47487,7 @@ var SocketIoPlugin = class {
47008
47487
  if ("socket.io" in deps) return true;
47009
47488
  }
47010
47489
  try {
47011
- const pkgPath = path63.join(ctx.rootPath, "package.json");
47490
+ const pkgPath = path64.join(ctx.rootPath, "package.json");
47012
47491
  const content = fs54.readFileSync(pkgPath, "utf-8");
47013
47492
  const pkg = JSON.parse(content);
47014
47493
  const deps = {
@@ -47064,7 +47543,7 @@ var SocketIoPlugin = class {
47064
47543
 
47065
47544
  // src/indexer/plugins/integration/testing/testing/index.ts
47066
47545
  import fs55 from "fs";
47067
- import path64 from "path";
47546
+ import path65 from "path";
47068
47547
  var PAGE_GOTO_RE = /page\.goto\s*\(\s*['"`]([^'"`]+)['"`]/g;
47069
47548
  var CY_VISIT_RE = /cy\.visit\s*\(\s*['"`]([^'"`]+)['"`]/g;
47070
47549
  var REQUEST_METHOD_RE = /request\s*\.\s*(get|post|put|delete|patch)\s*\(\s*['"`]([^'"`]+)['"`]/g;
@@ -47171,7 +47650,7 @@ var TestingPlugin = class {
47171
47650
  }
47172
47651
  }
47173
47652
  try {
47174
- const pkgPath = path64.join(ctx.rootPath, "package.json");
47653
+ const pkgPath = path65.join(ctx.rootPath, "package.json");
47175
47654
  const content = fs55.readFileSync(pkgPath, "utf-8");
47176
47655
  const pkg = JSON.parse(content);
47177
47656
  const deps = {
@@ -47245,8 +47724,8 @@ var TestingPlugin = class {
47245
47724
  // src/indexer/plugins/integration/tooling/celery/index.ts
47246
47725
  import { createRequire as createRequire20 } from "module";
47247
47726
  import fs56 from "fs";
47248
- import path65 from "path";
47249
- import { ok as ok55, err as err30 } from "neverthrow";
47727
+ import path66 from "path";
47728
+ import { ok as ok55, err as err31 } from "neverthrow";
47250
47729
  var require21 = createRequire20(import.meta.url);
47251
47730
  var Parser19 = require21("tree-sitter");
47252
47731
  var PythonGrammar7 = require21("tree-sitter-python");
@@ -47261,25 +47740,25 @@ function getParser16() {
47261
47740
  function hasPythonDep6(rootPath, depName) {
47262
47741
  for (const reqFile of ["requirements.txt", "requirements/base.txt", "requirements/prod.txt"]) {
47263
47742
  try {
47264
- const content = fs56.readFileSync(path65.join(rootPath, reqFile), "utf-8");
47743
+ const content = fs56.readFileSync(path66.join(rootPath, reqFile), "utf-8");
47265
47744
  if (new RegExp(`^${escapeRegExp(depName)}\\b`, "m").test(content)) return true;
47266
47745
  } catch {
47267
47746
  }
47268
47747
  }
47269
47748
  try {
47270
- const content = fs56.readFileSync(path65.join(rootPath, "pyproject.toml"), "utf-8");
47749
+ const content = fs56.readFileSync(path66.join(rootPath, "pyproject.toml"), "utf-8");
47271
47750
  if (content.includes(depName)) return true;
47272
47751
  } catch {
47273
47752
  }
47274
47753
  for (const f of ["setup.py", "setup.cfg"]) {
47275
47754
  try {
47276
- const content = fs56.readFileSync(path65.join(rootPath, f), "utf-8");
47755
+ const content = fs56.readFileSync(path66.join(rootPath, f), "utf-8");
47277
47756
  if (content.includes(depName)) return true;
47278
47757
  } catch {
47279
47758
  }
47280
47759
  }
47281
47760
  try {
47282
- const content = fs56.readFileSync(path65.join(rootPath, "Pipfile"), "utf-8");
47761
+ const content = fs56.readFileSync(path66.join(rootPath, "Pipfile"), "utf-8");
47283
47762
  if (content.includes(depName)) return true;
47284
47763
  } catch {
47285
47764
  }
@@ -47451,7 +47930,7 @@ var CeleryPlugin = class {
47451
47930
  const parser = getParser16();
47452
47931
  tree = parser.parse(source);
47453
47932
  } catch (e) {
47454
- return err30(parseError(filePath, `tree-sitter parse failed: ${e}`));
47933
+ return err31(parseError(filePath, `tree-sitter parse failed: ${e}`));
47455
47934
  }
47456
47935
  const root = tree.rootNode;
47457
47936
  const tasks = extractCeleryTasks(root);
@@ -47528,7 +48007,7 @@ var CeleryPlugin = class {
47528
48007
 
47529
48008
  // src/indexer/plugins/integration/tooling/n8n/index.ts
47530
48009
  import fs57 from "fs";
47531
- import path66 from "path";
48010
+ import path67 from "path";
47532
48011
  var CODE_TYPES = /* @__PURE__ */ new Set([
47533
48012
  "n8n-nodes-base.code",
47534
48013
  "n8n-nodes-base.function",
@@ -48158,7 +48637,7 @@ function collectNodeFiles(dir) {
48158
48637
  try {
48159
48638
  const entries = fs57.readdirSync(dir, { withFileTypes: true });
48160
48639
  for (const entry of entries) {
48161
- const fullPath = path66.join(dir, entry.name);
48640
+ const fullPath = path67.join(dir, entry.name);
48162
48641
  if (entry.isDirectory()) {
48163
48642
  results.push(...collectNodeFiles(fullPath));
48164
48643
  } else if (entry.name.endsWith(".node.ts")) {
@@ -48418,13 +48897,13 @@ var N8nPlugin = class {
48418
48897
  }
48419
48898
  }
48420
48899
  try {
48421
- if (fs57.existsSync(path66.join(ctx.rootPath, ".n8n"))) return true;
48900
+ if (fs57.existsSync(path67.join(ctx.rootPath, ".n8n"))) return true;
48422
48901
  } catch {
48423
48902
  }
48424
48903
  const nodeDirs = ["nodes", "src/nodes"];
48425
48904
  for (const dir of nodeDirs) {
48426
48905
  try {
48427
- const fullDir = path66.join(ctx.rootPath, dir);
48906
+ const fullDir = path67.join(ctx.rootPath, dir);
48428
48907
  if (fs57.existsSync(fullDir) && fs57.statSync(fullDir).isDirectory()) {
48429
48908
  const files = collectNodeFiles(fullDir);
48430
48909
  if (files.length > 0) return true;
@@ -48435,12 +48914,12 @@ var N8nPlugin = class {
48435
48914
  const searchDirs = ["workflows", "n8n", ".n8n", "."];
48436
48915
  for (const dir of searchDirs) {
48437
48916
  try {
48438
- const fullDir = path66.join(ctx.rootPath, dir);
48917
+ const fullDir = path67.join(ctx.rootPath, dir);
48439
48918
  if (!fs57.existsSync(fullDir) || !fs57.statSync(fullDir).isDirectory()) continue;
48440
48919
  const files = fs57.readdirSync(fullDir).filter((f) => f.endsWith(".json"));
48441
48920
  for (const file of files.slice(0, 5)) {
48442
48921
  try {
48443
- const content = fs57.readFileSync(path66.join(fullDir, file));
48922
+ const content = fs57.readFileSync(path67.join(fullDir, file));
48444
48923
  if (parseN8nWorkflow(content)) return true;
48445
48924
  } catch {
48446
48925
  }
@@ -48502,7 +48981,7 @@ var N8nPlugin = class {
48502
48981
  routes: [],
48503
48982
  frameworkRole: role,
48504
48983
  metadata: {
48505
- workflowName: workflow.name ?? path66.basename(filePath, ".json"),
48984
+ workflowName: workflow.name ?? path67.basename(filePath, ".json"),
48506
48985
  workflowId: workflow.id,
48507
48986
  active: workflow.active ?? false,
48508
48987
  nodeCount: workflow.nodes.length,
@@ -48837,7 +49316,7 @@ function findNodeByteOffset(source, nodeName) {
48837
49316
 
48838
49317
  // src/indexer/plugins/integration/tooling/data-fetching/index.ts
48839
49318
  import fs58 from "fs";
48840
- import path67 from "path";
49319
+ import path68 from "path";
48841
49320
  var USE_QUERY_OBJECT_RE = /\b(useQuery|useInfiniteQuery)\s*\(\s*\{[^}]*?queryFn\s*:\s*[^}]*?fetch\s*\(\s*[`'"](\/[^'"`$]*?)['"`]/g;
48842
49321
  var USE_QUERY_ARRAY_RE = /\b(useQuery|useInfiniteQuery)\s*\(\s*\[[^\]]*\]\s*,\s*(?:\([^)]*\)\s*=>|function\s*\([^)]*\)\s*\{)[^)]*?fetch\s*\(\s*[`'"](\/[^'"`$]*?)['"`]/g;
48843
49322
  var USE_MUTATION_RE = /\b(useMutation)\s*\(\s*\{[^}]*?mutationFn\s*:\s*[^}]*?fetch\s*\(\s*[`'"](\/[^'"`$]*?)['"`][^)]*?(?:method\s*:\s*['"`](\w+)['"`])?/g;
@@ -48909,7 +49388,7 @@ var DataFetchingPlugin = class {
48909
49388
  if ("@tanstack/react-query" in deps || "swr" in deps) return true;
48910
49389
  }
48911
49390
  try {
48912
- const pkgPath = path67.join(ctx.rootPath, "package.json");
49391
+ const pkgPath = path68.join(ctx.rootPath, "package.json");
48913
49392
  const content = fs58.readFileSync(pkgPath, "utf-8");
48914
49393
  const pkg = JSON.parse(content);
48915
49394
  const deps = {
@@ -49013,7 +49492,7 @@ function createAllIntegrationPlugins() {
49013
49492
 
49014
49493
  // src/indexer/watcher.ts
49015
49494
  import * as parcelWatcher from "@parcel/watcher";
49016
- import path68 from "path";
49495
+ import path69 from "path";
49017
49496
  var IGNORE_DIRS = [
49018
49497
  "vendor",
49019
49498
  "node_modules",
@@ -49038,12 +49517,12 @@ var FileWatcher = class {
49038
49517
  debounceTimer = null;
49039
49518
  pendingPaths = /* @__PURE__ */ new Set();
49040
49519
  async start(rootPath, config, onChanges, debounceMs = DEFAULT_DEBOUNCE_MS, onDeletes) {
49041
- const ignoreDirs = IGNORE_DIRS.map((d) => path68.join(rootPath, d));
49520
+ const ignoreDirs = IGNORE_DIRS.map((d) => path69.join(rootPath, d));
49042
49521
  this.subscription = await parcelWatcher.subscribe(
49043
49522
  rootPath,
49044
- async (err31, events) => {
49045
- if (err31) {
49046
- logger.error({ error: err31 }, "Watcher error");
49523
+ async (err32, events) => {
49524
+ if (err32) {
49525
+ logger.error({ error: err32 }, "Watcher error");
49047
49526
  return;
49048
49527
  }
49049
49528
  const notIgnored = (p4) => !ignoreDirs.some((d) => p4.startsWith(d));
@@ -49094,12 +49573,12 @@ import http from "http";
49094
49573
  // src/cli-init.ts
49095
49574
  import { Command } from "commander";
49096
49575
  import fs68 from "fs";
49097
- import path77 from "path";
49576
+ import path78 from "path";
49098
49577
  import * as p from "@clack/prompts";
49099
49578
 
49100
49579
  // src/init/mcp-client.ts
49101
49580
  import fs59 from "fs";
49102
- import path69 from "path";
49581
+ import path70 from "path";
49103
49582
  import os2 from "os";
49104
49583
  var HOME = os2.homedir();
49105
49584
  function configureMcpClients(clientNames, projectRoot, opts) {
@@ -49131,14 +49610,14 @@ function configureMcpClients(clientNames, projectRoot, opts) {
49131
49610
  try {
49132
49611
  const action = writeTraceMcpEntry(configPath, entry);
49133
49612
  results.push({ target: configPath, action, detail: `${name} (${opts.scope})` });
49134
- } catch (err31) {
49135
- results.push({ target: configPath, action: "skipped", detail: `Error: ${err31.message}` });
49613
+ } catch (err32) {
49614
+ results.push({ target: configPath, action: "skipped", detail: `Error: ${err32.message}` });
49136
49615
  }
49137
49616
  }
49138
49617
  return results;
49139
49618
  }
49140
49619
  function writeTraceMcpEntry(configPath, entry) {
49141
- const dir = path69.dirname(configPath);
49620
+ const dir = path70.dirname(configPath);
49142
49621
  if (!fs59.existsSync(dir)) fs59.mkdirSync(dir, { recursive: true });
49143
49622
  let config = {};
49144
49623
  let isNew = true;
@@ -49159,15 +49638,15 @@ function writeTraceMcpEntry(configPath, entry) {
49159
49638
  function getConfigPath(name, projectRoot, scope) {
49160
49639
  switch (name) {
49161
49640
  case "claude-code":
49162
- return scope === "global" ? path69.join(HOME, ".claude.json") : path69.join(projectRoot, ".mcp.json");
49641
+ return scope === "global" ? path70.join(HOME, ".claude.json") : path70.join(projectRoot, ".mcp.json");
49163
49642
  case "claude-desktop":
49164
- return process.platform === "darwin" ? path69.join(HOME, "Library", "Application Support", "Claude", "claude_desktop_config.json") : path69.join(process.env.APPDATA ?? path69.join(HOME, "AppData", "Roaming"), "Claude", "claude_desktop_config.json");
49643
+ return process.platform === "darwin" ? path70.join(HOME, "Library", "Application Support", "Claude", "claude_desktop_config.json") : path70.join(process.env.APPDATA ?? path70.join(HOME, "AppData", "Roaming"), "Claude", "claude_desktop_config.json");
49165
49644
  case "cursor":
49166
- return scope === "global" ? path69.join(HOME, ".cursor", "mcp.json") : path69.join(projectRoot, ".cursor", "mcp.json");
49645
+ return scope === "global" ? path70.join(HOME, ".cursor", "mcp.json") : path70.join(projectRoot, ".cursor", "mcp.json");
49167
49646
  case "windsurf":
49168
- return scope === "global" ? path69.join(HOME, ".windsurf", "mcp.json") : path69.join(projectRoot, ".windsurf", "mcp.json");
49647
+ return scope === "global" ? path70.join(HOME, ".windsurf", "mcp.json") : path70.join(projectRoot, ".windsurf", "mcp.json");
49169
49648
  case "continue":
49170
- return scope === "global" ? path69.join(HOME, ".continue", "mcpServers", "mcp.json") : path69.join(projectRoot, ".continue", "mcpServers", "mcp.json");
49649
+ return scope === "global" ? path70.join(HOME, ".continue", "mcpServers", "mcp.json") : path70.join(projectRoot, ".continue", "mcpServers", "mcp.json");
49171
49650
  default:
49172
49651
  return null;
49173
49652
  }
@@ -49175,13 +49654,13 @@ function getConfigPath(name, projectRoot, scope) {
49175
49654
 
49176
49655
  // src/init/claude-md.ts
49177
49656
  import fs60 from "fs";
49178
- import path70 from "path";
49657
+ import path71 from "path";
49179
49658
  var START_MARKER = "<!-- trace-mcp:start -->";
49180
49659
  var END_MARKER = "<!-- trace-mcp:end -->";
49181
49660
  var BLOCK = `${START_MARKER}
49182
49661
  ## trace-mcp Tool Routing
49183
49662
 
49184
- Use trace-mcp tools for code intelligence \u2014 they understand framework relationships, not just text.
49663
+ IMPORTANT: For ANY code exploration task, ALWAYS use trace-mcp tools first. NEVER use Read/Grep/Glob/Bash(ls,find) for navigating source code.
49185
49664
 
49186
49665
  | Task | trace-mcp tool | Instead of |
49187
49666
  |------|---------------|------------|
@@ -49190,16 +49669,22 @@ Use trace-mcp tools for code intelligence \u2014 they understand framework relat
49190
49669
  | Read one symbol's source | \`get_symbol\` | Read (full file) |
49191
49670
  | What breaks if I change X | \`get_change_impact\` | guessing |
49192
49671
  | All usages of a symbol | \`find_usages\` | Grep |
49672
+ | All implementations of an interface | \`get_type_hierarchy\` | ls/find on directories |
49673
+ | All classes implementing X | \`search\` with \`implements\` filter | Grep |
49674
+ | Project health / coverage gaps | \`self_audit\` | manual inspection |
49675
+ | Dead code / dead exports | \`get_dead_code\` / \`get_dead_exports\` | Grep for unused |
49193
49676
  | Context for a task | \`get_feature_context\` | reading 15 files |
49194
49677
  | Tests for a symbol | \`get_tests_for\` | Glob + Grep |
49195
49678
  | HTTP request flow | \`get_request_flow\` | reading route files |
49196
49679
  | DB model relationships | \`get_model_context\` | reading model + migrations |
49680
+ | Component tree | \`get_component_tree\` | reading component files |
49681
+ | Circular dependencies | \`get_circular_imports\` | manual tracing |
49197
49682
 
49198
- Use Read/Grep/Glob for non-code files (.md, .json, .yaml, config).
49683
+ Use Read/Grep/Glob ONLY for non-code files (.md, .json, .yaml, config) or before Edit.
49199
49684
  Start sessions with \`get_project_map\` (summary_only=true).
49200
49685
  ${END_MARKER}`;
49201
49686
  function updateClaudeMd(projectRoot, opts) {
49202
- const filePath = opts.scope === "global" ? path70.join(process.env.HOME ?? process.env.USERPROFILE ?? "", ".claude", "CLAUDE.md") : path70.join(projectRoot, "CLAUDE.md");
49687
+ const filePath = opts.scope === "global" ? path71.join(process.env.HOME ?? process.env.USERPROFILE ?? "", ".claude", "CLAUDE.md") : path71.join(projectRoot, "CLAUDE.md");
49203
49688
  if (opts.dryRun) {
49204
49689
  if (!fs60.existsSync(filePath)) {
49205
49690
  return { target: filePath, action: "skipped", detail: "Would create CLAUDE.md" };
@@ -49234,7 +49719,7 @@ function escapeRegex4(s) {
49234
49719
 
49235
49720
  // src/init/hooks.ts
49236
49721
  import fs61 from "fs";
49237
- import path71 from "path";
49722
+ import path72 from "path";
49238
49723
  import os3 from "os";
49239
49724
 
49240
49725
  // src/init/types.ts
@@ -49243,12 +49728,12 @@ var REINDEX_HOOK_VERSION = "0.1.0";
49243
49728
 
49244
49729
  // src/init/hooks.ts
49245
49730
  var HOME2 = os3.homedir();
49246
- var HOOK_DEST = path71.join(HOME2, ".claude", "hooks", "trace-mcp-guard.sh");
49247
- var REINDEX_HOOK_DEST = path71.join(HOME2, ".claude", "hooks", "trace-mcp-reindex.sh");
49731
+ var HOOK_DEST = path72.join(HOME2, ".claude", "hooks", "trace-mcp-guard.sh");
49732
+ var REINDEX_HOOK_DEST = path72.join(HOME2, ".claude", "hooks", "trace-mcp-reindex.sh");
49248
49733
  function getHookSourcePath() {
49249
49734
  const candidates = [
49250
- path71.resolve(import.meta.dirname ?? ".", "..", "..", "hooks", "trace-mcp-guard.sh"),
49251
- path71.resolve(process.cwd(), "hooks", "trace-mcp-guard.sh")
49735
+ path72.resolve(import.meta.dirname ?? ".", "..", "..", "hooks", "trace-mcp-guard.sh"),
49736
+ path72.resolve(process.cwd(), "hooks", "trace-mcp-guard.sh")
49252
49737
  ];
49253
49738
  for (const c of candidates) {
49254
49739
  if (fs61.existsSync(c)) return c;
@@ -49256,17 +49741,17 @@ function getHookSourcePath() {
49256
49741
  throw new Error("Could not find hooks/trace-mcp-guard.sh \u2014 trace-mcp installation may be corrupted.");
49257
49742
  }
49258
49743
  function installGuardHook(opts) {
49259
- const settingsPath = opts.global ? path71.join(HOME2, ".claude", "settings.json") : path71.resolve(process.cwd(), ".claude", "settings.local.json");
49744
+ const settingsPath = opts.global ? path72.join(HOME2, ".claude", "settings.json") : path72.resolve(process.cwd(), ".claude", "settings.local.json");
49260
49745
  if (opts.dryRun) {
49261
49746
  return { target: HOOK_DEST, action: "skipped", detail: "Would install guard hook" };
49262
49747
  }
49263
49748
  const hookSrc = getHookSourcePath();
49264
- const hookDir = path71.dirname(HOOK_DEST);
49749
+ const hookDir = path72.dirname(HOOK_DEST);
49265
49750
  if (!fs61.existsSync(hookDir)) fs61.mkdirSync(hookDir, { recursive: true });
49266
49751
  const isUpdate = fs61.existsSync(HOOK_DEST);
49267
49752
  fs61.copyFileSync(hookSrc, HOOK_DEST);
49268
49753
  fs61.chmodSync(HOOK_DEST, 493);
49269
- const settingsDir = path71.dirname(settingsPath);
49754
+ const settingsDir = path72.dirname(settingsPath);
49270
49755
  if (!fs61.existsSync(settingsDir)) fs61.mkdirSync(settingsDir, { recursive: true });
49271
49756
  const settings = fs61.existsSync(settingsPath) ? JSON.parse(fs61.readFileSync(settingsPath, "utf-8")) : {};
49272
49757
  if (!settings.hooks) settings.hooks = {};
@@ -49292,7 +49777,7 @@ function installGuardHook(opts) {
49292
49777
  };
49293
49778
  }
49294
49779
  function uninstallGuardHook(opts) {
49295
- const settingsPath = opts.global ? path71.join(HOME2, ".claude", "settings.json") : path71.resolve(process.cwd(), ".claude", "settings.local.json");
49780
+ const settingsPath = opts.global ? path72.join(HOME2, ".claude", "settings.json") : path72.resolve(process.cwd(), ".claude", "settings.local.json");
49296
49781
  if (fs61.existsSync(settingsPath)) {
49297
49782
  const settings = JSON.parse(fs61.readFileSync(settingsPath, "utf-8"));
49298
49783
  const pre = settings.hooks?.PreToolUse;
@@ -49314,8 +49799,8 @@ function isHookOutdated(installedVersion) {
49314
49799
  }
49315
49800
  function getReindexHookSourcePath() {
49316
49801
  const candidates = [
49317
- path71.resolve(import.meta.dirname ?? ".", "..", "..", "hooks", "trace-mcp-reindex.sh"),
49318
- path71.resolve(process.cwd(), "hooks", "trace-mcp-reindex.sh")
49802
+ path72.resolve(import.meta.dirname ?? ".", "..", "..", "hooks", "trace-mcp-reindex.sh"),
49803
+ path72.resolve(process.cwd(), "hooks", "trace-mcp-reindex.sh")
49319
49804
  ];
49320
49805
  for (const c of candidates) {
49321
49806
  if (fs61.existsSync(c)) return c;
@@ -49323,17 +49808,17 @@ function getReindexHookSourcePath() {
49323
49808
  throw new Error("Could not find hooks/trace-mcp-reindex.sh \u2014 trace-mcp installation may be corrupted.");
49324
49809
  }
49325
49810
  function installReindexHook(opts) {
49326
- const settingsPath = opts.global ? path71.join(HOME2, ".claude", "settings.json") : path71.resolve(process.cwd(), ".claude", "settings.local.json");
49811
+ const settingsPath = opts.global ? path72.join(HOME2, ".claude", "settings.json") : path72.resolve(process.cwd(), ".claude", "settings.local.json");
49327
49812
  if (opts.dryRun) {
49328
49813
  return { target: REINDEX_HOOK_DEST, action: "skipped", detail: "Would install reindex hook" };
49329
49814
  }
49330
49815
  const hookSrc = getReindexHookSourcePath();
49331
- const hookDir = path71.dirname(REINDEX_HOOK_DEST);
49816
+ const hookDir = path72.dirname(REINDEX_HOOK_DEST);
49332
49817
  if (!fs61.existsSync(hookDir)) fs61.mkdirSync(hookDir, { recursive: true });
49333
49818
  const isUpdate = fs61.existsSync(REINDEX_HOOK_DEST);
49334
49819
  fs61.copyFileSync(hookSrc, REINDEX_HOOK_DEST);
49335
49820
  fs61.chmodSync(REINDEX_HOOK_DEST, 493);
49336
- const settingsDir = path71.dirname(settingsPath);
49821
+ const settingsDir = path72.dirname(settingsPath);
49337
49822
  if (!fs61.existsSync(settingsDir)) fs61.mkdirSync(settingsDir, { recursive: true });
49338
49823
  const settings = fs61.existsSync(settingsPath) ? JSON.parse(fs61.readFileSync(settingsPath, "utf-8")) : {};
49339
49824
  if (!settings.hooks) settings.hooks = {};
@@ -49361,10 +49846,10 @@ function installReindexHook(opts) {
49361
49846
 
49362
49847
  // src/init/ide-rules.ts
49363
49848
  import fs62 from "fs";
49364
- import path72 from "path";
49849
+ import path73 from "path";
49365
49850
  var START_MARKER2 = "<!-- trace-mcp:start -->";
49366
49851
  var END_MARKER2 = "<!-- trace-mcp:end -->";
49367
- var TOOL_ROUTING_POLICY = `Use trace-mcp MCP tools for all code intelligence tasks \u2014 they understand framework relationships, not just text.
49852
+ var TOOL_ROUTING_POLICY = `IMPORTANT: For ANY code exploration task, ALWAYS use trace-mcp tools first. NEVER use built-in search/grep/file listing for navigating source code.
49368
49853
 
49369
49854
  ## Tool Routing
49370
49855
 
@@ -49375,13 +49860,19 @@ var TOOL_ROUTING_POLICY = `Use trace-mcp MCP tools for all code intelligence tas
49375
49860
  | Read one symbol's source | \`get_symbol\` | reading full file |
49376
49861
  | What breaks if I change X | \`get_change_impact\` | guessing |
49377
49862
  | All usages of a symbol | \`find_usages\` | grep / find references |
49863
+ | All implementations of an interface | \`get_type_hierarchy\` | listing directories |
49864
+ | All classes implementing X | \`search\` with \`implements\` filter | grep |
49865
+ | Project health / coverage gaps | \`self_audit\` | manual inspection |
49866
+ | Dead code / dead exports | \`get_dead_code\` / \`get_dead_exports\` | grep for unused |
49378
49867
  | Context for a task | \`get_feature_context\` | reading many files |
49379
49868
  | Tests for a symbol | \`get_tests_for\` | searching test files |
49380
49869
  | HTTP request flow | \`get_request_flow\` | reading route files |
49381
49870
  | DB model relationships | \`get_model_context\` | reading model + migration files |
49871
+ | Component tree | \`get_component_tree\` | reading component files |
49872
+ | Circular dependencies | \`get_circular_imports\` | manual tracing |
49382
49873
 
49383
49874
  Start sessions with \`get_project_map\` (summary_only=true) to get project overview.
49384
- Use built-in file reading only for non-code files (.md, .json, .yaml, config).`;
49875
+ Use built-in file reading ONLY for non-code files (.md, .json, .yaml, config) or before editing.`;
49385
49876
  var CURSOR_RULE = `---
49386
49877
  description: trace-mcp tool routing \u2014 prefer trace-mcp MCP tools over built-in search for code intelligence
49387
49878
  globs:
@@ -49391,9 +49882,9 @@ alwaysApply: true
49391
49882
  ${TOOL_ROUTING_POLICY}
49392
49883
  `;
49393
49884
  function installCursorRules(projectRoot, opts) {
49394
- const base = opts.global ? path72.join(process.env.HOME ?? process.env.USERPROFILE ?? "", ".cursor") : path72.join(projectRoot, ".cursor");
49395
- const rulesDir = path72.join(base, "rules");
49396
- const filePath = path72.join(rulesDir, "trace-mcp.mdc");
49885
+ const base = opts.global ? path73.join(process.env.HOME ?? process.env.USERPROFILE ?? "", ".cursor") : path73.join(projectRoot, ".cursor");
49886
+ const rulesDir = path73.join(base, "rules");
49887
+ const filePath = path73.join(rulesDir, "trace-mcp.mdc");
49397
49888
  if (opts.dryRun) {
49398
49889
  if (fs62.existsSync(filePath)) {
49399
49890
  const content = fs62.readFileSync(filePath, "utf-8");
@@ -49422,7 +49913,7 @@ var WINDSURF_BLOCK = `${START_MARKER2}
49422
49913
  ${TOOL_ROUTING_POLICY}
49423
49914
  ${END_MARKER2}`;
49424
49915
  function installWindsurfRules(projectRoot, opts) {
49425
- const filePath = opts.global ? path72.join(process.env.HOME ?? process.env.USERPROFILE ?? "", ".windsurfrules") : path72.join(projectRoot, ".windsurfrules");
49916
+ const filePath = opts.global ? path73.join(process.env.HOME ?? process.env.USERPROFILE ?? "", ".windsurfrules") : path73.join(projectRoot, ".windsurfrules");
49426
49917
  if (opts.dryRun) {
49427
49918
  if (!fs62.existsSync(filePath)) {
49428
49919
  return { target: filePath, action: "skipped", detail: "Would create .windsurfrules" };
@@ -49457,12 +49948,12 @@ function escapeRegex5(s) {
49457
49948
 
49458
49949
  // src/init/detector.ts
49459
49950
  import fs63 from "fs";
49460
- import path73 from "path";
49951
+ import path74 from "path";
49461
49952
  import os4 from "os";
49462
49953
  import Database4 from "better-sqlite3";
49463
49954
  var HOME3 = os4.homedir();
49464
49955
  function detectProject(dir) {
49465
- const projectRoot = path73.resolve(dir);
49956
+ const projectRoot = path74.resolve(dir);
49466
49957
  const ctx = buildProjectContext(projectRoot);
49467
49958
  const packageManagers = detectPackageManagers(projectRoot);
49468
49959
  const registry = new PluginRegistry();
@@ -49491,7 +49982,7 @@ function detectProject(dir) {
49491
49982
  const mcpClients = detectMcpClients(projectRoot);
49492
49983
  const existingConfig = detectExistingConfig(projectRoot);
49493
49984
  const existingDb = detectExistingDb(projectRoot);
49494
- const claudeMdPath = path73.join(projectRoot, "CLAUDE.md");
49985
+ const claudeMdPath = path74.join(projectRoot, "CLAUDE.md");
49495
49986
  const hasClaudeMd = fs63.existsSync(claudeMdPath);
49496
49987
  const claudeMdHasTraceMcpBlock = hasClaudeMd && fs63.readFileSync(claudeMdPath, "utf-8").includes("<!-- trace-mcp:start -->");
49497
49988
  const { hasGuardHook, guardHookVersion } = detectGuardHook();
@@ -49512,8 +50003,8 @@ function detectProject(dir) {
49512
50003
  function detectPackageManagers(root) {
49513
50004
  const managers = [];
49514
50005
  const check = (file, type, lockfiles) => {
49515
- if (fs63.existsSync(path73.join(root, file))) {
49516
- const lockfile = lockfiles.find((l) => fs63.existsSync(path73.join(root, l)));
50006
+ if (fs63.existsSync(path74.join(root, file))) {
50007
+ const lockfile = lockfiles.find((l) => fs63.existsSync(path74.join(root, l)));
49517
50008
  managers.push({ type, lockfile });
49518
50009
  }
49519
50010
  };
@@ -49527,7 +50018,7 @@ function detectPackageManagers(root) {
49527
50018
  check("pyproject.toml", "poetry", ["poetry.lock", "uv.lock"]);
49528
50019
  if (managers.length > 0 && managers[managers.length - 1].type === "poetry") {
49529
50020
  if (managers[managers.length - 1].lockfile === "uv.lock") managers[managers.length - 1].type = "uv";
49530
- else if (!managers[managers.length - 1].lockfile && fs63.existsSync(path73.join(root, "requirements.txt"))) {
50021
+ else if (!managers[managers.length - 1].lockfile && fs63.existsSync(path74.join(root, "requirements.txt"))) {
49531
50022
  managers[managers.length - 1].type = "pip";
49532
50023
  }
49533
50024
  }
@@ -49536,7 +50027,7 @@ function detectPackageManagers(root) {
49536
50027
  check("Gemfile", "bundler", ["Gemfile.lock"]);
49537
50028
  check("pom.xml", "maven", []);
49538
50029
  if (!managers.some((m) => m.type === "maven")) {
49539
- if (fs63.existsSync(path73.join(root, "build.gradle")) || fs63.existsSync(path73.join(root, "build.gradle.kts"))) {
50030
+ if (fs63.existsSync(path74.join(root, "build.gradle")) || fs63.existsSync(path74.join(root, "build.gradle.kts"))) {
49540
50031
  managers.push({ type: "gradle", lockfile: void 0 });
49541
50032
  }
49542
50033
  }
@@ -49555,40 +50046,40 @@ function detectMcpClients(projectRoot) {
49555
50046
  }
49556
50047
  };
49557
50048
  if (projectRoot) {
49558
- checkConfig("claude-code", path73.join(projectRoot, ".mcp.json"));
50049
+ checkConfig("claude-code", path74.join(projectRoot, ".mcp.json"));
49559
50050
  }
49560
- checkConfig("claude-code", path73.join(HOME3, ".claude.json"));
49561
- checkConfig("claude-code", path73.join(HOME3, ".claude", "settings.json"));
50051
+ checkConfig("claude-code", path74.join(HOME3, ".claude.json"));
50052
+ checkConfig("claude-code", path74.join(HOME3, ".claude", "settings.json"));
49562
50053
  const platform = os4.platform();
49563
50054
  if (platform === "darwin") {
49564
- checkConfig("claude-desktop", path73.join(HOME3, "Library", "Application Support", "Claude", "claude_desktop_config.json"));
50055
+ checkConfig("claude-desktop", path74.join(HOME3, "Library", "Application Support", "Claude", "claude_desktop_config.json"));
49565
50056
  } else if (platform === "win32") {
49566
- const appData = process.env.APPDATA ?? path73.join(HOME3, "AppData", "Roaming");
49567
- checkConfig("claude-desktop", path73.join(appData, "Claude", "claude_desktop_config.json"));
50057
+ const appData = process.env.APPDATA ?? path74.join(HOME3, "AppData", "Roaming");
50058
+ checkConfig("claude-desktop", path74.join(appData, "Claude", "claude_desktop_config.json"));
49568
50059
  }
49569
- checkConfig("cursor", path73.join(HOME3, ".cursor", "mcp.json"));
50060
+ checkConfig("cursor", path74.join(HOME3, ".cursor", "mcp.json"));
49570
50061
  if (projectRoot && !clients.some((c) => c.name === "cursor")) {
49571
- checkConfig("cursor", path73.join(projectRoot, ".cursor", "mcp.json"));
50062
+ checkConfig("cursor", path74.join(projectRoot, ".cursor", "mcp.json"));
49572
50063
  }
49573
- checkConfig("windsurf", path73.join(HOME3, ".windsurf", "mcp.json"));
50064
+ checkConfig("windsurf", path74.join(HOME3, ".windsurf", "mcp.json"));
49574
50065
  if (projectRoot && !clients.some((c) => c.name === "windsurf")) {
49575
- checkConfig("windsurf", path73.join(projectRoot, ".windsurf", "mcp.json"));
50066
+ checkConfig("windsurf", path74.join(projectRoot, ".windsurf", "mcp.json"));
49576
50067
  }
49577
- checkConfig("continue", path73.join(HOME3, ".continue", "mcpServers", "mcp.json"));
50068
+ checkConfig("continue", path74.join(HOME3, ".continue", "mcpServers", "mcp.json"));
49578
50069
  if (projectRoot && !clients.some((c) => c.name === "continue")) {
49579
- checkConfig("continue", path73.join(projectRoot, ".continue", "mcpServers", "mcp.json"));
50070
+ checkConfig("continue", path74.join(projectRoot, ".continue", "mcpServers", "mcp.json"));
49580
50071
  }
49581
50072
  return clients;
49582
50073
  }
49583
50074
  function detectExistingConfig(root) {
49584
50075
  const candidates = [
49585
- path73.join(root, ".trace-mcp.json"),
49586
- path73.join(root, ".config", "trace-mcp.json")
50076
+ path74.join(root, ".trace-mcp.json"),
50077
+ path74.join(root, ".config", "trace-mcp.json")
49587
50078
  ];
49588
50079
  for (const p4 of candidates) {
49589
50080
  if (fs63.existsSync(p4)) return { path: p4 };
49590
50081
  }
49591
- const pkgPath = path73.join(root, "package.json");
50082
+ const pkgPath = path74.join(root, "package.json");
49592
50083
  if (fs63.existsSync(pkgPath)) {
49593
50084
  try {
49594
50085
  const pkg = JSON.parse(fs63.readFileSync(pkgPath, "utf-8"));
@@ -49599,7 +50090,7 @@ function detectExistingConfig(root) {
49599
50090
  return null;
49600
50091
  }
49601
50092
  function detectExistingDb(root, globalDbPath) {
49602
- const candidates = globalDbPath ? [globalDbPath, path73.join(root, ".trace-mcp", "index.db")] : [path73.join(root, ".trace-mcp", "index.db")];
50093
+ const candidates = globalDbPath ? [globalDbPath, path74.join(root, ".trace-mcp", "index.db")] : [path74.join(root, ".trace-mcp", "index.db")];
49603
50094
  const dbPath = candidates.find((p4) => fs63.existsSync(p4));
49604
50095
  if (!dbPath) return null;
49605
50096
  try {
@@ -49615,7 +50106,7 @@ function detectExistingDb(root, globalDbPath) {
49615
50106
  }
49616
50107
  }
49617
50108
  function detectGuardHook() {
49618
- const hookPath = path73.join(HOME3, ".claude", "hooks", "trace-mcp-guard.sh");
50109
+ const hookPath = path74.join(HOME3, ".claude", "hooks", "trace-mcp-guard.sh");
49619
50110
  if (!fs63.existsSync(hookPath)) return { hasGuardHook: false, guardHookVersion: null };
49620
50111
  const content = fs63.readFileSync(hookPath, "utf-8");
49621
50112
  const match = content.match(/^# trace-mcp-guard v(.+)$/m);
@@ -49627,7 +50118,7 @@ function detectGuardHook() {
49627
50118
 
49628
50119
  // src/init/conflict-detector.ts
49629
50120
  import fs64 from "fs";
49630
- import path74 from "path";
50121
+ import path75 from "path";
49631
50122
  import os5 from "os";
49632
50123
  var HOME4 = os5.homedir();
49633
50124
  var COMPETING_MCP_SERVERS = {
@@ -49721,9 +50212,9 @@ var COMPETING_PROJECT_FILES = [
49721
50212
  { file: ".greptile.yaml", competitor: "greptile" }
49722
50213
  ];
49723
50214
  var COMPETING_GLOBAL_DIRS = [
49724
- { dir: path74.join(HOME4, ".code-index"), competitor: "jcodemunch-mcp" },
49725
- { dir: path74.join(HOME4, ".repomix"), competitor: "repomix" },
49726
- { dir: path74.join(HOME4, ".aider.tags.cache.v3"), competitor: "aider" }
50215
+ { dir: path75.join(HOME4, ".code-index"), competitor: "jcodemunch-mcp" },
50216
+ { dir: path75.join(HOME4, ".repomix"), competitor: "repomix" },
50217
+ { dir: path75.join(HOME4, ".aider.tags.cache.v3"), competitor: "aider" }
49727
50218
  ];
49728
50219
  function detectConflicts(projectRoot) {
49729
50220
  const conflicts = [];
@@ -49782,31 +50273,31 @@ function getMcpConfigPaths(projectRoot) {
49782
50273
  const paths = [];
49783
50274
  const platform = os5.platform();
49784
50275
  if (projectRoot) {
49785
- paths.push({ clientName: "claude-code", configPath: path74.join(projectRoot, ".mcp.json") });
50276
+ paths.push({ clientName: "claude-code", configPath: path75.join(projectRoot, ".mcp.json") });
49786
50277
  }
49787
- paths.push({ clientName: "claude-code", configPath: path74.join(HOME4, ".claude", "settings.json") });
50278
+ paths.push({ clientName: "claude-code", configPath: path75.join(HOME4, ".claude", "settings.json") });
49788
50279
  if (platform === "darwin") {
49789
- paths.push({ clientName: "claude-desktop", configPath: path74.join(HOME4, "Library", "Application Support", "Claude", "claude_desktop_config.json") });
50280
+ paths.push({ clientName: "claude-desktop", configPath: path75.join(HOME4, "Library", "Application Support", "Claude", "claude_desktop_config.json") });
49790
50281
  } else if (platform === "win32") {
49791
- const appData = process.env.APPDATA ?? path74.join(HOME4, "AppData", "Roaming");
49792
- paths.push({ clientName: "claude-desktop", configPath: path74.join(appData, "Claude", "claude_desktop_config.json") });
50282
+ const appData = process.env.APPDATA ?? path75.join(HOME4, "AppData", "Roaming");
50283
+ paths.push({ clientName: "claude-desktop", configPath: path75.join(appData, "Claude", "claude_desktop_config.json") });
49793
50284
  }
49794
- paths.push({ clientName: "cursor", configPath: path74.join(HOME4, ".cursor", "mcp.json") });
50285
+ paths.push({ clientName: "cursor", configPath: path75.join(HOME4, ".cursor", "mcp.json") });
49795
50286
  if (projectRoot) {
49796
- paths.push({ clientName: "cursor", configPath: path74.join(projectRoot, ".cursor", "mcp.json") });
50287
+ paths.push({ clientName: "cursor", configPath: path75.join(projectRoot, ".cursor", "mcp.json") });
49797
50288
  }
49798
- paths.push({ clientName: "windsurf", configPath: path74.join(HOME4, ".windsurf", "mcp.json") });
50289
+ paths.push({ clientName: "windsurf", configPath: path75.join(HOME4, ".windsurf", "mcp.json") });
49799
50290
  if (projectRoot) {
49800
- paths.push({ clientName: "windsurf", configPath: path74.join(projectRoot, ".windsurf", "mcp.json") });
50291
+ paths.push({ clientName: "windsurf", configPath: path75.join(projectRoot, ".windsurf", "mcp.json") });
49801
50292
  }
49802
- paths.push({ clientName: "continue", configPath: path74.join(HOME4, ".continue", "mcpServers", "mcp.json") });
50293
+ paths.push({ clientName: "continue", configPath: path75.join(HOME4, ".continue", "mcpServers", "mcp.json") });
49803
50294
  return paths;
49804
50295
  }
49805
50296
  function scanHooksInSettings() {
49806
50297
  const conflicts = [];
49807
50298
  const settingsFiles = [
49808
- path74.join(HOME4, ".claude", "settings.json"),
49809
- path74.join(HOME4, ".claude", "settings.local.json")
50299
+ path75.join(HOME4, ".claude", "settings.json"),
50300
+ path75.join(HOME4, ".claude", "settings.local.json")
49810
50301
  ];
49811
50302
  for (const settingsPath of settingsFiles) {
49812
50303
  if (!fs64.existsSync(settingsPath)) continue;
@@ -49848,7 +50339,7 @@ function scanHooksInSettings() {
49848
50339
  }
49849
50340
  function scanHookScriptFiles() {
49850
50341
  const conflicts = [];
49851
- const hooksDir = path74.join(HOME4, ".claude", "hooks");
50342
+ const hooksDir = path75.join(HOME4, ".claude", "hooks");
49852
50343
  if (!fs64.existsSync(hooksDir)) return conflicts;
49853
50344
  let files;
49854
50345
  try {
@@ -49860,7 +50351,7 @@ function scanHookScriptFiles() {
49860
50351
  if (file.startsWith("trace-mcp")) continue;
49861
50352
  for (const { pattern, competitor } of COMPETING_HOOK_PATTERNS) {
49862
50353
  if (pattern.test(file)) {
49863
- const filePath = path74.join(hooksDir, file);
50354
+ const filePath = path75.join(hooksDir, file);
49864
50355
  conflicts.push({
49865
50356
  id: `hook_script:${file}:${competitor}`,
49866
50357
  category: "hook_script",
@@ -49879,13 +50370,13 @@ function scanHookScriptFiles() {
49879
50370
  function scanClaudeMdFiles(projectRoot) {
49880
50371
  const conflicts = [];
49881
50372
  const files = [
49882
- path74.join(HOME4, ".claude", "CLAUDE.md"),
49883
- path74.join(HOME4, ".claude", "AGENTS.md")
50373
+ path75.join(HOME4, ".claude", "CLAUDE.md"),
50374
+ path75.join(HOME4, ".claude", "AGENTS.md")
49884
50375
  ];
49885
50376
  if (projectRoot) {
49886
50377
  files.push(
49887
- path74.join(projectRoot, "CLAUDE.md"),
49888
- path74.join(projectRoot, "AGENTS.md")
50378
+ path75.join(projectRoot, "CLAUDE.md"),
50379
+ path75.join(projectRoot, "AGENTS.md")
49889
50380
  );
49890
50381
  }
49891
50382
  for (const filePath of files) {
@@ -49919,35 +50410,35 @@ function scanClaudeMdFiles(projectRoot) {
49919
50410
  function scanIdeRuleFiles(projectRoot) {
49920
50411
  const conflicts = [];
49921
50412
  const ruleFiles = [];
49922
- ruleFiles.push({ path: path74.join(HOME4, ".cursorrules"), type: ".cursorrules (global)" });
49923
- ruleFiles.push({ path: path74.join(HOME4, ".windsurfrules"), type: ".windsurfrules (global)" });
50413
+ ruleFiles.push({ path: path75.join(HOME4, ".cursorrules"), type: ".cursorrules (global)" });
50414
+ ruleFiles.push({ path: path75.join(HOME4, ".windsurfrules"), type: ".windsurfrules (global)" });
49924
50415
  if (projectRoot) {
49925
- ruleFiles.push({ path: path74.join(projectRoot, ".cursorrules"), type: ".cursorrules" });
49926
- ruleFiles.push({ path: path74.join(projectRoot, ".windsurfrules"), type: ".windsurfrules" });
49927
- ruleFiles.push({ path: path74.join(projectRoot, ".clinerules"), type: ".clinerules" });
49928
- ruleFiles.push({ path: path74.join(projectRoot, ".continuerules"), type: ".continuerules" });
49929
- ruleFiles.push({ path: path74.join(projectRoot, ".github", "copilot-instructions.md"), type: "copilot-instructions.md" });
49930
- const clineRulesDir = path74.join(projectRoot, ".clinerules");
50416
+ ruleFiles.push({ path: path75.join(projectRoot, ".cursorrules"), type: ".cursorrules" });
50417
+ ruleFiles.push({ path: path75.join(projectRoot, ".windsurfrules"), type: ".windsurfrules" });
50418
+ ruleFiles.push({ path: path75.join(projectRoot, ".clinerules"), type: ".clinerules" });
50419
+ ruleFiles.push({ path: path75.join(projectRoot, ".continuerules"), type: ".continuerules" });
50420
+ ruleFiles.push({ path: path75.join(projectRoot, ".github", "copilot-instructions.md"), type: "copilot-instructions.md" });
50421
+ const clineRulesDir = path75.join(projectRoot, ".clinerules");
49931
50422
  if (fs64.existsSync(clineRulesDir)) {
49932
50423
  try {
49933
50424
  const stat = fs64.statSync(clineRulesDir);
49934
50425
  if (stat.isDirectory()) {
49935
50426
  for (const file of fs64.readdirSync(clineRulesDir)) {
49936
- ruleFiles.push({ path: path74.join(clineRulesDir, file), type: `.clinerules/${file}` });
50427
+ ruleFiles.push({ path: path75.join(clineRulesDir, file), type: `.clinerules/${file}` });
49937
50428
  }
49938
50429
  }
49939
50430
  } catch {
49940
50431
  }
49941
50432
  }
49942
50433
  }
49943
- const cursorRulesDirs = [path74.join(HOME4, ".cursor", "rules")];
49944
- if (projectRoot) cursorRulesDirs.push(path74.join(projectRoot, ".cursor", "rules"));
50434
+ const cursorRulesDirs = [path75.join(HOME4, ".cursor", "rules")];
50435
+ if (projectRoot) cursorRulesDirs.push(path75.join(projectRoot, ".cursor", "rules"));
49945
50436
  for (const rulesDir of cursorRulesDirs) {
49946
50437
  if (!fs64.existsSync(rulesDir)) continue;
49947
50438
  try {
49948
50439
  for (const file of fs64.readdirSync(rulesDir)) {
49949
50440
  if (!file.endsWith(".mdc") || file === "trace-mcp.mdc") continue;
49950
- ruleFiles.push({ path: path74.join(rulesDir, file), type: `.cursor/rules/${file}` });
50441
+ ruleFiles.push({ path: path75.join(rulesDir, file), type: `.cursor/rules/${file}` });
49951
50442
  }
49952
50443
  } catch {
49953
50444
  }
@@ -49981,7 +50472,7 @@ function scanIdeRuleFiles(projectRoot) {
49981
50472
  function scanProjectConfigFiles(projectRoot) {
49982
50473
  const conflicts = [];
49983
50474
  for (const { file, competitor } of COMPETING_PROJECT_FILES) {
49984
- const filePath = path74.join(projectRoot, file);
50475
+ const filePath = path75.join(projectRoot, file);
49985
50476
  if (!fs64.existsSync(filePath)) continue;
49986
50477
  conflicts.push({
49987
50478
  id: `config:${competitor}:${file}`,
@@ -50005,7 +50496,7 @@ function scanProjectConfigDirs(projectRoot) {
50005
50496
  { dir: ".continue", competitor: "continue.dev" }
50006
50497
  ];
50007
50498
  for (const { dir, competitor } of dirs) {
50008
- const fullPath = path74.join(projectRoot, dir);
50499
+ const fullPath = path75.join(projectRoot, dir);
50009
50500
  if (!fs64.existsSync(fullPath)) continue;
50010
50501
  let stat;
50011
50502
  try {
@@ -50031,18 +50522,18 @@ function scanProjectConfigDirs(projectRoot) {
50031
50522
  function scanContinueConfigs(projectRoot) {
50032
50523
  const conflicts = [];
50033
50524
  const configPaths = [
50034
- path74.join(HOME4, ".continue", "config.yaml"),
50035
- path74.join(HOME4, ".continue", "config.json")
50525
+ path75.join(HOME4, ".continue", "config.yaml"),
50526
+ path75.join(HOME4, ".continue", "config.json")
50036
50527
  ];
50037
50528
  if (projectRoot) {
50038
50529
  configPaths.push(
50039
- path74.join(projectRoot, ".continue", "config.yaml"),
50040
- path74.join(projectRoot, ".continue", "config.json")
50530
+ path75.join(projectRoot, ".continue", "config.yaml"),
50531
+ path75.join(projectRoot, ".continue", "config.json")
50041
50532
  );
50042
50533
  }
50043
- const mcpServersDirs = [path74.join(HOME4, ".continue", "mcpServers")];
50534
+ const mcpServersDirs = [path75.join(HOME4, ".continue", "mcpServers")];
50044
50535
  if (projectRoot) {
50045
- mcpServersDirs.push(path74.join(projectRoot, ".continue", "mcpServers"));
50536
+ mcpServersDirs.push(path75.join(projectRoot, ".continue", "mcpServers"));
50046
50537
  }
50047
50538
  for (const mcpDir of mcpServersDirs) {
50048
50539
  if (!fs64.existsSync(mcpDir)) continue;
@@ -50054,7 +50545,7 @@ function scanContinueConfigs(projectRoot) {
50054
50545
  }
50055
50546
  for (const file of files) {
50056
50547
  if (!file.endsWith(".json")) continue;
50057
- const filePath = path74.join(mcpDir, file);
50548
+ const filePath = path75.join(mcpDir, file);
50058
50549
  let content;
50059
50550
  try {
50060
50551
  content = fs64.readFileSync(filePath, "utf-8");
@@ -50083,11 +50574,11 @@ function scanContinueConfigs(projectRoot) {
50083
50574
  }
50084
50575
  function scanGitHooks(projectRoot) {
50085
50576
  const conflicts = [];
50086
- const hooksDir = path74.join(projectRoot, ".git", "hooks");
50577
+ const hooksDir = path75.join(projectRoot, ".git", "hooks");
50087
50578
  if (!fs64.existsSync(hooksDir)) return conflicts;
50088
50579
  const hookFiles = ["pre-commit", "post-commit", "prepare-commit-msg"];
50089
50580
  for (const hookFile of hookFiles) {
50090
- const hookPath = path74.join(hooksDir, hookFile);
50581
+ const hookPath = path75.join(hooksDir, hookFile);
50091
50582
  if (!fs64.existsSync(hookPath)) continue;
50092
50583
  let content;
50093
50584
  try {
@@ -50197,8 +50688,8 @@ function fixMcpServer(conflict, opts) {
50197
50688
  }
50198
50689
  fs65.writeFileSync(configPath, JSON.stringify(parsed, null, 2) + "\n");
50199
50690
  return { conflictId: conflict.id, action: "removed", detail: `Removed "${serverName}" from ${shortPath2(configPath)}`, target: configPath };
50200
- } catch (err31) {
50201
- return { conflictId: conflict.id, action: "skipped", detail: `Failed to update config: ${err31.message}`, target: configPath };
50691
+ } catch (err32) {
50692
+ return { conflictId: conflict.id, action: "skipped", detail: `Failed to update config: ${err32.message}`, target: configPath };
50202
50693
  }
50203
50694
  }
50204
50695
  function fixHookInSettings(conflict, opts) {
@@ -50241,8 +50732,8 @@ function fixHookInSettings(conflict, opts) {
50241
50732
  }
50242
50733
  fs65.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
50243
50734
  return { conflictId: conflict.id, action: "removed", detail: `Removed ${competitor} hooks from ${shortPath2(settingsPath)}`, target: settingsPath };
50244
- } catch (err31) {
50245
- return { conflictId: conflict.id, action: "skipped", detail: `Failed to update settings: ${err31.message}`, target: settingsPath };
50735
+ } catch (err32) {
50736
+ return { conflictId: conflict.id, action: "skipped", detail: `Failed to update settings: ${err32.message}`, target: settingsPath };
50246
50737
  }
50247
50738
  }
50248
50739
  function fixHookScript(conflict, opts) {
@@ -50256,8 +50747,8 @@ function fixHookScript(conflict, opts) {
50256
50747
  try {
50257
50748
  fs65.unlinkSync(scriptPath);
50258
50749
  return { conflictId: conflict.id, action: "removed", detail: `Deleted ${shortPath2(scriptPath)}`, target: scriptPath };
50259
- } catch (err31) {
50260
- return { conflictId: conflict.id, action: "skipped", detail: `Failed to delete: ${err31.message}`, target: scriptPath };
50750
+ } catch (err32) {
50751
+ return { conflictId: conflict.id, action: "skipped", detail: `Failed to delete: ${err32.message}`, target: scriptPath };
50261
50752
  }
50262
50753
  }
50263
50754
  function fixClaudeMdBlock(conflict, opts) {
@@ -50270,30 +50761,20 @@ function fixClaudeMdBlock(conflict, opts) {
50270
50761
  }
50271
50762
  try {
50272
50763
  const content = fs65.readFileSync(filePath, "utf-8");
50273
- const markerPatterns = [
50274
- /<!-- ?jcodemunch:start ?-->[\s\S]*?<!-- ?jcodemunch:end ?-->\n?/gi,
50275
- /<!-- ?code-index:start ?-->[\s\S]*?<!-- ?code-index:end ?-->\n?/gi,
50276
- /<!-- ?repomix:start ?-->[\s\S]*?<!-- ?repomix:end ?-->\n?/gi,
50277
- /<!-- ?aider:start ?-->[\s\S]*?<!-- ?aider:end ?-->\n?/gi,
50278
- /<!-- ?cline:start ?-->[\s\S]*?<!-- ?cline:end ?-->\n?/gi,
50279
- /<!-- ?cody:start ?-->[\s\S]*?<!-- ?cody:end ?-->\n?/gi,
50280
- /<!-- ?greptile:start ?-->[\s\S]*?<!-- ?greptile:end ?-->\n?/gi,
50281
- /<!-- ?sourcegraph:start ?-->[\s\S]*?<!-- ?sourcegraph:end ?-->\n?/gi,
50282
- /<!-- ?code-compass:start ?-->[\s\S]*?<!-- ?code-compass:end ?-->\n?/gi,
50283
- /<!-- ?repo-map:start ?-->[\s\S]*?<!-- ?repo-map:end ?-->\n?/gi
50284
- ];
50285
- let updated = content;
50286
- for (const pattern of markerPatterns) {
50287
- updated = updated.replace(pattern, "");
50288
- }
50764
+ const tools = ["jcodemunch", "code-index", "repomix", "aider", "cline", "cody", "greptile", "sourcegraph", "code-compass", "repo-map"];
50765
+ const markerPattern = new RegExp(
50766
+ `<!-- ?(${tools.join("|")}):start ?-->[\\s\\S]*?<!-- ?\\1:end ?-->\\n?`,
50767
+ "gi"
50768
+ );
50769
+ let updated = content.replace(markerPattern, "");
50289
50770
  updated = updated.replace(/\n{3,}/g, "\n\n").trim() + "\n";
50290
50771
  if (updated === content) {
50291
50772
  return { conflictId: conflict.id, action: "skipped", detail: "No marker-delimited blocks found to remove", target: filePath };
50292
50773
  }
50293
50774
  fs65.writeFileSync(filePath, updated);
50294
50775
  return { conflictId: conflict.id, action: "cleaned", detail: `Removed ${conflict.competitor} block from ${shortPath2(filePath)}`, target: filePath };
50295
- } catch (err31) {
50296
- return { conflictId: conflict.id, action: "skipped", detail: `Failed to update: ${err31.message}`, target: filePath };
50776
+ } catch (err32) {
50777
+ return { conflictId: conflict.id, action: "skipped", detail: `Failed to update: ${err32.message}`, target: filePath };
50297
50778
  }
50298
50779
  }
50299
50780
  function fixConfigFile(conflict, opts) {
@@ -50312,8 +50793,8 @@ function fixConfigFile(conflict, opts) {
50312
50793
  fs65.unlinkSync(filePath);
50313
50794
  }
50314
50795
  return { conflictId: conflict.id, action: "removed", detail: `Deleted ${shortPath2(filePath)}`, target: filePath };
50315
- } catch (err31) {
50316
- return { conflictId: conflict.id, action: "skipped", detail: `Failed to delete: ${err31.message}`, target: filePath };
50796
+ } catch (err32) {
50797
+ return { conflictId: conflict.id, action: "skipped", detail: `Failed to delete: ${err32.message}`, target: filePath };
50317
50798
  }
50318
50799
  }
50319
50800
  function fixGlobalArtifact(conflict, opts) {
@@ -50327,8 +50808,8 @@ function fixGlobalArtifact(conflict, opts) {
50327
50808
  try {
50328
50809
  fs65.rmSync(dirPath, { recursive: true, force: true });
50329
50810
  return { conflictId: conflict.id, action: "removed", detail: `Removed ${shortPath2(dirPath)}`, target: dirPath };
50330
- } catch (err31) {
50331
- return { conflictId: conflict.id, action: "skipped", detail: `Failed to remove: ${err31.message}`, target: dirPath };
50811
+ } catch (err32) {
50812
+ return { conflictId: conflict.id, action: "skipped", detail: `Failed to remove: ${err32.message}`, target: dirPath };
50332
50813
  }
50333
50814
  }
50334
50815
  function shortPath2(p4) {
@@ -50339,7 +50820,7 @@ function shortPath2(p4) {
50339
50820
 
50340
50821
  // src/project-root.ts
50341
50822
  import fs66 from "fs";
50342
- import path75 from "path";
50823
+ import path76 from "path";
50343
50824
  var ROOT_MARKERS = [
50344
50825
  ".git",
50345
50826
  "package.json",
@@ -50353,14 +50834,14 @@ var ROOT_MARKERS = [
50353
50834
  "build.gradle.kts"
50354
50835
  ];
50355
50836
  function findProjectRoot(from) {
50356
- let dir = path75.resolve(from ?? process.cwd());
50837
+ let dir = path76.resolve(from ?? process.cwd());
50357
50838
  while (true) {
50358
50839
  for (const marker of ROOT_MARKERS) {
50359
- if (fs66.existsSync(path75.join(dir, marker))) {
50840
+ if (fs66.existsSync(path76.join(dir, marker))) {
50360
50841
  return dir;
50361
50842
  }
50362
50843
  }
50363
- const parent = path75.dirname(dir);
50844
+ const parent = path76.dirname(dir);
50364
50845
  if (parent === dir) {
50365
50846
  throw new Error(
50366
50847
  `Could not find project root from ${from ?? process.cwd()}. Looked for: ${ROOT_MARKERS.join(", ")}`
@@ -50604,7 +51085,7 @@ function generateConfig(detection) {
50604
51085
 
50605
51086
  // src/registry.ts
50606
51087
  import fs67 from "fs";
50607
- import path76 from "path";
51088
+ import path77 from "path";
50608
51089
  function emptyRegistry() {
50609
51090
  return { version: 1, projects: {} };
50610
51091
  }
@@ -50625,7 +51106,7 @@ function saveRegistry(reg) {
50625
51106
  fs67.renameSync(tmp, REGISTRY_PATH);
50626
51107
  }
50627
51108
  function registerProject(root) {
50628
- const absRoot = path76.resolve(root);
51109
+ const absRoot = path77.resolve(root);
50629
51110
  const reg = loadRegistry2();
50630
51111
  if (reg.projects[absRoot]) {
50631
51112
  return reg.projects[absRoot];
@@ -50642,7 +51123,7 @@ function registerProject(root) {
50642
51123
  return entry;
50643
51124
  }
50644
51125
  function getProject(root) {
50645
- const absRoot = path76.resolve(root);
51126
+ const absRoot = path77.resolve(root);
50646
51127
  const reg = loadRegistry2();
50647
51128
  return reg.projects[absRoot] ?? null;
50648
51129
  }
@@ -50651,7 +51132,7 @@ function listProjects() {
50651
51132
  return Object.values(reg.projects);
50652
51133
  }
50653
51134
  function updateLastIndexed(root) {
50654
- const absRoot = path76.resolve(root);
51135
+ const absRoot = path77.resolve(root);
50655
51136
  const reg = loadRegistry2();
50656
51137
  if (reg.projects[absRoot]) {
50657
51138
  reg.projects[absRoot].lastIndexed = (/* @__PURE__ */ new Date()).toISOString();
@@ -50861,7 +51342,7 @@ function registerAndIndexProject(dir, opts) {
50861
51342
  const config = generateConfig(detection);
50862
51343
  saveProjectConfig(projectRoot, { root: config.root, include: config.include, exclude: config.exclude });
50863
51344
  const dbPath = getDbPath(projectRoot);
50864
- const oldDbPath = path77.join(projectRoot, ".trace-mcp", "index.db");
51345
+ const oldDbPath = path78.join(projectRoot, ".trace-mcp", "index.db");
50865
51346
  if (fs68.existsSync(oldDbPath) && !fs68.existsSync(dbPath)) {
50866
51347
  fs68.copyFileSync(oldDbPath, dbPath);
50867
51348
  }
@@ -50895,11 +51376,11 @@ function shortPath3(p4) {
50895
51376
  // src/cli-upgrade.ts
50896
51377
  import { Command as Command2 } from "commander";
50897
51378
  import fs69 from "fs";
50898
- import path78 from "path";
51379
+ import path79 from "path";
50899
51380
  var upgradeCommand = new Command2("upgrade").description("Upgrade trace-mcp: run DB migrations, reindex with latest plugins, update hooks and CLAUDE.md").argument("[dir]", "Project directory (omit to upgrade all registered projects)").option("--skip-hooks", "Do not update guard hooks").option("--skip-reindex", "Do not trigger reindex").option("--skip-claude-md", "Do not update CLAUDE.md block").option("--dry-run", "Show what would be done without writing files").option("--json", "Output results as JSON").action(async (dir, opts) => {
50900
51381
  const projectRoots = [];
50901
51382
  if (dir) {
50902
- projectRoots.push(path78.resolve(dir));
51383
+ projectRoots.push(path79.resolve(dir));
50903
51384
  } else {
50904
51385
  const projects = listProjects();
50905
51386
  if (projects.length === 0) {
@@ -50981,7 +51462,7 @@ var upgradeCommand = new Command2("upgrade").description("Upgrade trace-mcp: run
50981
51462
  console.log(header);
50982
51463
  for (const { projectRoot, steps } of allSteps) {
50983
51464
  console.log(`
50984
- Project: ${path78.basename(projectRoot)} (${projectRoot})`);
51465
+ Project: ${path79.basename(projectRoot)} (${projectRoot})`);
50985
51466
  for (const step of steps) {
50986
51467
  console.log(` ${step.action}: ${step.detail ?? step.target}`);
50987
51468
  }
@@ -50993,10 +51474,10 @@ var upgradeCommand = new Command2("upgrade").description("Upgrade trace-mcp: run
50993
51474
  // src/cli-add.ts
50994
51475
  import { Command as Command3 } from "commander";
50995
51476
  import fs70 from "fs";
50996
- import path79 from "path";
51477
+ import path80 from "path";
50997
51478
  import * as p2 from "@clack/prompts";
50998
51479
  var addCommand = new Command3("add").description("Register a project for indexing: detect root, create DB, add to registry").argument("[dir]", "Project directory (default: current directory)", ".").option("--force", "Re-register even if already registered").option("--json", "Output results as JSON").action(async (dir, opts) => {
50999
- const resolvedDir = path79.resolve(dir);
51480
+ const resolvedDir = path80.resolve(dir);
51000
51481
  if (!fs70.existsSync(resolvedDir)) {
51001
51482
  console.error(`Directory does not exist: ${resolvedDir}`);
51002
51483
  process.exit(1);
@@ -51051,7 +51532,7 @@ DB: ${shortPath4(existing.dbPath)}`, "Existing");
51051
51532
  ensureGlobalDirs();
51052
51533
  saveProjectConfig(projectRoot, configForSave);
51053
51534
  const dbPath = getDbPath(projectRoot);
51054
- const oldDbPath = path79.join(projectRoot, ".trace-mcp", "index.db");
51535
+ const oldDbPath = path80.join(projectRoot, ".trace-mcp", "index.db");
51055
51536
  let migrated = false;
51056
51537
  if (fs70.existsSync(oldDbPath) && !fs70.existsSync(dbPath)) {
51057
51538
  fs70.copyFileSync(oldDbPath, dbPath);
@@ -51228,7 +51709,7 @@ function shortPath5(p4) {
51228
51709
  // src/cli-ci.ts
51229
51710
  import { Command as Command5 } from "commander";
51230
51711
  import { execFileSync as execFileSync7 } from "child_process";
51231
- import path80 from "path";
51712
+ import path81 from "path";
51232
51713
  import fs71 from "fs";
51233
51714
 
51234
51715
  // src/ci/report-generator.ts
@@ -51608,8 +52089,8 @@ function writeOutput(outputPath, content) {
51608
52089
  if (outputPath === "-" || !outputPath) {
51609
52090
  process.stdout.write(content + "\n");
51610
52091
  } else {
51611
- const resolved = path80.resolve(outputPath);
51612
- fs71.mkdirSync(path80.dirname(resolved), { recursive: true });
52092
+ const resolved = path81.resolve(outputPath);
52093
+ fs71.mkdirSync(path81.dirname(resolved), { recursive: true });
51613
52094
  fs71.writeFileSync(resolved, content, "utf-8");
51614
52095
  logger.info({ path: resolved }, "CI report written");
51615
52096
  }
@@ -51845,22 +52326,22 @@ program.command("serve").description("Start MCP server (stdio transport)").actio
51845
52326
  ) : null;
51846
52327
  const runEmbeddings = () => {
51847
52328
  if (!embeddingPipeline) return;
51848
- embeddingPipeline.indexUnembedded().catch((err31) => {
51849
- logger.error({ error: err31 }, "Embedding indexing failed");
52329
+ embeddingPipeline.indexUnembedded().catch((err32) => {
52330
+ logger.error({ error: err32 }, "Embedding indexing failed");
51850
52331
  });
51851
52332
  };
51852
52333
  const runSummarization = () => {
51853
52334
  if (!summarizationPipeline) return;
51854
- summarizationPipeline.summarizeUnsummarized().catch((err31) => {
51855
- logger.error({ error: err31 }, "Summarization failed");
52335
+ summarizationPipeline.summarizeUnsummarized().catch((err32) => {
52336
+ logger.error({ error: err32 }, "Summarization failed");
51856
52337
  });
51857
52338
  };
51858
52339
  pipeline.indexAll().then(() => {
51859
52340
  runSummarization();
51860
52341
  runEmbeddings();
51861
52342
  runFederationAutoSync(projectRoot, config);
51862
- }).catch((err31) => {
51863
- logger.error({ error: err31 }, "Initial indexing failed");
52343
+ }).catch((err32) => {
52344
+ logger.error({ error: err32 }, "Initial indexing failed");
51864
52345
  });
51865
52346
  await watcher.start(projectRoot, config, async (paths) => {
51866
52347
  await pipeline.indexFiles(paths);
@@ -51913,22 +52394,22 @@ program.command("serve-http").description("Start MCP server (HTTP/SSE transport)
51913
52394
  ) : null;
51914
52395
  const runEmbeddings = () => {
51915
52396
  if (!embeddingPipeline) return;
51916
- embeddingPipeline.indexUnembedded().catch((err31) => {
51917
- logger.error({ error: err31 }, "Embedding indexing failed");
52397
+ embeddingPipeline.indexUnembedded().catch((err32) => {
52398
+ logger.error({ error: err32 }, "Embedding indexing failed");
51918
52399
  });
51919
52400
  };
51920
52401
  const runSummarization2 = () => {
51921
52402
  if (!summarizationPipeline2) return;
51922
- summarizationPipeline2.summarizeUnsummarized().catch((err31) => {
51923
- logger.error({ error: err31 }, "Summarization failed");
52403
+ summarizationPipeline2.summarizeUnsummarized().catch((err32) => {
52404
+ logger.error({ error: err32 }, "Summarization failed");
51924
52405
  });
51925
52406
  };
51926
52407
  pipeline.indexAll().then(() => {
51927
52408
  runSummarization2();
51928
52409
  runEmbeddings();
51929
52410
  runFederationAutoSync(projectRoot, config);
51930
- }).catch((err31) => {
51931
- logger.error({ error: err31 }, "Initial indexing failed");
52411
+ }).catch((err32) => {
52412
+ logger.error({ error: err32 }, "Initial indexing failed");
51932
52413
  });
51933
52414
  await watcher.start(projectRoot, config, async (paths) => {
51934
52415
  await pipeline.indexFiles(paths);
@@ -52031,7 +52512,7 @@ program.command("serve-http").description("Start MCP server (HTTP/SSE transport)
52031
52512
  });
52032
52513
  });
52033
52514
  program.command("index").description("Index a project directory").argument("<dir>", "Directory to index").option("-f, --force", "Force reindex all files").action(async (dir, opts) => {
52034
- const resolvedDir = path81.resolve(dir);
52515
+ const resolvedDir = path82.resolve(dir);
52035
52516
  if (!fs72.existsSync(resolvedDir)) {
52036
52517
  logger.error({ dir: resolvedDir }, "Directory does not exist");
52037
52518
  process.exit(1);
@@ -52056,13 +52537,13 @@ program.command("index").description("Index a project directory").argument("<dir
52056
52537
  db.close();
52057
52538
  });
52058
52539
  program.command("index-file").description("Incrementally reindex a single file (called by the PostToolUse auto-reindex hook)").argument("<file>", "Absolute or relative path to the file to reindex").action(async (file) => {
52059
- const resolvedFile = path81.resolve(file);
52540
+ const resolvedFile = path82.resolve(file);
52060
52541
  if (!fs72.existsSync(resolvedFile)) {
52061
52542
  process.exit(0);
52062
52543
  }
52063
52544
  let projectRoot;
52064
52545
  try {
52065
- projectRoot = findProjectRoot(path81.dirname(resolvedFile));
52546
+ projectRoot = findProjectRoot(path82.dirname(resolvedFile));
52066
52547
  } catch {
52067
52548
  process.exit(0);
52068
52549
  }