@grafema/cli 0.1.1-alpha

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 (66) hide show
  1. package/LICENSE +190 -0
  2. package/dist/cli.d.ts +6 -0
  3. package/dist/cli.d.ts.map +1 -0
  4. package/dist/cli.js +36 -0
  5. package/dist/commands/analyze.d.ts +6 -0
  6. package/dist/commands/analyze.d.ts.map +1 -0
  7. package/dist/commands/analyze.js +209 -0
  8. package/dist/commands/check.d.ts +10 -0
  9. package/dist/commands/check.d.ts.map +1 -0
  10. package/dist/commands/check.js +295 -0
  11. package/dist/commands/coverage.d.ts +11 -0
  12. package/dist/commands/coverage.d.ts.map +1 -0
  13. package/dist/commands/coverage.js +96 -0
  14. package/dist/commands/explore.d.ts +6 -0
  15. package/dist/commands/explore.d.ts.map +1 -0
  16. package/dist/commands/explore.js +633 -0
  17. package/dist/commands/get.d.ts +10 -0
  18. package/dist/commands/get.d.ts.map +1 -0
  19. package/dist/commands/get.js +189 -0
  20. package/dist/commands/impact.d.ts +10 -0
  21. package/dist/commands/impact.d.ts.map +1 -0
  22. package/dist/commands/impact.js +313 -0
  23. package/dist/commands/init.d.ts +6 -0
  24. package/dist/commands/init.d.ts.map +1 -0
  25. package/dist/commands/init.js +94 -0
  26. package/dist/commands/overview.d.ts +6 -0
  27. package/dist/commands/overview.d.ts.map +1 -0
  28. package/dist/commands/overview.js +91 -0
  29. package/dist/commands/query.d.ts +13 -0
  30. package/dist/commands/query.d.ts.map +1 -0
  31. package/dist/commands/query.js +340 -0
  32. package/dist/commands/server.d.ts +11 -0
  33. package/dist/commands/server.d.ts.map +1 -0
  34. package/dist/commands/server.js +300 -0
  35. package/dist/commands/stats.d.ts +6 -0
  36. package/dist/commands/stats.d.ts.map +1 -0
  37. package/dist/commands/stats.js +52 -0
  38. package/dist/commands/trace.d.ts +10 -0
  39. package/dist/commands/trace.d.ts.map +1 -0
  40. package/dist/commands/trace.js +270 -0
  41. package/dist/utils/codePreview.d.ts +28 -0
  42. package/dist/utils/codePreview.d.ts.map +1 -0
  43. package/dist/utils/codePreview.js +51 -0
  44. package/dist/utils/errorFormatter.d.ts +24 -0
  45. package/dist/utils/errorFormatter.d.ts.map +1 -0
  46. package/dist/utils/errorFormatter.js +32 -0
  47. package/dist/utils/formatNode.d.ts +53 -0
  48. package/dist/utils/formatNode.d.ts.map +1 -0
  49. package/dist/utils/formatNode.js +49 -0
  50. package/package.json +54 -0
  51. package/src/cli.ts +41 -0
  52. package/src/commands/analyze.ts +271 -0
  53. package/src/commands/check.ts +379 -0
  54. package/src/commands/coverage.ts +108 -0
  55. package/src/commands/explore.tsx +1056 -0
  56. package/src/commands/get.ts +265 -0
  57. package/src/commands/impact.ts +400 -0
  58. package/src/commands/init.ts +112 -0
  59. package/src/commands/overview.ts +108 -0
  60. package/src/commands/query.ts +425 -0
  61. package/src/commands/server.ts +335 -0
  62. package/src/commands/stats.ts +58 -0
  63. package/src/commands/trace.ts +341 -0
  64. package/src/utils/codePreview.ts +77 -0
  65. package/src/utils/errorFormatter.ts +35 -0
  66. package/src/utils/formatNode.ts +88 -0
@@ -0,0 +1,112 @@
1
+ /**
2
+ * Init command - Initialize Grafema in a project
3
+ */
4
+
5
+ import { Command } from 'commander';
6
+ import { resolve, join } from 'path';
7
+ import { existsSync, mkdirSync, writeFileSync, readFileSync } from 'fs';
8
+ import { exitWithError } from '../utils/errorFormatter.js';
9
+ import { stringify as stringifyYAML } from 'yaml';
10
+ import { DEFAULT_CONFIG } from '@grafema/core';
11
+
12
+ /**
13
+ * Generate config.yaml content with commented future features.
14
+ * Only includes implemented features (plugins).
15
+ */
16
+ function generateConfigYAML(): string {
17
+ // Start with working default config
18
+ const config = {
19
+ // Plugin list (fully implemented)
20
+ plugins: DEFAULT_CONFIG.plugins,
21
+ };
22
+
23
+ // Convert to YAML
24
+ const yaml = stringifyYAML(config, {
25
+ lineWidth: 0, // Don't wrap long lines
26
+ });
27
+
28
+ // Add header comment
29
+ return `# Grafema Configuration
30
+ # Documentation: https://github.com/grafema/grafema#configuration
31
+
32
+ ${yaml}
33
+ # Future: File discovery patterns (not yet implemented)
34
+ # Grafema currently uses entrypoint-based discovery (follows imports from package.json main field)
35
+ # Glob-based include/exclude patterns will be added in a future release
36
+ #
37
+ # include:
38
+ # - "src/**/*.{ts,js,tsx,jsx}"
39
+ # exclude:
40
+ # - "**/*.test.ts"
41
+ # - "node_modules/**"
42
+ `;
43
+ }
44
+
45
+ interface InitOptions {
46
+ force?: boolean;
47
+ }
48
+
49
+ export const initCommand = new Command('init')
50
+ .description('Initialize Grafema in current project')
51
+ .argument('[path]', 'Project path', '.')
52
+ .option('-f, --force', 'Overwrite existing config')
53
+ .action(async (path: string, options: InitOptions) => {
54
+ const projectPath = resolve(path);
55
+ const grafemaDir = join(projectPath, '.grafema');
56
+ const configPath = join(grafemaDir, 'config.yaml');
57
+ const packageJsonPath = join(projectPath, 'package.json');
58
+ const tsconfigPath = join(projectPath, 'tsconfig.json');
59
+
60
+ // Check package.json
61
+ if (!existsSync(packageJsonPath)) {
62
+ exitWithError('No package.json found', [
63
+ 'Initialize a project: npm init',
64
+ 'Or check you are in the right directory'
65
+ ]);
66
+ }
67
+ console.log('✓ Found package.json');
68
+
69
+ // Detect TypeScript
70
+ const isTypeScript = existsSync(tsconfigPath);
71
+ if (isTypeScript) {
72
+ console.log('✓ Detected TypeScript project');
73
+ } else {
74
+ console.log('✓ Detected JavaScript project');
75
+ }
76
+
77
+ // Check existing config
78
+ if (existsSync(configPath) && !options.force) {
79
+ console.log('');
80
+ console.log('✓ Grafema already initialized');
81
+ console.log(' → Use --force to overwrite config');
82
+ console.log('');
83
+ console.log('Next: Run "grafema analyze" to build the code graph');
84
+ return;
85
+ }
86
+
87
+ // Create .grafema directory
88
+ if (!existsSync(grafemaDir)) {
89
+ mkdirSync(grafemaDir, { recursive: true });
90
+ }
91
+
92
+ // Write config
93
+ const configContent = generateConfigYAML();
94
+ writeFileSync(configPath, configContent);
95
+ console.log('✓ Created .grafema/config.yaml');
96
+
97
+ // Add to .gitignore if exists
98
+ const gitignorePath = join(projectPath, '.gitignore');
99
+ if (existsSync(gitignorePath)) {
100
+ const gitignore = readFileSync(gitignorePath, 'utf-8');
101
+ if (!gitignore.includes('.grafema/graph.rfdb')) {
102
+ writeFileSync(
103
+ gitignorePath,
104
+ gitignore + '\n# Grafema\n.grafema/graph.rfdb\n.grafema/rfdb.sock\n'
105
+ );
106
+ console.log('✓ Updated .gitignore');
107
+ }
108
+ }
109
+
110
+ console.log('');
111
+ console.log('Next: Run "grafema analyze" to build the code graph');
112
+ });
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Overview command - Project dashboard
3
+ */
4
+
5
+ import { Command } from 'commander';
6
+ import { resolve, join } from 'path';
7
+ import { existsSync } from 'fs';
8
+ import { RFDBServerBackend } from '@grafema/core';
9
+ import { exitWithError } from '../utils/errorFormatter.js';
10
+
11
+ interface NodeStats {
12
+ type: string;
13
+ count: number;
14
+ }
15
+
16
+ export const overviewCommand = new Command('overview')
17
+ .description('Show project overview and statistics')
18
+ .option('-p, --project <path>', 'Project path', '.')
19
+ .option('-j, --json', 'Output as JSON')
20
+ .action(async (options: { project: string; json?: boolean }) => {
21
+ const projectPath = resolve(options.project);
22
+ const grafemaDir = join(projectPath, '.grafema');
23
+ const dbPath = join(grafemaDir, 'graph.rfdb');
24
+
25
+ if (!existsSync(dbPath)) {
26
+ exitWithError('No graph database found', ['Run: grafema analyze']);
27
+ }
28
+
29
+ const backend = new RFDBServerBackend({ dbPath });
30
+ await backend.connect();
31
+
32
+ try {
33
+ const stats = await backend.getStats();
34
+
35
+ if (options.json) {
36
+ console.log(JSON.stringify(stats, null, 2));
37
+ return;
38
+ }
39
+
40
+ // Header
41
+ console.log('');
42
+ console.log('📊 Project Overview');
43
+ console.log('');
44
+
45
+ // Code Structure
46
+ console.log('Code Structure:');
47
+ const modules = stats.nodesByType['MODULE'] || 0;
48
+ const functions = stats.nodesByType['FUNCTION'] || 0;
49
+ const classes = stats.nodesByType['CLASS'] || 0;
50
+ const variables = stats.nodesByType['VARIABLE'] || 0;
51
+ const calls = stats.nodesByType['CALL'] || 0;
52
+
53
+ console.log(`├─ Modules: ${modules}`);
54
+ console.log(`├─ Functions: ${functions}`);
55
+ console.log(`├─ Classes: ${classes}`);
56
+ console.log(`├─ Variables: ${variables}`);
57
+ console.log(`└─ Call sites: ${calls}`);
58
+ console.log('');
59
+
60
+ // External Interactions (namespaced types)
61
+ console.log('External Interactions:');
62
+ const httpRoutes = stats.nodesByType['http:route'] || 0;
63
+ const dbQueries = stats.nodesByType['db:query'] || 0;
64
+ const socketEmit = stats.nodesByType['socketio:emit'] || 0;
65
+ const socketOn = stats.nodesByType['socketio:on'] || 0;
66
+ const events = stats.nodesByType['event:listener'] || 0;
67
+
68
+ if (httpRoutes > 0) console.log(`├─ HTTP routes: ${httpRoutes}`);
69
+ if (dbQueries > 0) console.log(`├─ Database queries: ${dbQueries}`);
70
+ if (socketEmit + socketOn > 0) console.log(`├─ Socket.IO: ${socketEmit} emit, ${socketOn} listeners`);
71
+ if (events > 0) console.log(`├─ Event listeners: ${events}`);
72
+
73
+ // Check for external module refs
74
+ const externalModules = stats.nodesByType['EXTERNAL_MODULE'] || 0;
75
+ if (externalModules > 0) console.log(`└─ External modules: ${externalModules}`);
76
+
77
+ if (httpRoutes + dbQueries + socketEmit + socketOn + events + externalModules === 0) {
78
+ console.log('└─ (none detected)');
79
+ }
80
+ console.log('');
81
+
82
+ // Graph Statistics
83
+ console.log('Graph Statistics:');
84
+ console.log(`├─ Total nodes: ${stats.nodeCount}`);
85
+ console.log(`├─ Total edges: ${stats.edgeCount}`);
86
+
87
+ // Show edge breakdown
88
+ const callEdges = stats.edgesByType['CALLS'] || 0;
89
+ const containsEdges = stats.edgesByType['CONTAINS'] || 0;
90
+ const importsEdges = stats.edgesByType['IMPORTS'] || 0;
91
+
92
+ console.log(`├─ Calls: ${callEdges}`);
93
+ console.log(`├─ Contains: ${containsEdges}`);
94
+ console.log(`└─ Imports: ${importsEdges}`);
95
+ console.log('');
96
+
97
+ // Find most called functions (via incoming CALLS edges)
98
+ // This requires a query - simplified for now
99
+ console.log('Next steps:');
100
+ console.log('→ grafema query "function <name>" Search for a function');
101
+ console.log('→ grafema trace "<var> from <fn>" Trace data flow');
102
+ console.log('→ grafema impact "<target>" Analyze change impact');
103
+ console.log('→ grafema explore Interactive navigation');
104
+
105
+ } finally {
106
+ await backend.close();
107
+ }
108
+ });
@@ -0,0 +1,425 @@
1
+ /**
2
+ * Query command - Search the code graph
3
+ *
4
+ * Supports patterns like:
5
+ * grafema query "function authenticate"
6
+ * grafema query "class UserService"
7
+ * grafema query "authenticate" (searches all types)
8
+ *
9
+ * For raw Datalog queries, use --raw flag
10
+ */
11
+
12
+ import { Command } from 'commander';
13
+ import { resolve, join, relative } from 'path';
14
+ import { existsSync } from 'fs';
15
+ import { RFDBServerBackend } from '@grafema/core';
16
+ import { formatNodeDisplay, formatNodeInline } from '../utils/formatNode.js';
17
+ import { exitWithError } from '../utils/errorFormatter.js';
18
+
19
+ interface QueryOptions {
20
+ project: string;
21
+ json?: boolean;
22
+ limit: string;
23
+ raw?: boolean;
24
+ }
25
+
26
+ interface NodeInfo {
27
+ id: string;
28
+ type: string;
29
+ name: string;
30
+ file: string;
31
+ line?: number;
32
+ [key: string]: unknown;
33
+ }
34
+
35
+ export const queryCommand = new Command('query')
36
+ .description('Search the code graph')
37
+ .argument('<pattern>', 'Search pattern: "function X", "class Y", or just "X"')
38
+ .option('-p, --project <path>', 'Project path', '.')
39
+ .option('-j, --json', 'Output as JSON')
40
+ .option('-l, --limit <n>', 'Limit results', '10')
41
+ .option('--raw', 'Execute raw Datalog query')
42
+ .action(async (pattern: string, options: QueryOptions) => {
43
+ const projectPath = resolve(options.project);
44
+ const grafemaDir = join(projectPath, '.grafema');
45
+ const dbPath = join(grafemaDir, 'graph.rfdb');
46
+
47
+ if (!existsSync(dbPath)) {
48
+ exitWithError('No graph database found', ['Run: grafema analyze']);
49
+ }
50
+
51
+ const backend = new RFDBServerBackend({ dbPath });
52
+ await backend.connect();
53
+
54
+ try {
55
+ // Raw Datalog mode
56
+ if (options.raw) {
57
+ await executeRawQuery(backend, pattern, options);
58
+ return;
59
+ }
60
+
61
+ // Parse pattern
62
+ const { type, name } = parsePattern(pattern);
63
+ const limit = parseInt(options.limit, 10);
64
+
65
+ // Find matching nodes
66
+ const nodes = await findNodes(backend, type, name, limit);
67
+
68
+ if (nodes.length === 0) {
69
+ console.log(`No results for "${pattern}"`);
70
+ if (type) {
71
+ console.log(` → Try: grafema query "${name}" (search all types)`);
72
+ }
73
+ return;
74
+ }
75
+
76
+ if (options.json) {
77
+ const results = await Promise.all(
78
+ nodes.map(async (node) => ({
79
+ ...node,
80
+ calledBy: await getCallers(backend, node.id, 5),
81
+ calls: await getCallees(backend, node.id, 5),
82
+ }))
83
+ );
84
+ console.log(JSON.stringify(results, null, 2));
85
+ return;
86
+ }
87
+
88
+ // Display results
89
+ for (const node of nodes) {
90
+ console.log('');
91
+ displayNode(node, projectPath);
92
+
93
+ // Show callers and callees for functions
94
+ if (node.type === 'FUNCTION' || node.type === 'CLASS') {
95
+ const callers = await getCallers(backend, node.id, 5);
96
+ const callees = await getCallees(backend, node.id, 5);
97
+
98
+ if (callers.length > 0) {
99
+ console.log('');
100
+ console.log(`Called by (${callers.length}${callers.length >= 5 ? '+' : ''}):`);
101
+ for (const caller of callers) {
102
+ console.log(` <- ${formatNodeInline(caller)}`);
103
+ }
104
+ }
105
+
106
+ if (callees.length > 0) {
107
+ console.log('');
108
+ console.log(`Calls (${callees.length}${callees.length >= 5 ? '+' : ''}):`);
109
+ for (const callee of callees) {
110
+ console.log(` -> ${formatNodeInline(callee)}`);
111
+ }
112
+ }
113
+ }
114
+ }
115
+
116
+ if (nodes.length > 1) {
117
+ console.log('');
118
+ console.log(`Found ${nodes.length} results. Use more specific pattern to narrow.`);
119
+ }
120
+
121
+ } finally {
122
+ await backend.close();
123
+ }
124
+ });
125
+
126
+ /**
127
+ * Parse search pattern like "function authenticate" or just "authenticate"
128
+ */
129
+ function parsePattern(pattern: string): { type: string | null; name: string } {
130
+ const words = pattern.trim().split(/\s+/);
131
+
132
+ if (words.length >= 2) {
133
+ const typeWord = words[0].toLowerCase();
134
+ const name = words.slice(1).join(' ');
135
+
136
+ const typeMap: Record<string, string> = {
137
+ function: 'FUNCTION',
138
+ fn: 'FUNCTION',
139
+ func: 'FUNCTION',
140
+ class: 'CLASS',
141
+ module: 'MODULE',
142
+ variable: 'VARIABLE',
143
+ var: 'VARIABLE',
144
+ const: 'CONSTANT',
145
+ constant: 'CONSTANT',
146
+ };
147
+
148
+ if (typeMap[typeWord]) {
149
+ return { type: typeMap[typeWord], name };
150
+ }
151
+ }
152
+
153
+ return { type: null, name: pattern.trim() };
154
+ }
155
+
156
+ /**
157
+ * Find nodes by type and name
158
+ */
159
+ async function findNodes(
160
+ backend: RFDBServerBackend,
161
+ type: string | null,
162
+ name: string,
163
+ limit: number
164
+ ): Promise<NodeInfo[]> {
165
+ const results: NodeInfo[] = [];
166
+ const searchTypes = type
167
+ ? [type]
168
+ : ['FUNCTION', 'CLASS', 'MODULE', 'VARIABLE', 'CONSTANT'];
169
+
170
+ for (const nodeType of searchTypes) {
171
+ for await (const node of backend.queryNodes({ nodeType: nodeType as any })) {
172
+ const nodeName = node.name || '';
173
+ // Case-insensitive partial match
174
+ if (nodeName.toLowerCase().includes(name.toLowerCase())) {
175
+ results.push({
176
+ id: node.id,
177
+ type: node.type || nodeType,
178
+ name: nodeName,
179
+ file: node.file || '',
180
+ line: node.line,
181
+ });
182
+ if (results.length >= limit) break;
183
+ }
184
+ }
185
+ if (results.length >= limit) break;
186
+ }
187
+
188
+ return results;
189
+ }
190
+
191
+ /**
192
+ * Get functions that call this node
193
+ *
194
+ * Logic: FUNCTION ← CONTAINS ← CALL → CALLS → TARGET
195
+ * We need to find CALL nodes that CALLS this target,
196
+ * then find the FUNCTION that CONTAINS each CALL
197
+ */
198
+ async function getCallers(
199
+ backend: RFDBServerBackend,
200
+ nodeId: string,
201
+ limit: number
202
+ ): Promise<NodeInfo[]> {
203
+ const callers: NodeInfo[] = [];
204
+ const seen = new Set<string>();
205
+
206
+ try {
207
+ // Find CALL nodes that call this target
208
+ const callEdges = await backend.getIncomingEdges(nodeId, ['CALLS']);
209
+
210
+ for (const edge of callEdges) {
211
+ if (callers.length >= limit) break;
212
+
213
+ const callNode = await backend.getNode(edge.src);
214
+ if (!callNode) continue;
215
+
216
+ // Find the FUNCTION that contains this CALL
217
+ const containingFunc = await findContainingFunction(backend, callNode.id);
218
+
219
+ if (containingFunc && !seen.has(containingFunc.id)) {
220
+ seen.add(containingFunc.id);
221
+ callers.push({
222
+ id: containingFunc.id,
223
+ type: containingFunc.type || 'FUNCTION',
224
+ name: containingFunc.name || '<anonymous>',
225
+ file: containingFunc.file || '',
226
+ line: containingFunc.line,
227
+ });
228
+ }
229
+ }
230
+ } catch {
231
+ // Ignore errors
232
+ }
233
+
234
+ return callers;
235
+ }
236
+
237
+ /**
238
+ * Find the FUNCTION or CLASS that contains a node
239
+ *
240
+ * Path can be: CALL → CONTAINS → SCOPE → CONTAINS → SCOPE → HAS_SCOPE → FUNCTION
241
+ * So we need to follow both CONTAINS and HAS_SCOPE edges
242
+ */
243
+ async function findContainingFunction(
244
+ backend: RFDBServerBackend,
245
+ nodeId: string,
246
+ maxDepth: number = 15
247
+ ): Promise<NodeInfo | null> {
248
+ const visited = new Set<string>();
249
+ const queue: Array<{ id: string; depth: number }> = [{ id: nodeId, depth: 0 }];
250
+
251
+ while (queue.length > 0) {
252
+ const { id, depth } = queue.shift()!;
253
+
254
+ if (visited.has(id) || depth > maxDepth) continue;
255
+ visited.add(id);
256
+
257
+ try {
258
+ // Get incoming edges: CONTAINS, HAS_SCOPE, and DECLARES (for variables in functions)
259
+ const edges = await backend.getIncomingEdges(id, null);
260
+
261
+ for (const edge of edges) {
262
+ const edgeType = (edge as any).edgeType || (edge as any).type;
263
+
264
+ // Only follow structural edges
265
+ if (!['CONTAINS', 'HAS_SCOPE', 'DECLARES'].includes(edgeType)) continue;
266
+
267
+ const parentNode = await backend.getNode(edge.src);
268
+ if (!parentNode || visited.has(parentNode.id)) continue;
269
+
270
+ const parentType = parentNode.type;
271
+
272
+ // FUNCTION, CLASS, or MODULE (for top-level calls)
273
+ if (parentType === 'FUNCTION' || parentType === 'CLASS' || parentType === 'MODULE') {
274
+ return {
275
+ id: parentNode.id,
276
+ type: parentType,
277
+ name: parentNode.name || '<anonymous>',
278
+ file: parentNode.file || '',
279
+ line: parentNode.line,
280
+ };
281
+ }
282
+
283
+ // Continue searching from this parent
284
+ queue.push({ id: parentNode.id, depth: depth + 1 });
285
+ }
286
+ } catch {
287
+ // Ignore errors
288
+ }
289
+ }
290
+
291
+ return null;
292
+ }
293
+
294
+ /**
295
+ * Get functions that this node calls
296
+ *
297
+ * Logic: FUNCTION → CONTAINS → CALL → CALLS → TARGET
298
+ * Find all CALL nodes inside this function, then find what they call
299
+ */
300
+ async function getCallees(
301
+ backend: RFDBServerBackend,
302
+ nodeId: string,
303
+ limit: number
304
+ ): Promise<NodeInfo[]> {
305
+ const callees: NodeInfo[] = [];
306
+ const seen = new Set<string>();
307
+
308
+ try {
309
+ // Find all CALL nodes inside this function (via CONTAINS)
310
+ const callNodes = await findCallsInFunction(backend, nodeId);
311
+
312
+ for (const callNode of callNodes) {
313
+ if (callees.length >= limit) break;
314
+
315
+ // Find what this CALL calls
316
+ const callEdges = await backend.getOutgoingEdges(callNode.id, ['CALLS']);
317
+
318
+ for (const edge of callEdges) {
319
+ if (callees.length >= limit) break;
320
+
321
+ const targetNode = await backend.getNode(edge.dst);
322
+ if (!targetNode || seen.has(targetNode.id)) continue;
323
+
324
+ seen.add(targetNode.id);
325
+ callees.push({
326
+ id: targetNode.id,
327
+ type: targetNode.type || 'UNKNOWN',
328
+ name: targetNode.name || '<anonymous>',
329
+ file: targetNode.file || '',
330
+ line: targetNode.line,
331
+ });
332
+ }
333
+ }
334
+ } catch {
335
+ // Ignore errors
336
+ }
337
+
338
+ return callees;
339
+ }
340
+
341
+ /**
342
+ * Find all CALL nodes inside a function (recursively via CONTAINS)
343
+ */
344
+ async function findCallsInFunction(
345
+ backend: RFDBServerBackend,
346
+ nodeId: string,
347
+ maxDepth: number = 10
348
+ ): Promise<NodeInfo[]> {
349
+ const calls: NodeInfo[] = [];
350
+ const visited = new Set<string>();
351
+ const queue: Array<{ id: string; depth: number }> = [{ id: nodeId, depth: 0 }];
352
+
353
+ while (queue.length > 0) {
354
+ const { id, depth } = queue.shift()!;
355
+
356
+ if (visited.has(id) || depth > maxDepth) continue;
357
+ visited.add(id);
358
+
359
+ try {
360
+ // Get children via CONTAINS
361
+ const edges = await backend.getOutgoingEdges(id, ['CONTAINS']);
362
+
363
+ for (const edge of edges) {
364
+ const child = await backend.getNode(edge.dst);
365
+ if (!child) continue;
366
+
367
+ const childType = child.type;
368
+
369
+ if (childType === 'CALL') {
370
+ calls.push({
371
+ id: child.id,
372
+ type: 'CALL',
373
+ name: child.name || '',
374
+ file: child.file || '',
375
+ line: child.line,
376
+ });
377
+ }
378
+
379
+ // Continue searching in children (but not into nested functions)
380
+ if (childType !== 'FUNCTION' && childType !== 'CLASS') {
381
+ queue.push({ id: child.id, depth: depth + 1 });
382
+ }
383
+ }
384
+ } catch {
385
+ // Ignore
386
+ }
387
+ }
388
+
389
+ return calls;
390
+ }
391
+
392
+ /**
393
+ * Display a node with semantic ID as primary identifier
394
+ */
395
+ function displayNode(node: NodeInfo, projectPath: string): void {
396
+ console.log(formatNodeDisplay(node, { projectPath }));
397
+ }
398
+
399
+ /**
400
+ * Execute raw Datalog query (backwards compat)
401
+ */
402
+ async function executeRawQuery(
403
+ backend: RFDBServerBackend,
404
+ query: string,
405
+ options: QueryOptions
406
+ ): Promise<void> {
407
+ const results = await backend.datalogQuery(query);
408
+ const limit = parseInt(options.limit, 10);
409
+ const limited = results.slice(0, limit);
410
+
411
+ if (options.json) {
412
+ console.log(JSON.stringify(limited, null, 2));
413
+ } else {
414
+ if (limited.length === 0) {
415
+ console.log('No results.');
416
+ } else {
417
+ console.log(`Results (${limited.length}${results.length > limit ? ` of ${results.length}` : ''}):`);
418
+ console.log('');
419
+ for (const result of limited) {
420
+ const bindings = result.bindings.map((b) => `${b.name}=${b.value}`).join(', ');
421
+ console.log(` { ${bindings} }`);
422
+ }
423
+ }
424
+ }
425
+ }