@grafema/cli 0.2.4-beta → 0.2.6-beta

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 (139) hide show
  1. package/README.md +85 -0
  2. package/dist/cli.js +7 -2
  3. package/dist/cli.js.map +1 -0
  4. package/dist/commands/analyze.d.ts +3 -1
  5. package/dist/commands/analyze.d.ts.map +1 -1
  6. package/dist/commands/analyze.js +8 -266
  7. package/dist/commands/analyze.js.map +1 -0
  8. package/dist/commands/analyzeAction.d.ts +28 -0
  9. package/dist/commands/analyzeAction.d.ts.map +1 -0
  10. package/dist/commands/analyzeAction.js +243 -0
  11. package/dist/commands/analyzeAction.js.map +1 -0
  12. package/dist/commands/check.d.ts +2 -6
  13. package/dist/commands/check.d.ts.map +1 -1
  14. package/dist/commands/check.js +34 -48
  15. package/dist/commands/check.js.map +1 -0
  16. package/dist/commands/context.d.ts +16 -0
  17. package/dist/commands/context.d.ts.map +1 -0
  18. package/dist/commands/context.js +238 -0
  19. package/dist/commands/context.js.map +1 -0
  20. package/dist/commands/coverage.js +1 -0
  21. package/dist/commands/coverage.js.map +1 -0
  22. package/dist/commands/doctor/checks.d.ts.map +1 -1
  23. package/dist/commands/doctor/checks.js +10 -6
  24. package/dist/commands/doctor/checks.js.map +1 -0
  25. package/dist/commands/doctor/output.js +1 -0
  26. package/dist/commands/doctor/output.js.map +1 -0
  27. package/dist/commands/doctor/types.js +1 -0
  28. package/dist/commands/doctor/types.js.map +1 -0
  29. package/dist/commands/doctor.js +1 -0
  30. package/dist/commands/doctor.js.map +1 -0
  31. package/dist/commands/explain.d.ts.map +1 -1
  32. package/dist/commands/explain.js +5 -3
  33. package/dist/commands/explain.js.map +1 -0
  34. package/dist/commands/explore.d.ts.map +1 -1
  35. package/dist/commands/explore.js +9 -4
  36. package/dist/commands/explore.js.map +1 -0
  37. package/dist/commands/file.d.ts +15 -0
  38. package/dist/commands/file.d.ts.map +1 -0
  39. package/dist/commands/file.js +144 -0
  40. package/dist/commands/file.js.map +1 -0
  41. package/dist/commands/get.d.ts.map +1 -1
  42. package/dist/commands/get.js +7 -0
  43. package/dist/commands/get.js.map +1 -0
  44. package/dist/commands/impact.d.ts.map +1 -1
  45. package/dist/commands/impact.js +3 -3
  46. package/dist/commands/impact.js.map +1 -0
  47. package/dist/commands/init.d.ts.map +1 -1
  48. package/dist/commands/init.js +20 -2
  49. package/dist/commands/init.js.map +1 -0
  50. package/dist/commands/ls.d.ts.map +1 -1
  51. package/dist/commands/ls.js +10 -2
  52. package/dist/commands/ls.js.map +1 -0
  53. package/dist/commands/overview.d.ts.map +1 -1
  54. package/dist/commands/overview.js +1 -0
  55. package/dist/commands/overview.js.map +1 -0
  56. package/dist/commands/query.d.ts +8 -0
  57. package/dist/commands/query.d.ts.map +1 -1
  58. package/dist/commands/query.js +217 -43
  59. package/dist/commands/query.js.map +1 -0
  60. package/dist/commands/schema.d.ts.map +1 -1
  61. package/dist/commands/schema.js +4 -2
  62. package/dist/commands/schema.js.map +1 -0
  63. package/dist/commands/server.d.ts +2 -1
  64. package/dist/commands/server.d.ts.map +1 -1
  65. package/dist/commands/server.js +76 -14
  66. package/dist/commands/server.js.map +1 -0
  67. package/dist/commands/setup-skill.d.ts +17 -0
  68. package/dist/commands/setup-skill.d.ts.map +1 -0
  69. package/dist/commands/setup-skill.js +131 -0
  70. package/dist/commands/setup-skill.js.map +1 -0
  71. package/dist/commands/stats.js +1 -0
  72. package/dist/commands/stats.js.map +1 -0
  73. package/dist/commands/trace.d.ts.map +1 -1
  74. package/dist/commands/trace.js +21 -10
  75. package/dist/commands/trace.js.map +1 -0
  76. package/dist/commands/types.js +1 -0
  77. package/dist/commands/types.js.map +1 -0
  78. package/dist/plugins/builtinPlugins.d.ts +10 -0
  79. package/dist/plugins/builtinPlugins.d.ts.map +1 -0
  80. package/dist/plugins/builtinPlugins.js +68 -0
  81. package/dist/plugins/builtinPlugins.js.map +1 -0
  82. package/dist/plugins/pluginLoader.d.ts +16 -0
  83. package/dist/plugins/pluginLoader.d.ts.map +1 -0
  84. package/dist/plugins/pluginLoader.js +101 -0
  85. package/dist/plugins/pluginLoader.js.map +1 -0
  86. package/dist/plugins/pluginResolver.js +38 -0
  87. package/dist/utils/codePreview.d.ts +1 -0
  88. package/dist/utils/codePreview.d.ts.map +1 -1
  89. package/dist/utils/codePreview.js +6 -3
  90. package/dist/utils/codePreview.js.map +1 -0
  91. package/dist/utils/errorFormatter.js +1 -0
  92. package/dist/utils/errorFormatter.js.map +1 -0
  93. package/dist/utils/formatNode.d.ts +1 -1
  94. package/dist/utils/formatNode.d.ts.map +1 -1
  95. package/dist/utils/formatNode.js +3 -2
  96. package/dist/utils/formatNode.js.map +1 -0
  97. package/dist/utils/pathUtils.d.ts +2 -0
  98. package/dist/utils/pathUtils.d.ts.map +1 -0
  99. package/dist/utils/pathUtils.js +9 -0
  100. package/dist/utils/pathUtils.js.map +1 -0
  101. package/dist/utils/progressRenderer.d.ts +119 -0
  102. package/dist/utils/progressRenderer.d.ts.map +1 -0
  103. package/dist/utils/progressRenderer.js +245 -0
  104. package/dist/utils/progressRenderer.js.map +1 -0
  105. package/dist/utils/spinner.d.ts +39 -0
  106. package/dist/utils/spinner.d.ts.map +1 -0
  107. package/dist/utils/spinner.js +84 -0
  108. package/dist/utils/spinner.js.map +1 -0
  109. package/package.json +8 -9
  110. package/skills/grafema-codebase-analysis/SKILL.md +295 -0
  111. package/skills/grafema-codebase-analysis/references/node-edge-types.md +123 -0
  112. package/skills/grafema-codebase-analysis/references/query-patterns.md +205 -0
  113. package/src/cli.ts +8 -2
  114. package/src/commands/analyze.ts +7 -342
  115. package/src/commands/analyzeAction.ts +284 -0
  116. package/src/commands/check.ts +38 -70
  117. package/src/commands/context.ts +309 -0
  118. package/src/commands/doctor/checks.ts +9 -6
  119. package/src/commands/explain.ts +4 -3
  120. package/src/commands/explore.tsx +15 -9
  121. package/src/commands/file.ts +179 -0
  122. package/src/commands/get.ts +8 -0
  123. package/src/commands/impact.ts +3 -4
  124. package/src/commands/init.ts +19 -3
  125. package/src/commands/ls.ts +11 -2
  126. package/src/commands/overview.ts +0 -4
  127. package/src/commands/query.ts +235 -44
  128. package/src/commands/schema.ts +3 -2
  129. package/src/commands/server.ts +85 -15
  130. package/src/commands/setup-skill.ts +162 -0
  131. package/src/commands/trace.ts +18 -9
  132. package/src/plugins/builtinPlugins.ts +108 -0
  133. package/src/plugins/pluginLoader.ts +123 -0
  134. package/src/plugins/pluginResolver.js +38 -0
  135. package/src/utils/codePreview.ts +7 -3
  136. package/src/utils/formatNode.ts +3 -3
  137. package/src/utils/pathUtils.ts +9 -0
  138. package/src/utils/progressRenderer.ts +288 -0
  139. package/src/utils/spinner.ts +94 -0
@@ -12,6 +12,7 @@ import { existsSync } from 'fs';
12
12
  import { RFDBServerBackend } from '@grafema/core';
13
13
  import { formatNodeDisplay } from '../utils/formatNode.js';
14
14
  import { exitWithError } from '../utils/errorFormatter.js';
15
+ import { Spinner } from '../utils/spinner.js';
15
16
 
16
17
  interface GetOptions {
17
18
  project: string;
@@ -66,11 +67,15 @@ Examples:
66
67
  const backend = new RFDBServerBackend({ dbPath });
67
68
  await backend.connect();
68
69
 
70
+ const spinner = new Spinner('Querying graph...');
71
+ spinner.start();
72
+
69
73
  try {
70
74
  // Retrieve node by semantic ID
71
75
  const node = await backend.getNode(semanticId);
72
76
 
73
77
  if (!node) {
78
+ spinner.stop();
74
79
  exitWithError('Node not found', [
75
80
  `ID: ${semanticId}`,
76
81
  'Try: grafema query "<name>" to search for nodes',
@@ -81,6 +86,8 @@ Examples:
81
86
  const incomingEdges = await backend.getIncomingEdges(semanticId, null);
82
87
  const outgoingEdges = await backend.getOutgoingEdges(semanticId, null);
83
88
 
89
+ spinner.stop();
90
+
84
91
  if (options.json) {
85
92
  await outputJSON(backend, node, incomingEdges, outgoingEdges);
86
93
  } else {
@@ -88,6 +95,7 @@ Examples:
88
95
  }
89
96
 
90
97
  } finally {
98
+ spinner.stop();
91
99
  await backend.close();
92
100
  }
93
101
  });
@@ -7,10 +7,9 @@
7
7
  */
8
8
 
9
9
  import { Command } from 'commander';
10
- import { resolve, join, dirname } from 'path';
11
- import { relative } from 'path';
10
+ import { isAbsolute, resolve, join, dirname, relative } from 'path';
12
11
  import { existsSync } from 'fs';
13
- import { RFDBServerBackend, findContainingFunction as findContainingFunctionCore, type CallerInfo } from '@grafema/core';
12
+ import { RFDBServerBackend, findContainingFunction as findContainingFunctionCore } from '@grafema/core';
14
13
  import { formatNodeDisplay, formatNodeInline } from '../utils/formatNode.js';
15
14
  import { exitWithError } from '../utils/errorFormatter.js';
16
15
 
@@ -313,7 +312,7 @@ async function findCallsToNode(
313
312
  */
314
313
  function getModulePath(file: string, projectPath: string): string {
315
314
  if (!file) return '<unknown>';
316
- const relPath = relative(projectPath, file);
315
+ const relPath = isAbsolute(file) ? relative(projectPath, file) : file;
317
316
  const dir = dirname(relPath);
318
317
  return dir === '.' ? relPath : `${dir}/*`;
319
318
  }
@@ -8,9 +8,9 @@ import { existsSync, mkdirSync, writeFileSync, readFileSync } from 'fs';
8
8
  import { spawn } from 'child_process';
9
9
  import { createInterface } from 'readline';
10
10
  import { fileURLToPath } from 'url';
11
- import { exitWithError } from '../utils/errorFormatter.js';
12
11
  import { stringify as stringifyYAML } from 'yaml';
13
- import { DEFAULT_CONFIG } from '@grafema/core';
12
+ import { DEFAULT_CONFIG, GRAFEMA_VERSION, getSchemaVersion } from '@grafema/core';
13
+ import { installSkill } from './setup-skill.js';
14
14
 
15
15
  const __dirname = fileURLToPath(new URL('.', import.meta.url));
16
16
 
@@ -21,6 +21,7 @@ const __dirname = fileURLToPath(new URL('.', import.meta.url));
21
21
  function generateConfigYAML(): string {
22
22
  // Start with working default config
23
23
  const config = {
24
+ version: getSchemaVersion(GRAFEMA_VERSION),
24
25
  // Plugin list (fully implemented)
25
26
  plugins: DEFAULT_CONFIG.plugins,
26
27
  };
@@ -75,7 +76,9 @@ function askYesNo(question: string): Promise<boolean> {
75
76
  function runAnalyze(projectPath: string): Promise<number> {
76
77
  return new Promise((resolve) => {
77
78
  const cliPath = join(__dirname, '..', 'cli.js');
78
- const child = spawn('node', [cliPath, 'analyze', projectPath], {
79
+ // Use process.execPath (absolute path to current Node binary) instead of
80
+ // 'node' to avoid PATH lookup failures when nvm isn't loaded in the shell.
81
+ const child = spawn(process.execPath, [cliPath, 'analyze', projectPath], {
79
82
  stdio: 'inherit', // Pass through all I/O for user to see progress
80
83
  });
81
84
  child.on('close', (code) => resolve(code ?? 1));
@@ -92,6 +95,9 @@ function printNextSteps(): void {
92
95
  console.log(' 1. Review config: code .grafema/config.yaml');
93
96
  console.log(' 2. Build graph: grafema analyze');
94
97
  console.log(' 3. Explore: grafema overview');
98
+ console.log('');
99
+ console.log('For AI-assisted setup, use the Grafema MCP server');
100
+ console.log('with the "onboard_project" prompt.');
95
101
  }
96
102
 
97
103
  /**
@@ -180,6 +186,16 @@ Examples:
180
186
  }
181
187
  }
182
188
 
189
+ // Auto-install Agent Skill for AI-assisted development
190
+ try {
191
+ const installed = installSkill(projectPath);
192
+ if (installed) {
193
+ console.log('✓ Installed Agent Skill (.claude/skills/grafema-codebase-analysis/)');
194
+ }
195
+ } catch {
196
+ // Non-critical — don't fail init if skill install fails
197
+ }
198
+
183
199
  printNextSteps();
184
200
 
185
201
  // Prompt to run analyze in interactive mode
@@ -11,10 +11,12 @@
11
11
  */
12
12
 
13
13
  import { Command } from 'commander';
14
- import { resolve, join, relative } from 'path';
14
+ import { resolve, join } from 'path';
15
+ import { toRelativeDisplay } from '../utils/pathUtils.js';
15
16
  import { existsSync } from 'fs';
16
17
  import { RFDBServerBackend } from '@grafema/core';
17
18
  import { exitWithError } from '../utils/errorFormatter.js';
19
+ import { Spinner } from '../utils/spinner.js';
18
20
 
19
21
  interface LsOptions {
20
22
  project: string;
@@ -66,6 +68,9 @@ Discover available types:
66
68
  const backend = new RFDBServerBackend({ dbPath });
67
69
  await backend.connect();
68
70
 
71
+ const spinner = new Spinner('Querying graph...');
72
+ spinner.start();
73
+
69
74
  try {
70
75
  const limit = parseInt(options.limit, 10);
71
76
  const nodeType = options.type;
@@ -73,6 +78,7 @@ Discover available types:
73
78
  // Check if type exists in graph
74
79
  const typeCounts = await backend.countNodesByType();
75
80
  if (!typeCounts[nodeType]) {
81
+ spinner.stop();
76
82
  const availableTypes = Object.keys(typeCounts).sort();
77
83
  exitWithError(`No nodes of type "${nodeType}" found`, [
78
84
  'Available types:',
@@ -103,6 +109,8 @@ Discover available types:
103
109
  const totalCount = typeCounts[nodeType];
104
110
  const showing = nodes.length;
105
111
 
112
+ spinner.stop();
113
+
106
114
  if (options.json) {
107
115
  console.log(JSON.stringify({
108
116
  type: nodeType,
@@ -125,6 +133,7 @@ Discover available types:
125
133
  }
126
134
  }
127
135
  } finally {
136
+ spinner.stop();
128
137
  await backend.close();
129
138
  }
130
139
  });
@@ -134,7 +143,7 @@ Discover available types:
134
143
  * Different types show different fields.
135
144
  */
136
145
  function formatNodeForList(node: NodeInfo, nodeType: string, projectPath: string): string {
137
- const relFile = node.file ? relative(projectPath, node.file) : '';
146
+ const relFile = node.file ? toRelativeDisplay(node.file, projectPath) : '';
138
147
  const loc = node.line ? `${relFile}:${node.line}` : relFile;
139
148
 
140
149
  // HTTP routes: METHOD PATH (location)
@@ -8,10 +8,6 @@ import { existsSync } from 'fs';
8
8
  import { RFDBServerBackend } from '@grafema/core';
9
9
  import { exitWithError } from '../utils/errorFormatter.js';
10
10
 
11
- interface NodeStats {
12
- type: string;
13
- count: number;
14
- }
15
11
 
16
12
  export const overviewCommand = new Command('overview')
17
13
  .description('Show project overview and statistics')
@@ -10,11 +10,22 @@
10
10
  */
11
11
 
12
12
  import { Command } from 'commander';
13
- import { resolve, join, relative, basename } from 'path';
13
+ import { resolve, join, basename } from 'path';
14
+ import { toRelativeDisplay } from '../utils/pathUtils.js';
14
15
  import { existsSync } from 'fs';
15
- import { RFDBServerBackend, parseSemanticId, findCallsInFunction as findCallsInFunctionCore, findContainingFunction as findContainingFunctionCore } from '@grafema/core';
16
+ import { RFDBServerBackend, parseSemanticId, parseSemanticIdV2, findCallsInFunction as findCallsInFunctionCore, findContainingFunction as findContainingFunctionCore } from '@grafema/core';
16
17
  import { formatNodeDisplay, formatNodeInline, formatLocation } from '../utils/formatNode.js';
17
18
  import { exitWithError } from '../utils/errorFormatter.js';
19
+ import { Spinner } from '../utils/spinner.js';
20
+
21
+ // Node type constants to avoid magic string duplication
22
+ const HTTP_ROUTE_TYPE = 'http:route';
23
+ const HTTP_REQUEST_TYPE = 'http:request';
24
+ const SOCKETIO_EVENT_TYPE = 'socketio:event';
25
+ const SOCKETIO_EMIT_TYPE = 'socketio:emit';
26
+ const SOCKETIO_ON_TYPE = 'socketio:on';
27
+ const GRAFEMA_PLUGIN_TYPE = 'grafema:plugin';
28
+ const PROPERTY_ACCESS_TYPE = 'PROPERTY_ACCESS';
18
29
 
19
30
  interface QueryOptions {
20
31
  project: string;
@@ -75,6 +86,9 @@ export const queryCommand = new Command('query')
75
86
  '--raw',
76
87
  `Execute raw Datalog query
77
88
 
89
+ Supports both direct queries and Datalog rules.
90
+ Rules (containing ":-") define a violation predicate and return matching nodes.
91
+
78
92
  Predicates:
79
93
  type(Id, Type) Find nodes by type or get type of node
80
94
  node(Id, Type) Alias for type
@@ -83,10 +97,14 @@ Predicates:
83
97
  path(Src, Dst) Check reachability between nodes
84
98
  incoming(Dst, Src, T) Find incoming edges
85
99
 
86
- Examples:
100
+ Direct queries:
87
101
  grafema query --raw 'type(X, "FUNCTION")'
88
102
  grafema query --raw 'type(X, "FUNCTION"), attr(X, "name", "main")'
89
- grafema query --raw 'edge(X, Y, "CALLS")'`
103
+ grafema query --raw 'edge(X, Y, "CALLS")'
104
+
105
+ Rules (must define violation/1):
106
+ grafema query --raw 'violation(X) :- node(X, "FUNCTION").'
107
+ grafema query --raw 'violation(X) :- node(X, "http:route"), attr(X, "method", "POST").'`
90
108
  )
91
109
  .option(
92
110
  '-t, --type <nodeType>',
@@ -130,15 +148,23 @@ Examples:
130
148
  const backend = new RFDBServerBackend({ dbPath });
131
149
  await backend.connect();
132
150
 
151
+ const spinner = new Spinner('Querying graph...');
152
+ spinner.start();
153
+
133
154
  try {
155
+ const limit = parseInt(options.limit, 10);
156
+ if (isNaN(limit) || limit < 1) {
157
+ spinner.stop();
158
+ exitWithError('Invalid limit', ['Use a positive number, e.g.: --limit 10']);
159
+ }
160
+
134
161
  // Raw Datalog mode
135
162
  if (options.raw) {
136
- await executeRawQuery(backend, pattern, options);
163
+ spinner.stop();
164
+ await executeRawQuery(backend, pattern, limit, options.json);
137
165
  return;
138
166
  }
139
167
 
140
- const limit = parseInt(options.limit, 10);
141
-
142
168
  // Parse query with scope support
143
169
  let query: ParsedQuery;
144
170
 
@@ -159,6 +185,8 @@ Examples:
159
185
  // Find matching nodes
160
186
  const nodes = await findNodes(backend, query, limit);
161
187
 
188
+ spinner.stop();
189
+
162
190
  // Check if query has scope constraints for suggestion
163
191
  const hasScope = query.file !== null || query.scopes.length > 0;
164
192
 
@@ -218,6 +246,7 @@ Examples:
218
246
  }
219
247
 
220
248
  } finally {
249
+ spinner.stop();
221
250
  await backend.close();
222
251
  }
223
252
  });
@@ -237,23 +266,31 @@ function parsePattern(pattern: string): { type: string | null; name: string } {
237
266
  fn: 'FUNCTION',
238
267
  func: 'FUNCTION',
239
268
  class: 'CLASS',
269
+ interface: 'INTERFACE',
270
+ type: 'TYPE',
271
+ enum: 'ENUM',
240
272
  module: 'MODULE',
241
273
  variable: 'VARIABLE',
242
274
  var: 'VARIABLE',
243
275
  const: 'CONSTANT',
244
276
  constant: 'CONSTANT',
245
277
  // HTTP route aliases
246
- route: 'http:route',
247
- endpoint: 'http:route',
278
+ route: HTTP_ROUTE_TYPE,
279
+ endpoint: HTTP_ROUTE_TYPE,
248
280
  // HTTP request aliases
249
- request: 'http:request',
250
- fetch: 'http:request',
251
- api: 'http:request',
281
+ request: HTTP_REQUEST_TYPE,
282
+ fetch: HTTP_REQUEST_TYPE,
283
+ api: HTTP_REQUEST_TYPE,
252
284
  // Socket.IO aliases
253
- event: 'socketio:event',
254
- emit: 'socketio:emit',
255
- on: 'socketio:on',
256
- listener: 'socketio:on',
285
+ event: SOCKETIO_EVENT_TYPE,
286
+ emit: SOCKETIO_EMIT_TYPE,
287
+ on: SOCKETIO_ON_TYPE,
288
+ listener: SOCKETIO_ON_TYPE,
289
+ // Grafema internal
290
+ plugin: GRAFEMA_PLUGIN_TYPE,
291
+ // Property access aliases (REG-395)
292
+ property: PROPERTY_ACCESS_TYPE,
293
+ prop: PROPERTY_ACCESS_TYPE,
257
294
  };
258
295
 
259
296
  if (typeMap[typeWord]) {
@@ -357,6 +394,36 @@ export function isFileScope(scope: string): boolean {
357
394
  * @returns true if ID matches all constraints
358
395
  */
359
396
  export function matchesScope(semanticId: string, file: string | null, scopes: string[]): boolean {
397
+ // No constraints = everything matches (regardless of ID format)
398
+ if (file === null && scopes.length === 0) return true;
399
+
400
+ // Try v2 parsing first
401
+ const parsedV2 = parseSemanticIdV2(semanticId);
402
+ if (parsedV2) {
403
+ // File scope check (v2)
404
+ if (file !== null) {
405
+ if (parsedV2.file === file) {
406
+ // Exact match - OK
407
+ } else if (parsedV2.file.endsWith('/' + file)) {
408
+ // Partial path match - OK
409
+ } else if (basename(parsedV2.file) === file) {
410
+ // Basename exact match - OK
411
+ } else {
412
+ return false;
413
+ }
414
+ }
415
+
416
+ // Function/class scope check (v2): check namedParent
417
+ for (const scope of scopes) {
418
+ if (!parsedV2.namedParent || parsedV2.namedParent.toLowerCase() !== scope.toLowerCase()) {
419
+ return false;
420
+ }
421
+ }
422
+
423
+ return true;
424
+ }
425
+
426
+ // Fallback to v1 parsing
360
427
  const parsed = parseSemanticId(semanticId);
361
428
  if (!parsed) return false;
362
429
 
@@ -411,6 +478,17 @@ export function matchesScope(semanticId: string, file: string | null, scopes: st
411
478
  * @returns Human-readable scope context or null
412
479
  */
413
480
  export function extractScopeContext(semanticId: string): string | null {
481
+ // Try v2 parsing first
482
+ const parsedV2 = parseSemanticIdV2(semanticId);
483
+ if (parsedV2) {
484
+ if (parsedV2.namedParent) {
485
+ return `inside ${parsedV2.namedParent}`;
486
+ }
487
+ // v2 with no parent = top-level
488
+ return null;
489
+ }
490
+
491
+ // Fallback to v1 parsing
414
492
  const parsed = parseSemanticId(semanticId);
415
493
  if (!parsed) return null;
416
494
 
@@ -462,7 +540,7 @@ function matchesSearchPattern(
462
540
  const lowerPattern = pattern.toLowerCase();
463
541
 
464
542
  // HTTP routes: search method and path
465
- if (nodeType === 'http:route') {
543
+ if (nodeType === HTTP_ROUTE_TYPE) {
466
544
  const method = (node.method || '').toLowerCase();
467
545
  const path = (node.path || '').toLowerCase();
468
546
 
@@ -488,7 +566,7 @@ function matchesSearchPattern(
488
566
  }
489
567
 
490
568
  // HTTP requests: search method and url
491
- if (nodeType === 'http:request') {
569
+ if (nodeType === HTTP_REQUEST_TYPE) {
492
570
  const method = (node.method || '').toLowerCase();
493
571
  const url = (node.url || '').toLowerCase();
494
572
 
@@ -514,13 +592,13 @@ function matchesSearchPattern(
514
592
  }
515
593
 
516
594
  // Socket.IO event channels: search name field (standard)
517
- if (nodeType === 'socketio:event') {
595
+ if (nodeType === SOCKETIO_EVENT_TYPE) {
518
596
  const nodeName = (node.name || '').toLowerCase();
519
597
  return nodeName.includes(lowerPattern);
520
598
  }
521
599
 
522
600
  // Socket.IO emit/on: search event field
523
- if (nodeType === 'socketio:emit' || nodeType === 'socketio:on') {
601
+ if (nodeType === SOCKETIO_EMIT_TYPE || nodeType === SOCKETIO_ON_TYPE) {
524
602
  const eventName = (node.event || '').toLowerCase();
525
603
  return eventName.includes(lowerPattern);
526
604
  }
@@ -544,18 +622,22 @@ async function findNodes(
544
622
  : [
545
623
  'FUNCTION',
546
624
  'CLASS',
625
+ 'INTERFACE',
626
+ 'TYPE',
627
+ 'ENUM',
547
628
  'MODULE',
548
629
  'VARIABLE',
549
630
  'CONSTANT',
550
- 'http:route',
551
- 'http:request',
552
- 'socketio:event',
553
- 'socketio:emit',
554
- 'socketio:on'
631
+ HTTP_ROUTE_TYPE,
632
+ HTTP_REQUEST_TYPE,
633
+ SOCKETIO_EVENT_TYPE,
634
+ SOCKETIO_EMIT_TYPE,
635
+ SOCKETIO_ON_TYPE,
636
+ PROPERTY_ACCESS_TYPE,
555
637
  ];
556
638
 
557
639
  for (const nodeType of searchTypes) {
558
- for await (const node of backend.queryNodes({ nodeType: nodeType as any })) {
640
+ for await (const node of backend.queryNodes({ nodeType })) {
559
641
  // Type-aware field matching (name)
560
642
  const nameMatches = matchesSearchPattern(node, nodeType, query.name);
561
643
  if (!nameMatches) continue;
@@ -576,24 +658,24 @@ async function findNodes(
576
658
  nodeInfo.scopeContext = extractScopeContext(node.id);
577
659
 
578
660
  // Include method and path for http:route nodes
579
- if (nodeType === 'http:route') {
661
+ if (nodeType === HTTP_ROUTE_TYPE) {
580
662
  nodeInfo.method = node.method as string | undefined;
581
663
  nodeInfo.path = node.path as string | undefined;
582
664
  }
583
665
 
584
666
  // Include method and url for http:request nodes
585
- if (nodeType === 'http:request') {
667
+ if (nodeType === HTTP_REQUEST_TYPE) {
586
668
  nodeInfo.method = node.method as string | undefined;
587
669
  nodeInfo.url = node.url as string | undefined;
588
670
  }
589
671
 
590
672
  // Include event field for Socket.IO nodes
591
- if (nodeType === 'socketio:event' || nodeType === 'socketio:emit' || nodeType === 'socketio:on') {
673
+ if (nodeType === SOCKETIO_EVENT_TYPE || nodeType === SOCKETIO_EMIT_TYPE || nodeType === SOCKETIO_ON_TYPE) {
592
674
  nodeInfo.event = node.event as string | undefined;
593
675
  }
594
676
 
595
677
  // Include emit-specific fields
596
- if (nodeType === 'socketio:emit') {
678
+ if (nodeType === SOCKETIO_EMIT_TYPE) {
597
679
  nodeInfo.room = node.room as string | undefined;
598
680
  nodeInfo.namespace = node.namespace as string | undefined;
599
681
  nodeInfo.broadcast = node.broadcast as boolean | undefined;
@@ -601,11 +683,26 @@ async function findNodes(
601
683
  }
602
684
 
603
685
  // Include listener-specific fields
604
- if (nodeType === 'socketio:on') {
686
+ if (nodeType === SOCKETIO_ON_TYPE) {
605
687
  nodeInfo.objectName = node.objectName as string | undefined;
606
688
  nodeInfo.handlerName = node.handlerName as string | undefined;
607
689
  }
608
690
 
691
+ // Include plugin-specific fields
692
+ if (nodeType === GRAFEMA_PLUGIN_TYPE) {
693
+ nodeInfo.phase = node.phase as string | undefined;
694
+ nodeInfo.priority = node.priority as number | undefined;
695
+ nodeInfo.builtin = node.builtin as boolean | undefined;
696
+ nodeInfo.createsNodes = node.createsNodes as string[] | undefined;
697
+ nodeInfo.createsEdges = node.createsEdges as string[] | undefined;
698
+ nodeInfo.dependencies = node.dependencies as string[] | undefined;
699
+ }
700
+
701
+ // Include objectName for PROPERTY_ACCESS nodes (REG-395)
702
+ if (nodeType === PROPERTY_ACCESS_TYPE) {
703
+ nodeInfo.objectName = node.objectName as string | undefined;
704
+ }
705
+
609
706
  results.push(nodeInfo);
610
707
  if (results.length >= limit) break;
611
708
  }
@@ -712,29 +809,35 @@ async function getCallees(
712
809
  */
713
810
  async function displayNode(node: NodeInfo, projectPath: string, backend: RFDBServerBackend): Promise<void> {
714
811
  // Special formatting for HTTP routes
715
- if (node.type === 'http:route' && node.method && node.path) {
812
+ if (node.type === HTTP_ROUTE_TYPE && node.method && node.path) {
716
813
  console.log(formatHttpRouteDisplay(node, projectPath));
717
814
  return;
718
815
  }
719
816
 
720
817
  // Special formatting for HTTP requests
721
- if (node.type === 'http:request') {
818
+ if (node.type === HTTP_REQUEST_TYPE) {
722
819
  console.log(formatHttpRequestDisplay(node, projectPath));
723
820
  return;
724
821
  }
725
822
 
726
823
  // Special formatting for Socket.IO event channels
727
- if (node.type === 'socketio:event') {
824
+ if (node.type === SOCKETIO_EVENT_TYPE) {
728
825
  console.log(await formatSocketEventDisplay(node, projectPath, backend));
729
826
  return;
730
827
  }
731
828
 
732
829
  // Special formatting for Socket.IO emit/on
733
- if (node.type === 'socketio:emit' || node.type === 'socketio:on') {
830
+ if (node.type === SOCKETIO_EMIT_TYPE || node.type === SOCKETIO_ON_TYPE) {
734
831
  console.log(formatSocketIONodeDisplay(node, projectPath));
735
832
  return;
736
833
  }
737
834
 
835
+ // Special formatting for Grafema plugin nodes
836
+ if (node.type === GRAFEMA_PLUGIN_TYPE) {
837
+ console.log(formatPluginDisplay(node, projectPath));
838
+ return;
839
+ }
840
+
738
841
  console.log(formatNodeDisplay(node, { projectPath }));
739
842
 
740
843
  // Add scope context if present
@@ -758,7 +861,7 @@ function formatHttpRouteDisplay(node: NodeInfo, projectPath: string): string {
758
861
 
759
862
  // Line 2: Location
760
863
  if (node.file) {
761
- const relPath = relative(projectPath, node.file);
864
+ const relPath = toRelativeDisplay(node.file, projectPath);
762
865
  const loc = node.line ? `${relPath}:${node.line}` : relPath;
763
866
  lines.push(` Location: ${loc}`);
764
867
  }
@@ -783,7 +886,7 @@ function formatHttpRequestDisplay(node: NodeInfo, projectPath: string): string {
783
886
 
784
887
  // Line 2: Location
785
888
  if (node.file) {
786
- const relPath = relative(projectPath, node.file);
889
+ const relPath = toRelativeDisplay(node.file, projectPath);
787
890
  const loc = node.line ? `${relPath}:${node.line}` : relPath;
788
891
  lines.push(` Location: ${loc}`);
789
892
  }
@@ -870,7 +973,7 @@ function formatSocketIONodeDisplay(node: NodeInfo, projectPath: string): string
870
973
  }
871
974
 
872
975
  // Emit-specific fields
873
- if (node.type === 'socketio:emit') {
976
+ if (node.type === SOCKETIO_EMIT_TYPE) {
874
977
  if (node.room) {
875
978
  lines.push(` Room: ${node.room}`);
876
979
  }
@@ -883,7 +986,7 @@ function formatSocketIONodeDisplay(node: NodeInfo, projectPath: string): string
883
986
  }
884
987
 
885
988
  // Listener-specific fields
886
- if (node.type === 'socketio:on' && node.handlerName) {
989
+ if (node.type === SOCKETIO_ON_TYPE && node.handlerName) {
887
990
  lines.push(` Handler: ${node.handlerName}`);
888
991
  }
889
992
 
@@ -891,18 +994,96 @@ function formatSocketIONodeDisplay(node: NodeInfo, projectPath: string): string
891
994
  }
892
995
 
893
996
  /**
894
- * Execute raw Datalog query (backwards compat)
997
+ * Format Grafema plugin node for display.
998
+ *
999
+ * Output:
1000
+ * [grafema:plugin] HTTPConnectionEnricher
1001
+ * Phase: ENRICHMENT (priority: 50)
1002
+ * Creates: edges: INTERACTS_WITH, HTTP_RECEIVES
1003
+ * Dependencies: ExpressRouteAnalyzer, FetchAnalyzer, ExpressResponseAnalyzer
1004
+ * Source: packages/core/src/plugins/enrichment/HTTPConnectionEnricher.ts
1005
+ */
1006
+ function formatPluginDisplay(node: NodeInfo, projectPath: string): string {
1007
+ const lines: string[] = [];
1008
+
1009
+ lines.push(`[${node.type}] ${node.name}`);
1010
+
1011
+ const phase = (node.phase as string) || 'unknown';
1012
+ const priority = (node.priority as number) ?? 0;
1013
+ lines.push(` Phase: ${phase} (priority: ${priority})`);
1014
+
1015
+ const createsNodes = (node.createsNodes as string[]) || [];
1016
+ const createsEdges = (node.createsEdges as string[]) || [];
1017
+ const createsParts: string[] = [];
1018
+ if (createsNodes.length > 0) createsParts.push(`nodes: ${createsNodes.join(', ')}`);
1019
+ if (createsEdges.length > 0) createsParts.push(`edges: ${createsEdges.join(', ')}`);
1020
+ if (createsParts.length > 0) {
1021
+ lines.push(` Creates: ${createsParts.join('; ')}`);
1022
+ }
1023
+
1024
+ const deps = (node.dependencies as string[]) || [];
1025
+ if (deps.length > 0) {
1026
+ lines.push(` Dependencies: ${deps.join(', ')}`);
1027
+ }
1028
+
1029
+ if (node.file) {
1030
+ const relPath = toRelativeDisplay(node.file, projectPath);
1031
+ lines.push(` Source: ${relPath}`);
1032
+ }
1033
+
1034
+ return lines.join('\n');
1035
+ }
1036
+
1037
+ /** Built-in Datalog predicates supported by RFDB server */
1038
+ export const BUILTIN_PREDICATES = new Set([
1039
+ 'node', 'type', 'edge', 'incoming', 'path',
1040
+ 'attr', 'attr_edge',
1041
+ 'neq', 'starts_with', 'not_starts_with',
1042
+ ]);
1043
+
1044
+ /** Extract predicate names from a Datalog query string */
1045
+ export function extractPredicates(query: string): string[] {
1046
+ const regex = /\b([a-z_][a-z0-9_]*)\s*\(/g;
1047
+ const predicates = new Set<string>();
1048
+ let match;
1049
+ while ((match = regex.exec(query)) !== null) {
1050
+ predicates.add(match[1]);
1051
+ }
1052
+ return [...predicates];
1053
+ }
1054
+
1055
+ /** Extract predicate names defined as rule heads (word(...) :-) */
1056
+ export function extractRuleHeads(query: string): Set<string> {
1057
+ const regex = /\b([a-z_][a-z0-9_]*)\s*\([^)]*\)\s*:-/g;
1058
+ const heads = new Set<string>();
1059
+ let match;
1060
+ while ((match = regex.exec(query)) !== null) {
1061
+ heads.add(match[1]);
1062
+ }
1063
+ return heads;
1064
+ }
1065
+
1066
+ /** Find predicates in a query that are not built-in and not user-defined rule heads */
1067
+ export function getUnknownPredicates(query: string): string[] {
1068
+ const predicates = extractPredicates(query);
1069
+ const ruleHeads = extractRuleHeads(query);
1070
+ return predicates.filter(p => !BUILTIN_PREDICATES.has(p) && !ruleHeads.has(p));
1071
+ }
1072
+
1073
+ /**
1074
+ * Execute raw Datalog query.
1075
+ * Uses unified executeDatalog endpoint which auto-detects rules vs direct queries.
895
1076
  */
896
1077
  async function executeRawQuery(
897
1078
  backend: RFDBServerBackend,
898
1079
  query: string,
899
- options: QueryOptions
1080
+ limit: number,
1081
+ json?: boolean
900
1082
  ): Promise<void> {
901
- const results = await backend.datalogQuery(query);
902
- const limit = parseInt(options.limit, 10);
1083
+ const results = await backend.executeDatalog(query);
903
1084
  const limited = results.slice(0, limit);
904
1085
 
905
- if (options.json) {
1086
+ if (json) {
906
1087
  console.log(JSON.stringify(limited, null, 2));
907
1088
  } else {
908
1089
  if (limited.length === 0) {
@@ -916,4 +1097,14 @@ async function executeRawQuery(
916
1097
  }
917
1098
  }
918
1099
  }
1100
+
1101
+ // Show warning for unknown predicates (on stderr, works in both text and JSON mode)
1102
+ if (limited.length === 0) {
1103
+ const unknown = getUnknownPredicates(query);
1104
+ if (unknown.length > 0) {
1105
+ const unknownList = unknown.map(p => `'${p}'`).join(', ');
1106
+ const builtinList = [...BUILTIN_PREDICATES].join(', ');
1107
+ console.error(`Note: unknown predicate ${unknownList}. Built-in predicates: ${builtinList}`);
1108
+ }
1109
+ }
919
1110
  }