@codragraph/cli 2.1.1 → 2.1.5

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.
Files changed (112) hide show
  1. package/README.md +36 -9
  2. package/dist/cli/ai-context.js +298 -1
  3. package/dist/cli/analyze.js +19 -2
  4. package/dist/cli/index.js +33 -12
  5. package/dist/cli/serve.d.ts +1 -0
  6. package/dist/cli/serve.js +3 -1
  7. package/dist/cli/setup.js +36 -19
  8. package/dist/cli/status.d.ts +13 -0
  9. package/dist/cli/status.js +99 -0
  10. package/dist/cli/tool.js +73 -33
  11. package/dist/config/ignore-service.js +3 -0
  12. package/dist/core/cgdb/pool-adapter.js +130 -20
  13. package/dist/core/graphstore/cgdb-row-source.js +3 -2
  14. package/dist/core/group/bridge-db.js +42 -10
  15. package/dist/core/ingestion/parsing-processor.js +7 -1
  16. package/dist/core/ingestion/pipeline-phases/parse-impl.js +4 -0
  17. package/dist/core/ingestion/workers/parse-worker.js +1 -1
  18. package/dist/core/ingestion/workers/worker-pool.d.ts +14 -1
  19. package/dist/core/ingestion/workers/worker-pool.js +33 -17
  20. package/dist/core/run-analyze.d.ts +20 -0
  21. package/dist/core/run-analyze.js +225 -1
  22. package/dist/core/search/bm25-index.d.ts +0 -11
  23. package/dist/core/search/bm25-index.js +7 -84
  24. package/dist/core/search/hybrid-search.js +11 -3
  25. package/dist/mcp/local/local-backend.d.ts +2 -0
  26. package/dist/mcp/local/local-backend.js +235 -18
  27. package/dist/mcp/resources.js +2 -2
  28. package/dist/server/api.d.ts +14 -2
  29. package/dist/server/api.js +90 -7
  30. package/dist/server/mcp-http.d.ts +22 -0
  31. package/dist/server/mcp-http.js +21 -2
  32. package/dist/server/web-dashboard.d.ts +28 -0
  33. package/dist/server/web-dashboard.js +61 -0
  34. package/dist/web/assets/agent-D5lb0zXz.js +1089 -0
  35. package/dist/web/assets/architectureDiagram-EMZXCZ2Q-CZtc99v_.js +36 -0
  36. package/dist/web/assets/blockDiagram-IGV67L2C-BtoUp-6Y.js +132 -0
  37. package/dist/web/assets/c4Diagram-DFAF54RM-C4Hl3J2U.js +10 -0
  38. package/dist/web/assets/chunk-3GS5O3IE-DkUjU0WD.js +231 -0
  39. package/dist/web/assets/chunk-3YCYZ6SJ-CQkVgT_z.js +1 -0
  40. package/dist/web/assets/chunk-7RZVMHOQ-BitYcNVR.js +338 -0
  41. package/dist/web/assets/chunk-AEOMTBSW-BgTIXPsY.js +1 -0
  42. package/dist/web/assets/chunk-H3VCZNTA-Cx5XV_aC.js +13 -0
  43. package/dist/web/assets/chunk-HN6EAY2L-BBnyTNdB.js +1 -0
  44. package/dist/web/assets/chunk-KSICW3F5-BYzvDLNI.js +15 -0
  45. package/dist/web/assets/chunk-O5ABG6QK-dHwHzA6n.js +1 -0
  46. package/dist/web/assets/chunk-PK6DOVAG-CvsEnugt.js +206 -0
  47. package/dist/web/assets/chunk-RWUO3TPN-BgRTY0_k.js +1 -0
  48. package/dist/web/assets/chunk-TBF5ZNIQ-DL5stGM1.js +1 -0
  49. package/dist/web/assets/chunk-TU3PZOEN-RLyvLcv-.js +1 -0
  50. package/dist/web/assets/classDiagram-PPOCWD7C-DTr8QIOf.js +1 -0
  51. package/dist/web/assets/classDiagram-v2-23LJLIIU-DTr8QIOf.js +1 -0
  52. package/dist/web/assets/context-builder-22jU3V56.js +16 -0
  53. package/dist/web/assets/cose-bilkent-PNC4W37J-DVhePRYg.js +1 -0
  54. package/dist/web/assets/dagre-E77IOHMT-Dzx0A6ZU.js +4 -0
  55. package/dist/web/assets/diagram-H7BISOXX-CC9pRew1.js +43 -0
  56. package/dist/web/assets/diagram-JC5VWROH-Bau_i9tf.js +24 -0
  57. package/dist/web/assets/diagram-LXUTUG65-D9_FM2Gt.js +10 -0
  58. package/dist/web/assets/diagram-WEHSV5V5-BMlayouL.js +24 -0
  59. package/dist/web/assets/erDiagram-GCSMX5X6-C3dhDFA8.js +85 -0
  60. package/dist/web/assets/flowDiagram-OTCZ4VVT-CWSFWmhr.js +162 -0
  61. package/dist/web/assets/ganttDiagram-MUNLMDZQ-D3a67Yol.js +292 -0
  62. package/dist/web/assets/gitGraphDiagram-3HKGZ4G3-7jmry-vM.js +106 -0
  63. package/dist/web/assets/index-BgeqpYgd.js +1415 -0
  64. package/dist/web/assets/index-CT0GtFLZ.css +1 -0
  65. package/dist/web/assets/infoDiagram-MN7RKWGX-G7lhP0Ib.js +2 -0
  66. package/dist/web/assets/ishikawaDiagram-YMYX4NHK-DUoJvNP2.js +70 -0
  67. package/dist/web/assets/journeyDiagram-SO5T7YLQ-RMFPNNqz.js +139 -0
  68. package/dist/web/assets/kanban-definition-LJHFXRCJ-BzpDs1K9.js +89 -0
  69. package/dist/web/assets/katex-GD7MH7QM-DBQvrix-.js +261 -0
  70. package/dist/web/assets/mindmap-definition-2EUWGEK5-Bk0O4roa.js +96 -0
  71. package/dist/web/assets/pieDiagram-3IATQBI2-DKU7kpgS.js +30 -0
  72. package/dist/web/assets/quadrantDiagram-E256RVCF-BY0TGWCS.js +7 -0
  73. package/dist/web/assets/requirementDiagram-M5DCFWZL-DLHOVTSv.js +84 -0
  74. package/dist/web/assets/sankeyDiagram-L3NBLAOT-DVMj5rX2.js +10 -0
  75. package/dist/web/assets/sequenceDiagram-ZOUHS735-CJC73bV-.js +157 -0
  76. package/dist/web/assets/stateDiagram-MLPALWAM-BCFyESls.js +1 -0
  77. package/dist/web/assets/stateDiagram-v2-B5LQ5ZB2-DahzzIca.js +1 -0
  78. package/dist/web/assets/timeline-definition-5SPVSISX-TRSDRgPw.js +120 -0
  79. package/dist/web/assets/vennDiagram-IE5QUKF5-DNy7HRBM.js +34 -0
  80. package/dist/web/assets/wardley-RL74JXVD-BCRCBASE-B-eZEzf9.js +161 -0
  81. package/dist/web/assets/wardleyDiagram-XU3VSMPF-BP-r1xzR.js +20 -0
  82. package/dist/web/assets/xychartDiagram-ZHJ5623Y-Dr9r7a35.js +7 -0
  83. package/dist/web/codragraph-logo-512.png +0 -0
  84. package/dist/web/codragraph-logo.png +0 -0
  85. package/dist/web/favicon.png +0 -0
  86. package/dist/web/index.html +36 -0
  87. package/hooks/claude/codragraph-hook.cjs +18 -110
  88. package/hooks/claude/pre-tool-use.sh +6 -1
  89. package/package.json +3 -1
  90. package/scripts/build.js +62 -4
  91. package/scripts/patch-tree-sitter-swift.cjs +0 -1
  92. package/skills/codragraph-cli.md +1 -1
  93. package/vendor/leiden/index.cjs +272 -285
  94. package/vendor/leiden/utils.cjs +264 -274
  95. package/dist/_shared/lbug/schema-constants.d.ts +0 -16
  96. package/dist/_shared/lbug/schema-constants.d.ts.map +0 -1
  97. package/dist/_shared/lbug/schema-constants.js +0 -67
  98. package/dist/_shared/lbug/schema-constants.js.map +0 -1
  99. package/dist/core/graphstore/lbug-row-source.d.ts +0 -19
  100. package/dist/core/graphstore/lbug-row-source.js +0 -141
  101. package/dist/core/lbug/content-read.d.ts +0 -46
  102. package/dist/core/lbug/content-read.js +0 -64
  103. package/dist/core/lbug/csv-generator.d.ts +0 -29
  104. package/dist/core/lbug/csv-generator.js +0 -492
  105. package/dist/core/lbug/lbug-adapter.d.ts +0 -176
  106. package/dist/core/lbug/lbug-adapter.js +0 -1320
  107. package/dist/core/lbug/pool-adapter.d.ts +0 -93
  108. package/dist/core/lbug/pool-adapter.js +0 -550
  109. package/dist/core/lbug/schema.d.ts +0 -62
  110. package/dist/core/lbug/schema.js +0 -502
  111. package/dist/mcp/core/lbug-adapter.d.ts +0 -5
  112. package/dist/mcp/core/lbug-adapter.js +0 -5
package/dist/cli/setup.js CHANGED
@@ -23,7 +23,7 @@ const pkg = require('../../package.json');
23
23
  const CLI_PACKAGE_SPEC = `@codragraph/cli@${pkg.version}`;
24
24
  /**
25
25
  * Resolve the absolute path to the `codragraph` binary if it's installed
26
- * globally (or via npm -g / yarn global). Returns null when not found.
26
+ * globally (or via npm -g / bun -g / yarn global). Returns null when not found.
27
27
  *
28
28
  * Note: the npm package is `@codragraph/cli`, but the executable it installs
29
29
  * is `codragraph` (see package.json `bin`). PATH lookup must use the bin name.
@@ -49,7 +49,7 @@ function resolveCodragraphBin() {
49
49
  .filter(Boolean);
50
50
  if (process.platform === 'win32') {
51
51
  // Prefer a Windows-executable shim. If none is on PATH, return null so
52
- // the caller falls through to the `cmd /c npx -y @codragraph/cli` path.
52
+ // the caller falls through to the package-runner fallback.
53
53
  const exe = lines.find((l) => /\.(cmd|exe|bat)$/i.test(l));
54
54
  return exe ?? null;
55
55
  }
@@ -63,28 +63,40 @@ function resolveCodragraphBin() {
63
63
  * The MCP server entry for all editors.
64
64
  *
65
65
  * Prefers the globally-installed `codragraph` binary (starts in ~1 s) over
66
- * `npx -y @codragraph/cli@<version>` (cold-cache install of native deps can take
66
+ * `npx` / `bunx` package execution (cold-cache install of native deps can take
67
67
  * >60 s, exceeding Claude Code's 30 s MCP connection timeout).
68
68
  *
69
- * Falls back to npx when the binary isn't on PATH — e.g. first-time
70
- * users who ran `npx @codragraph/cli analyze` but haven't done `npm i -g`.
69
+ * Falls back to the package runner that invoked setup when the binary isn't on
70
+ * PATH e.g. first-time `npx @codragraph/cli` or `bunx @codragraph/cli` users.
71
71
  *
72
72
  * Windows note: even when the bin is on PATH, we launch via `cmd /c codragraph
73
73
  * mcp` rather than writing the resolved path. Reason: `where codragraph`
74
74
  * returns the extensionless Unix shim before `codragraph.cmd`, and Node's
75
75
  * spawn/execFile cannot launch the extensionless shim on Windows. Letting
76
76
  * cmd resolve via PATHEXT is the only reliable path that works for npm-,
77
- * pnpm-, and yarn-installed shims alike.
77
+ * bun-, pnpm-, and yarn-installed shims alike.
78
78
  */
79
- function getMcpEntry() {
80
- const bin = resolveCodragraphBin();
81
- if (bin) {
79
+ function isRunningUnderBun() {
80
+ const userAgent = (process.env.npm_config_user_agent ?? '').toLowerCase();
81
+ const rawExecPath = process.env.npm_execpath ?? '';
82
+ const execPath = process.platform === 'win32'
83
+ ? path.win32.basename(rawExecPath).toLowerCase()
84
+ : path.basename(rawExecPath).toLowerCase();
85
+ return userAgent.startsWith('bun/') || execPath === 'bun' || execPath === 'bun.exe';
86
+ }
87
+ function getPackageRunnerMcpEntry() {
88
+ if (isRunningUnderBun()) {
82
89
  if (process.platform === 'win32') {
83
- return { command: 'cmd', args: ['/c', 'codragraph', 'mcp'] };
90
+ return {
91
+ command: 'cmd',
92
+ args: ['/c', 'bunx', CLI_PACKAGE_SPEC, 'mcp'],
93
+ };
84
94
  }
85
- return { command: bin, args: ['mcp'] };
95
+ return {
96
+ command: 'bunx',
97
+ args: [CLI_PACKAGE_SPEC, 'mcp'],
98
+ };
86
99
  }
87
- // Fallback: npx (works without a global install, but slow cold-start)
88
100
  if (process.platform === 'win32') {
89
101
  return {
90
102
  command: 'cmd',
@@ -96,6 +108,16 @@ function getMcpEntry() {
96
108
  args: ['-y', CLI_PACKAGE_SPEC, 'mcp'],
97
109
  };
98
110
  }
111
+ function getMcpEntry() {
112
+ const bin = resolveCodragraphBin();
113
+ if (bin) {
114
+ if (process.platform === 'win32') {
115
+ return { command: 'cmd', args: ['/c', 'codragraph', 'mcp'] };
116
+ }
117
+ return { command: bin, args: ['mcp'] };
118
+ }
119
+ return getPackageRunnerMcpEntry();
120
+ }
99
121
  /**
100
122
  * OpenCode uses a different MCP format: { type: "local", command: [...] }
101
123
  * where command is a flat array (command + args combined).
@@ -108,13 +130,8 @@ function getOpenCodeMcpEntry() {
108
130
  }
109
131
  return { type: 'local', command: [bin, 'mcp'] };
110
132
  }
111
- if (process.platform === 'win32') {
112
- return {
113
- type: 'local',
114
- command: ['cmd', '/c', 'npx', '-y', CLI_PACKAGE_SPEC, 'mcp'],
115
- };
116
- }
117
- return { type: 'local', command: ['npx', '-y', CLI_PACKAGE_SPEC, 'mcp'] };
133
+ const entry = getPackageRunnerMcpEntry();
134
+ return { type: 'local', command: [entry.command, ...entry.args] };
118
135
  }
119
136
  /**
120
137
  * Merge codragraph entry into an existing MCP config JSON object.
@@ -3,4 +3,17 @@
3
3
  *
4
4
  * Shows the indexing status of the current repository.
5
5
  */
6
+ import { type RepoMeta } from '../storage/repo-manager.js';
7
+ export declare const LARGE_INDEX_WARNING_BYTES: number;
8
+ export interface IndexStorageSummary {
9
+ path: string;
10
+ bytes: number;
11
+ fileCount: number;
12
+ unreadableEntries: number;
13
+ }
14
+ export declare const formatBytes: (bytes: number) => string;
15
+ export declare const summarizeIndexStorage: (storagePath: string) => Promise<IndexStorageSummary | null>;
16
+ export declare const formatIndexStatusLines: (summary: IndexStorageSummary | null, meta: Pick<RepoMeta, "stats" | "compress">, options?: {
17
+ largeIndexWarningBytes?: number;
18
+ }) => string[];
6
19
  export declare const statusCommand: () => Promise<void>;
@@ -3,8 +3,99 @@
3
3
  *
4
4
  * Shows the indexing status of the current repository.
5
5
  */
6
+ import fs from 'fs/promises';
7
+ import path from 'path';
6
8
  import { findRepo, getStoragePaths, hasKuzuIndex } from '../storage/repo-manager.js';
7
9
  import { getCurrentCommit, isGitRepo, getGitRoot } from '../storage/git.js';
10
+ export const LARGE_INDEX_WARNING_BYTES = 500 * 1024 * 1024;
11
+ const STORAGE_SCAN_BATCH_SIZE = 64;
12
+ export const formatBytes = (bytes) => {
13
+ if (!Number.isFinite(bytes) || bytes <= 0)
14
+ return '0 B';
15
+ const units = ['B', 'KB', 'MB', 'GB', 'TB'];
16
+ let value = bytes;
17
+ let unitIndex = 0;
18
+ while (value >= 1024 && unitIndex < units.length - 1) {
19
+ value /= 1024;
20
+ unitIndex += 1;
21
+ }
22
+ if (unitIndex === 0)
23
+ return `${Math.round(value)} ${units[unitIndex]}`;
24
+ return `${value.toFixed(1)} ${units[unitIndex]}`;
25
+ };
26
+ export const summarizeIndexStorage = async (storagePath) => {
27
+ let rootStat;
28
+ try {
29
+ rootStat = await fs.lstat(storagePath);
30
+ }
31
+ catch (error) {
32
+ if (error.code === 'ENOENT')
33
+ return null;
34
+ return { path: storagePath, bytes: 0, fileCount: 0, unreadableEntries: 1 };
35
+ }
36
+ if (!rootStat.isDirectory()) {
37
+ return { path: storagePath, bytes: rootStat.size, fileCount: 1, unreadableEntries: 0 };
38
+ }
39
+ const stack = [storagePath];
40
+ let bytes = 0;
41
+ let fileCount = 0;
42
+ let unreadableEntries = 0;
43
+ while (stack.length > 0) {
44
+ const current = stack.pop();
45
+ let entries;
46
+ try {
47
+ entries = await fs.readdir(current, { withFileTypes: true });
48
+ }
49
+ catch {
50
+ unreadableEntries += 1;
51
+ continue;
52
+ }
53
+ for (let i = 0; i < entries.length; i += STORAGE_SCAN_BATCH_SIZE) {
54
+ const batch = entries.slice(i, i + STORAGE_SCAN_BATCH_SIZE);
55
+ const stats = await Promise.all(batch.map(async (entry) => {
56
+ const entryPath = path.join(current, entry.name);
57
+ try {
58
+ return { entryPath, stat: await fs.lstat(entryPath) };
59
+ }
60
+ catch {
61
+ return { entryPath, stat: null };
62
+ }
63
+ }));
64
+ for (const { entryPath, stat } of stats) {
65
+ if (!stat) {
66
+ unreadableEntries += 1;
67
+ continue;
68
+ }
69
+ if (stat.isDirectory()) {
70
+ stack.push(entryPath);
71
+ continue;
72
+ }
73
+ bytes += stat.size;
74
+ fileCount += 1;
75
+ }
76
+ }
77
+ }
78
+ return { path: storagePath, bytes, fileCount, unreadableEntries };
79
+ };
80
+ export const formatIndexStatusLines = (summary, meta, options = {}) => {
81
+ const lines = [];
82
+ const threshold = options.largeIndexWarningBytes ?? LARGE_INDEX_WARNING_BYTES;
83
+ if (summary) {
84
+ const fileLabel = summary.fileCount === 1 ? 'file' : 'files';
85
+ lines.push(`Index size: ${formatBytes(summary.bytes)} (${summary.fileCount.toLocaleString('en-US')} ${fileLabel} under .codragraph)`);
86
+ if (summary.unreadableEntries > 0) {
87
+ const entryLabel = summary.unreadableEntries === 1 ? 'entry' : 'entries';
88
+ lines.push(`Index size note: ${summary.unreadableEntries.toLocaleString('en-US')} ${entryLabel} could not be read.`);
89
+ }
90
+ if (summary.bytes >= threshold) {
91
+ lines.push(`Storage warning: .codragraph is ${formatBytes(summary.bytes)} (>= ${formatBytes(threshold)}). Consider codragraph analyze --compress brotli (or zstd on Node >=22.15) and only use --embeddings when vectors are needed.`);
92
+ }
93
+ }
94
+ const embeddings = meta.stats?.embeddings;
95
+ lines.push(`Embeddings: ${typeof embeddings === 'number' ? embeddings.toLocaleString('en-US') : 'unknown'}`);
96
+ lines.push(`Compression: ${meta.compress ?? 'none'}`);
97
+ return lines;
98
+ };
8
99
  export const statusCommand = async () => {
9
100
  const cwd = process.cwd();
10
101
  if (!isGitRepo(cwd)) {
@@ -16,6 +107,7 @@ export const statusCommand = async () => {
16
107
  // Check if there's a stale KuzuDB index that needs migration
17
108
  const repoRoot = getGitRoot(cwd) ?? cwd;
18
109
  const { storagePath } = getStoragePaths(repoRoot);
110
+ const storageSummary = await summarizeIndexStorage(storagePath);
19
111
  if (await hasKuzuIndex(storagePath)) {
20
112
  console.log('Repository has a stale KuzuDB index from a previous version.');
21
113
  console.log('Run: codragraph analyze (rebuilds the index with LadybugDB)');
@@ -24,13 +116,20 @@ export const statusCommand = async () => {
24
116
  console.log('Repository not indexed.');
25
117
  console.log('Run: codragraph analyze');
26
118
  }
119
+ if (storageSummary) {
120
+ for (const line of formatIndexStatusLines(storageSummary, {}))
121
+ console.log(line);
122
+ }
27
123
  return;
28
124
  }
29
125
  const currentCommit = getCurrentCommit(repo.repoPath);
30
126
  const isUpToDate = currentCommit === repo.meta.lastCommit;
127
+ const storageSummary = await summarizeIndexStorage(repo.storagePath);
31
128
  console.log(`Repository: ${repo.repoPath}`);
32
129
  console.log(`Indexed: ${new Date(repo.meta.indexedAt).toLocaleString()}`);
33
130
  console.log(`Indexed commit: ${repo.meta.lastCommit?.slice(0, 7)}`);
34
131
  console.log(`Current commit: ${currentCommit?.slice(0, 7)}`);
132
+ for (const line of formatIndexStatusLines(storageSummary, repo.meta))
133
+ console.log(line);
35
134
  console.log(`Status: ${isUpToDate ? '✅ up-to-date' : '⚠️ stale (re-run codragraph analyze)'}`);
36
135
  };
package/dist/cli/tool.js CHANGED
@@ -17,6 +17,7 @@
17
17
  import { writeSync } from 'node:fs';
18
18
  import { LocalBackend } from '../mcp/local/local-backend.js';
19
19
  import { emitTokenStats } from './compress-stats.js';
20
+ import { findRepo } from '../storage/repo-manager.js';
20
21
  let _backend = null;
21
22
  async function getBackend() {
22
23
  if (_backend)
@@ -29,6 +30,21 @@ async function getBackend() {
29
30
  }
30
31
  return _backend;
31
32
  }
33
+ async function callToolOnce(toolName, params) {
34
+ const backend = await getBackend();
35
+ return backend.callTool(toolName, params);
36
+ }
37
+ async function resolveCliRepoParam(repoParam) {
38
+ if (repoParam)
39
+ return repoParam;
40
+ const currentRepo = await findRepo(process.cwd());
41
+ if (currentRepo)
42
+ return currentRepo.repoPath;
43
+ output(`Error: Current repository is not indexed: ${process.cwd()}\n` +
44
+ 'Run: npx @codragraph/cli analyze');
45
+ process.exitCode = 1;
46
+ return null;
47
+ }
32
48
  /**
33
49
  * Write tool output to stdout using low-level fd write.
34
50
  *
@@ -59,14 +75,16 @@ export async function queryCommand(queryText, options) {
59
75
  console.error('Usage: codragraph query <search_query>');
60
76
  process.exit(1);
61
77
  }
62
- const backend = await getBackend();
63
- const result = await backend.callTool('query', {
78
+ const repo = await resolveCliRepoParam(options?.repo);
79
+ if (!repo)
80
+ return;
81
+ const result = await callToolOnce('query', {
64
82
  query: queryText,
65
83
  task_context: options?.context,
66
84
  goal: options?.goal,
67
85
  limit: options?.limit ? parseInt(options.limit) : undefined,
68
86
  include_content: options?.content ?? false,
69
- repo: options?.repo,
87
+ repo,
70
88
  });
71
89
  output(result);
72
90
  emitTokenStats(result);
@@ -76,13 +94,15 @@ export async function contextCommand(name, options) {
76
94
  console.error('Usage: codragraph context <symbol_name> [--uid <uid>] [--file <path>]');
77
95
  process.exit(1);
78
96
  }
79
- const backend = await getBackend();
80
- const result = await backend.callTool('context', {
97
+ const repo = await resolveCliRepoParam(options?.repo);
98
+ if (!repo)
99
+ return;
100
+ const result = await callToolOnce('context', {
81
101
  name: name || undefined,
82
102
  uid: options?.uid,
83
103
  file_path: options?.file,
84
104
  include_content: options?.content ?? false,
85
- repo: options?.repo,
105
+ repo,
86
106
  });
87
107
  output(result);
88
108
  emitTokenStats(result);
@@ -93,13 +113,15 @@ export async function impactCommand(target, options) {
93
113
  process.exit(1);
94
114
  }
95
115
  try {
96
- const backend = await getBackend();
97
- const result = await backend.callTool('impact', {
116
+ const repo = await resolveCliRepoParam(options?.repo);
117
+ if (!repo)
118
+ return;
119
+ const result = await callToolOnce('impact', {
98
120
  target,
99
121
  direction: options?.direction || 'upstream',
100
122
  maxDepth: options?.depth ? parseInt(options.depth, 10) : undefined,
101
123
  includeTests: options?.includeTests ?? false,
102
- repo: options?.repo,
124
+ repo,
103
125
  });
104
126
  output(result);
105
127
  emitTokenStats(result);
@@ -121,26 +143,32 @@ export async function cypherCommand(query, options) {
121
143
  console.error('Usage: codragraph cypher <cypher_query>');
122
144
  process.exit(1);
123
145
  }
124
- const backend = await getBackend();
125
- const result = await backend.callTool('cypher', {
146
+ const repo = await resolveCliRepoParam(options?.repo);
147
+ if (!repo)
148
+ return;
149
+ const result = await callToolOnce('cypher', {
126
150
  query,
127
- repo: options?.repo,
151
+ repo,
128
152
  });
129
153
  output(result);
130
154
  }
131
155
  export async function featureClustersCommand(options) {
132
- const backend = await getBackend();
133
- const result = await backend.callTool('feature_clusters', {
134
- repo: options?.repo,
156
+ const repo = await resolveCliRepoParam(options?.repo);
157
+ if (!repo)
158
+ return;
159
+ const result = await callToolOnce('feature_clusters', {
160
+ repo,
135
161
  limit: options?.limit ? parseInt(options.limit, 10) : undefined,
136
162
  });
137
163
  output(result);
138
164
  }
139
165
  export async function clusterQueryCommand(query, options) {
140
- const backend = await getBackend();
141
- const result = await backend.callTool('cluster_query', {
166
+ const repo = await resolveCliRepoParam(options?.repo);
167
+ if (!repo)
168
+ return;
169
+ const result = await callToolOnce('cluster_query', {
142
170
  query,
143
- repo: options?.repo,
171
+ repo,
144
172
  limit: options?.limit ? parseInt(options.limit, 10) : undefined,
145
173
  });
146
174
  output(result);
@@ -150,10 +178,12 @@ export async function featureContextCommand(name, options) {
150
178
  console.error('Usage: codragraph feature-context <name>');
151
179
  process.exit(1);
152
180
  }
153
- const backend = await getBackend();
154
- const result = await backend.callTool('feature_context', {
181
+ const repo = await resolveCliRepoParam(options?.repo);
182
+ if (!repo)
183
+ return;
184
+ const result = await callToolOnce('feature_context', {
155
185
  name,
156
- repo: options?.repo,
186
+ repo,
157
187
  limit: options?.limit ? parseInt(options.limit, 10) : undefined,
158
188
  });
159
189
  output(result);
@@ -164,10 +194,12 @@ export async function clusterContextCommand(name, options) {
164
194
  console.error('Usage: codragraph cluster-context <name>');
165
195
  process.exit(1);
166
196
  }
167
- const backend = await getBackend();
168
- const result = await backend.callTool('cluster_context', {
197
+ const repo = await resolveCliRepoParam(options?.repo);
198
+ if (!repo)
199
+ return;
200
+ const result = await callToolOnce('cluster_context', {
169
201
  name,
170
- repo: options?.repo,
202
+ repo,
171
203
  limit: options?.limit ? parseInt(options.limit, 10) : undefined,
172
204
  });
173
205
  output(result);
@@ -178,10 +210,12 @@ export async function contextPackCommand(name, options) {
178
210
  console.error('Usage: codragraph context-pack <name>');
179
211
  process.exit(1);
180
212
  }
181
- const backend = await getBackend();
182
- const result = await backend.callTool('context_pack', {
213
+ const repo = await resolveCliRepoParam(options?.repo);
214
+ if (!repo)
215
+ return;
216
+ const result = await callToolOnce('context_pack', {
183
217
  name,
184
- repo: options?.repo,
218
+ repo,
185
219
  limit: options?.limit ? parseInt(options.limit, 10) : undefined,
186
220
  });
187
221
  output(result);
@@ -192,11 +226,13 @@ export async function clusterImpactCommand(name, options) {
192
226
  console.error('Usage: codragraph cluster-impact <name>');
193
227
  process.exit(1);
194
228
  }
195
- const backend = await getBackend();
196
- const result = await backend.callTool('cluster_impact', {
229
+ const repo = await resolveCliRepoParam(options?.repo);
230
+ if (!repo)
231
+ return;
232
+ const result = await callToolOnce('cluster_impact', {
197
233
  name,
198
234
  direction: options?.direction,
199
- repo: options?.repo,
235
+ repo,
200
236
  limit: options?.limit ? parseInt(options.limit, 10) : undefined,
201
237
  });
202
238
  output(result);
@@ -236,11 +272,15 @@ function formatDetectChangesResult(result) {
236
272
  return lines.join('\n').trim();
237
273
  }
238
274
  export async function detectChangesCommand(options) {
239
- const backend = await getBackend();
240
- const result = await backend.callTool('detect_changes', {
275
+ const repo = await resolveCliRepoParam(options?.repo);
276
+ if (!repo)
277
+ return;
278
+ const result = await callToolOnce('detect_changes', {
241
279
  scope: options?.scope || 'unstaged',
242
280
  base_ref: options?.baseRef,
243
- repo: options?.repo,
281
+ repo,
244
282
  });
245
283
  output(formatDetectChangesResult(result));
284
+ if (result?.error)
285
+ process.exitCode = 1;
246
286
  }
@@ -4,6 +4,7 @@ import nodePath from 'path';
4
4
  const DEFAULT_IGNORE_LIST = new Set([
5
5
  // Version Control
6
6
  '.git',
7
+ '.codragraph',
7
8
  '.svn',
8
9
  '.hg',
9
10
  '.bzr',
@@ -212,6 +213,8 @@ const IGNORED_EXTENSIONS = new Set([
212
213
  // Files to ignore by exact name
213
214
  const IGNORED_FILES = new Set([
214
215
  'package-lock.json',
216
+ 'bun.lock',
217
+ 'bun.lockb',
215
218
  'yarn.lock',
216
219
  'pnpm-lock.yaml',
217
220
  'composer.lock',
@@ -16,7 +16,49 @@
16
16
  */
17
17
  import fs from 'fs/promises';
18
18
  import cgdb from '@ladybugdb/core';
19
+ import { NODE_TABLES } from './schema.js';
19
20
  const pool = new Map();
21
+ const SIMPLE_LABELLESS_MATCH_RE = /^MATCH\s*\(\s*([A-Za-z_][A-Za-z0-9_]*)\s*\)/i;
22
+ const CYPHER_LIMIT_RE = /\bLIMIT\s+(\d+)\s*;?\s*$/i;
23
+ const CYPHER_RELATION_RE = /--|-\[|\]-|->|<-/;
24
+ const NATIVE_UNSAFE_NODE_LABELS = new Set(['Union']);
25
+ function quoteKnownNodeLabels(query) {
26
+ return query;
27
+ }
28
+ function getNativeUnsafeNodeLabel(query) {
29
+ const labelRe = /\(\s*[A-Za-z_][A-Za-z0-9_]*\s*:\s*`?([A-Za-z_][A-Za-z0-9_]*)`?(?=[\s){])/g;
30
+ for (const match of query.matchAll(labelRe)) {
31
+ const label = match[1];
32
+ if (NATIVE_UNSAFE_NODE_LABELS.has(label))
33
+ return label;
34
+ }
35
+ return null;
36
+ }
37
+ function getSimpleLabellessNodeAlias(query) {
38
+ const trimmed = query.trim();
39
+ const match = SIMPLE_LABELLESS_MATCH_RE.exec(trimmed);
40
+ if (!match)
41
+ return null;
42
+ if (CYPHER_RELATION_RE.test(trimmed))
43
+ return null;
44
+ if (/\bMATCH\b/i.test(trimmed.slice(match[0].length)))
45
+ return null;
46
+ return match[1];
47
+ }
48
+ function getCypherLimit(query) {
49
+ const match = CYPHER_LIMIT_RE.exec(query);
50
+ if (!match)
51
+ return null;
52
+ const limit = Number.parseInt(match[1], 10);
53
+ return Number.isFinite(limit) && limit > 0 ? limit : null;
54
+ }
55
+ function withCypherLimit(query, limit) {
56
+ const safeLimit = Math.max(1, Math.trunc(limit));
57
+ if (CYPHER_LIMIT_RE.test(query)) {
58
+ return query.replace(CYPHER_LIMIT_RE, `LIMIT ${safeLimit}`);
59
+ }
60
+ return `${query.replace(/;\s*$/, '')} LIMIT ${safeLimit}`;
61
+ }
20
62
  const poolCloseListeners = new Set();
21
63
  /**
22
64
  * Subscribe to pool-close events. Returns a disposer that removes the
@@ -463,15 +505,7 @@ function withTimeout(promise, ms, label) {
463
505
  });
464
506
  return Promise.race([promise, timeout]).finally(() => clearTimeout(timer));
465
507
  }
466
- export const executeQuery = async (repoId, cypher) => {
467
- const entry = pool.get(repoId);
468
- if (!entry) {
469
- throw new Error(`LadybugDB not initialized for repo "${repoId}". Call initCgdb first.`);
470
- }
471
- if (isWriteQuery(cypher)) {
472
- throw new Error('Write operations are not allowed. The pool adapter is read-only.');
473
- }
474
- entry.lastUsed = Date.now();
508
+ async function runQueryOnEntry(entry, cypher) {
475
509
  const conn = await checkout(entry);
476
510
  silenceStdout();
477
511
  activeQueryCount++;
@@ -486,17 +520,8 @@ export const executeQuery = async (repoId, cypher) => {
486
520
  restoreStdout();
487
521
  checkin(entry, conn);
488
522
  }
489
- };
490
- /**
491
- * Execute a parameterized query on a specific repo's connection pool.
492
- * Uses prepare/execute pattern to prevent Cypher injection.
493
- */
494
- export const executeParameterized = async (repoId, cypher, params) => {
495
- const entry = pool.get(repoId);
496
- if (!entry) {
497
- throw new Error(`LadybugDB not initialized for repo "${repoId}". Call initCgdb first.`);
498
- }
499
- entry.lastUsed = Date.now();
523
+ }
524
+ async function runParameterizedOnEntry(entry, cypher, params) {
500
525
  const conn = await checkout(entry);
501
526
  silenceStdout();
502
527
  activeQueryCount++;
@@ -516,6 +541,91 @@ export const executeParameterized = async (repoId, cypher, params) => {
516
541
  restoreStdout();
517
542
  checkin(entry, conn);
518
543
  }
544
+ }
545
+ async function runLabellessNodeScan(query, alias, runner) {
546
+ const limit = getCypherLimit(query) ?? 100;
547
+ const rows = [];
548
+ let lastError = null;
549
+ for (const label of NODE_TABLES) {
550
+ if (NATIVE_UNSAFE_NODE_LABELS.has(label))
551
+ continue;
552
+ if (rows.length >= limit)
553
+ break;
554
+ const labelQuery = withCypherLimit(query.replace(SIMPLE_LABELLESS_MATCH_RE, `MATCH (${alias}:\`${label}\`)`), limit - rows.length);
555
+ try {
556
+ const labelRows = await runner(labelQuery);
557
+ rows.push(...decorateLabellessRows(labelRows, label));
558
+ }
559
+ catch (err) {
560
+ const error = err instanceof Error ? err : new Error(String(err));
561
+ if (!isBenignLabelScanError(error)) {
562
+ lastError = error;
563
+ }
564
+ }
565
+ }
566
+ if (rows.length === 0 && lastError) {
567
+ throw lastError;
568
+ }
569
+ return rows.slice(0, limit);
570
+ }
571
+ function decorateLabellessRows(rows, label) {
572
+ return rows.map((row) => {
573
+ if (!row || typeof row !== 'object' || Array.isArray(row))
574
+ return row;
575
+ const decorated = { ...row, __cgLabel: label };
576
+ for (const key of ['type', 'kind', 'label']) {
577
+ const value = decorated[key];
578
+ if (value === '' || value === null || value === undefined) {
579
+ decorated[key] = label;
580
+ }
581
+ }
582
+ return decorated;
583
+ });
584
+ }
585
+ function isBenignLabelScanError(error) {
586
+ const message = error.message.toLowerCase();
587
+ return (message.includes('cannot find property') ||
588
+ message.includes('does not have property') ||
589
+ message.includes('property') && message.includes('not found'));
590
+ }
591
+ export const executeQuery = async (repoId, cypher) => {
592
+ const entry = pool.get(repoId);
593
+ if (!entry) {
594
+ throw new Error(`LadybugDB not initialized for repo "${repoId}". Call initCgdb first.`);
595
+ }
596
+ const safeCypher = quoteKnownNodeLabels(cypher);
597
+ if (isWriteQuery(safeCypher)) {
598
+ throw new Error('Write operations are not allowed. The pool adapter is read-only.');
599
+ }
600
+ if (getNativeUnsafeNodeLabel(safeCypher)) {
601
+ return [];
602
+ }
603
+ entry.lastUsed = Date.now();
604
+ const labellessAlias = getSimpleLabellessNodeAlias(safeCypher);
605
+ if (labellessAlias) {
606
+ return runLabellessNodeScan(safeCypher, labellessAlias, (labelQuery) => runQueryOnEntry(entry, labelQuery));
607
+ }
608
+ return runQueryOnEntry(entry, safeCypher);
609
+ };
610
+ /**
611
+ * Execute a parameterized query on a specific repo's connection pool.
612
+ * Uses prepare/execute pattern to prevent Cypher injection.
613
+ */
614
+ export const executeParameterized = async (repoId, cypher, params) => {
615
+ const entry = pool.get(repoId);
616
+ if (!entry) {
617
+ throw new Error(`LadybugDB not initialized for repo "${repoId}". Call initCgdb first.`);
618
+ }
619
+ entry.lastUsed = Date.now();
620
+ const safeCypher = quoteKnownNodeLabels(cypher);
621
+ if (getNativeUnsafeNodeLabel(safeCypher)) {
622
+ return [];
623
+ }
624
+ const labellessAlias = getSimpleLabellessNodeAlias(safeCypher);
625
+ if (labellessAlias) {
626
+ return runLabellessNodeScan(safeCypher, labellessAlias, (labelQuery) => runParameterizedOnEntry(entry, labelQuery, params));
627
+ }
628
+ return runParameterizedOnEntry(entry, safeCypher, params);
519
629
  };
520
630
  /**
521
631
  * Close one or all repo pools.
@@ -26,7 +26,7 @@ export const createCgdbRowSource = (opts = {}) => {
26
26
  // `core/search/bm25-index.ts` for FTS results. Tables that do not
27
27
  // exist on disk for a given repo throw here — we treat that as
28
28
  // "no rows" via the onSkip callback rather than a hard failure.
29
- rows = await executeQuery(`MATCH (n:${tableName}) RETURN n`);
29
+ rows = await executeQuery(`MATCH (n:${quoteCypherIdentifier(tableName)}) RETURN n`);
30
30
  }
31
31
  catch (err) {
32
32
  onSkip(tableName, err);
@@ -55,7 +55,7 @@ export const createCgdbRowSource = (opts = {}) => {
55
55
  // `rel`. Scalars give us a deterministic edge id even if the rel
56
56
  // payload's shape changes; `rel` carries any extra properties for
57
57
  // hashing.
58
- rows = await executeQuery(`MATCH (a)-[r:${REL_TABLE_NAME}]->(b) RETURN a.id AS \`from\`, b.id AS \`to\`, r.type AS type, r AS rel`);
58
+ rows = await executeQuery(`MATCH (a)-[r:${quoteCypherIdentifier(REL_TABLE_NAME)}]->(b) RETURN a.id AS \`from\`, b.id AS \`to\`, r.type AS type, r AS rel`);
59
59
  }
60
60
  catch (err) {
61
61
  onSkip(REL_TABLE_NAME, err);
@@ -100,6 +100,7 @@ const pickField = (row, named, positional) => {
100
100
  return row[named] ?? row[positional];
101
101
  };
102
102
  const isPlainObject = (v) => typeof v === 'object' && v !== null && !Array.isArray(v);
103
+ const quoteCypherIdentifier = (identifier) => `\`${identifier.replace(/`/g, '``')}\``;
103
104
  /**
104
105
  * Sanitize a node row for canonical hashing:
105
106
  * - Drop LadybugDB-specific internal fields (`_id`, `_label`) that are