@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.
- package/README.md +36 -9
- package/dist/cli/ai-context.js +298 -1
- package/dist/cli/analyze.js +19 -2
- package/dist/cli/index.js +33 -12
- package/dist/cli/serve.d.ts +1 -0
- package/dist/cli/serve.js +3 -1
- package/dist/cli/setup.js +36 -19
- package/dist/cli/status.d.ts +13 -0
- package/dist/cli/status.js +99 -0
- package/dist/cli/tool.js +73 -33
- package/dist/config/ignore-service.js +3 -0
- package/dist/core/cgdb/pool-adapter.js +130 -20
- package/dist/core/graphstore/cgdb-row-source.js +3 -2
- package/dist/core/group/bridge-db.js +42 -10
- package/dist/core/ingestion/parsing-processor.js +7 -1
- package/dist/core/ingestion/pipeline-phases/parse-impl.js +4 -0
- package/dist/core/ingestion/workers/parse-worker.js +1 -1
- package/dist/core/ingestion/workers/worker-pool.d.ts +14 -1
- package/dist/core/ingestion/workers/worker-pool.js +33 -17
- package/dist/core/run-analyze.d.ts +20 -0
- package/dist/core/run-analyze.js +225 -1
- package/dist/core/search/bm25-index.d.ts +0 -11
- package/dist/core/search/bm25-index.js +7 -84
- package/dist/core/search/hybrid-search.js +11 -3
- package/dist/mcp/local/local-backend.d.ts +2 -0
- package/dist/mcp/local/local-backend.js +235 -18
- package/dist/mcp/resources.js +2 -2
- package/dist/server/api.d.ts +14 -2
- package/dist/server/api.js +90 -7
- package/dist/server/mcp-http.d.ts +22 -0
- package/dist/server/mcp-http.js +21 -2
- package/dist/server/web-dashboard.d.ts +28 -0
- package/dist/server/web-dashboard.js +61 -0
- package/dist/web/assets/agent-D5lb0zXz.js +1089 -0
- package/dist/web/assets/architectureDiagram-EMZXCZ2Q-CZtc99v_.js +36 -0
- package/dist/web/assets/blockDiagram-IGV67L2C-BtoUp-6Y.js +132 -0
- package/dist/web/assets/c4Diagram-DFAF54RM-C4Hl3J2U.js +10 -0
- package/dist/web/assets/chunk-3GS5O3IE-DkUjU0WD.js +231 -0
- package/dist/web/assets/chunk-3YCYZ6SJ-CQkVgT_z.js +1 -0
- package/dist/web/assets/chunk-7RZVMHOQ-BitYcNVR.js +338 -0
- package/dist/web/assets/chunk-AEOMTBSW-BgTIXPsY.js +1 -0
- package/dist/web/assets/chunk-H3VCZNTA-Cx5XV_aC.js +13 -0
- package/dist/web/assets/chunk-HN6EAY2L-BBnyTNdB.js +1 -0
- package/dist/web/assets/chunk-KSICW3F5-BYzvDLNI.js +15 -0
- package/dist/web/assets/chunk-O5ABG6QK-dHwHzA6n.js +1 -0
- package/dist/web/assets/chunk-PK6DOVAG-CvsEnugt.js +206 -0
- package/dist/web/assets/chunk-RWUO3TPN-BgRTY0_k.js +1 -0
- package/dist/web/assets/chunk-TBF5ZNIQ-DL5stGM1.js +1 -0
- package/dist/web/assets/chunk-TU3PZOEN-RLyvLcv-.js +1 -0
- package/dist/web/assets/classDiagram-PPOCWD7C-DTr8QIOf.js +1 -0
- package/dist/web/assets/classDiagram-v2-23LJLIIU-DTr8QIOf.js +1 -0
- package/dist/web/assets/context-builder-22jU3V56.js +16 -0
- package/dist/web/assets/cose-bilkent-PNC4W37J-DVhePRYg.js +1 -0
- package/dist/web/assets/dagre-E77IOHMT-Dzx0A6ZU.js +4 -0
- package/dist/web/assets/diagram-H7BISOXX-CC9pRew1.js +43 -0
- package/dist/web/assets/diagram-JC5VWROH-Bau_i9tf.js +24 -0
- package/dist/web/assets/diagram-LXUTUG65-D9_FM2Gt.js +10 -0
- package/dist/web/assets/diagram-WEHSV5V5-BMlayouL.js +24 -0
- package/dist/web/assets/erDiagram-GCSMX5X6-C3dhDFA8.js +85 -0
- package/dist/web/assets/flowDiagram-OTCZ4VVT-CWSFWmhr.js +162 -0
- package/dist/web/assets/ganttDiagram-MUNLMDZQ-D3a67Yol.js +292 -0
- package/dist/web/assets/gitGraphDiagram-3HKGZ4G3-7jmry-vM.js +106 -0
- package/dist/web/assets/index-BgeqpYgd.js +1415 -0
- package/dist/web/assets/index-CT0GtFLZ.css +1 -0
- package/dist/web/assets/infoDiagram-MN7RKWGX-G7lhP0Ib.js +2 -0
- package/dist/web/assets/ishikawaDiagram-YMYX4NHK-DUoJvNP2.js +70 -0
- package/dist/web/assets/journeyDiagram-SO5T7YLQ-RMFPNNqz.js +139 -0
- package/dist/web/assets/kanban-definition-LJHFXRCJ-BzpDs1K9.js +89 -0
- package/dist/web/assets/katex-GD7MH7QM-DBQvrix-.js +261 -0
- package/dist/web/assets/mindmap-definition-2EUWGEK5-Bk0O4roa.js +96 -0
- package/dist/web/assets/pieDiagram-3IATQBI2-DKU7kpgS.js +30 -0
- package/dist/web/assets/quadrantDiagram-E256RVCF-BY0TGWCS.js +7 -0
- package/dist/web/assets/requirementDiagram-M5DCFWZL-DLHOVTSv.js +84 -0
- package/dist/web/assets/sankeyDiagram-L3NBLAOT-DVMj5rX2.js +10 -0
- package/dist/web/assets/sequenceDiagram-ZOUHS735-CJC73bV-.js +157 -0
- package/dist/web/assets/stateDiagram-MLPALWAM-BCFyESls.js +1 -0
- package/dist/web/assets/stateDiagram-v2-B5LQ5ZB2-DahzzIca.js +1 -0
- package/dist/web/assets/timeline-definition-5SPVSISX-TRSDRgPw.js +120 -0
- package/dist/web/assets/vennDiagram-IE5QUKF5-DNy7HRBM.js +34 -0
- package/dist/web/assets/wardley-RL74JXVD-BCRCBASE-B-eZEzf9.js +161 -0
- package/dist/web/assets/wardleyDiagram-XU3VSMPF-BP-r1xzR.js +20 -0
- package/dist/web/assets/xychartDiagram-ZHJ5623Y-Dr9r7a35.js +7 -0
- package/dist/web/codragraph-logo-512.png +0 -0
- package/dist/web/codragraph-logo.png +0 -0
- package/dist/web/favicon.png +0 -0
- package/dist/web/index.html +36 -0
- package/hooks/claude/codragraph-hook.cjs +18 -110
- package/hooks/claude/pre-tool-use.sh +6 -1
- package/package.json +3 -1
- package/scripts/build.js +62 -4
- package/scripts/patch-tree-sitter-swift.cjs +0 -1
- package/skills/codragraph-cli.md +1 -1
- package/vendor/leiden/index.cjs +272 -285
- package/vendor/leiden/utils.cjs +264 -274
- package/dist/_shared/lbug/schema-constants.d.ts +0 -16
- package/dist/_shared/lbug/schema-constants.d.ts.map +0 -1
- package/dist/_shared/lbug/schema-constants.js +0 -67
- package/dist/_shared/lbug/schema-constants.js.map +0 -1
- package/dist/core/graphstore/lbug-row-source.d.ts +0 -19
- package/dist/core/graphstore/lbug-row-source.js +0 -141
- package/dist/core/lbug/content-read.d.ts +0 -46
- package/dist/core/lbug/content-read.js +0 -64
- package/dist/core/lbug/csv-generator.d.ts +0 -29
- package/dist/core/lbug/csv-generator.js +0 -492
- package/dist/core/lbug/lbug-adapter.d.ts +0 -176
- package/dist/core/lbug/lbug-adapter.js +0 -1320
- package/dist/core/lbug/pool-adapter.d.ts +0 -93
- package/dist/core/lbug/pool-adapter.js +0 -550
- package/dist/core/lbug/schema.d.ts +0 -62
- package/dist/core/lbug/schema.js +0 -502
- package/dist/mcp/core/lbug-adapter.d.ts +0 -5
- 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
|
|
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
|
|
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
|
|
70
|
-
*
|
|
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
|
|
80
|
-
const
|
|
81
|
-
|
|
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 {
|
|
90
|
+
return {
|
|
91
|
+
command: 'cmd',
|
|
92
|
+
args: ['/c', 'bunx', CLI_PACKAGE_SPEC, 'mcp'],
|
|
93
|
+
};
|
|
84
94
|
}
|
|
85
|
-
return {
|
|
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
|
-
|
|
112
|
-
|
|
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.
|
package/dist/cli/status.d.ts
CHANGED
|
@@ -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>;
|
package/dist/cli/status.js
CHANGED
|
@@ -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
|
|
63
|
-
|
|
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
|
|
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
|
|
80
|
-
|
|
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
|
|
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
|
|
97
|
-
|
|
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
|
|
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
|
|
125
|
-
|
|
146
|
+
const repo = await resolveCliRepoParam(options?.repo);
|
|
147
|
+
if (!repo)
|
|
148
|
+
return;
|
|
149
|
+
const result = await callToolOnce('cypher', {
|
|
126
150
|
query,
|
|
127
|
-
repo
|
|
151
|
+
repo,
|
|
128
152
|
});
|
|
129
153
|
output(result);
|
|
130
154
|
}
|
|
131
155
|
export async function featureClustersCommand(options) {
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
|
|
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
|
|
141
|
-
|
|
166
|
+
const repo = await resolveCliRepoParam(options?.repo);
|
|
167
|
+
if (!repo)
|
|
168
|
+
return;
|
|
169
|
+
const result = await callToolOnce('cluster_query', {
|
|
142
170
|
query,
|
|
143
|
-
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
|
|
154
|
-
|
|
181
|
+
const repo = await resolveCliRepoParam(options?.repo);
|
|
182
|
+
if (!repo)
|
|
183
|
+
return;
|
|
184
|
+
const result = await callToolOnce('feature_context', {
|
|
155
185
|
name,
|
|
156
|
-
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
|
|
168
|
-
|
|
197
|
+
const repo = await resolveCliRepoParam(options?.repo);
|
|
198
|
+
if (!repo)
|
|
199
|
+
return;
|
|
200
|
+
const result = await callToolOnce('cluster_context', {
|
|
169
201
|
name,
|
|
170
|
-
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
|
|
182
|
-
|
|
213
|
+
const repo = await resolveCliRepoParam(options?.repo);
|
|
214
|
+
if (!repo)
|
|
215
|
+
return;
|
|
216
|
+
const result = await callToolOnce('context_pack', {
|
|
183
217
|
name,
|
|
184
|
-
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
|
|
196
|
-
|
|
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
|
|
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
|
|
240
|
-
|
|
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
|
|
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
|
-
|
|
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
|