@grafema/cli 0.2.5-beta → 0.2.7

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 (105) hide show
  1. package/README.md +12 -0
  2. package/dist/cli.js +6 -2
  3. package/dist/cli.js.map +1 -1
  4. package/dist/commands/analyze.d.ts +3 -10
  5. package/dist/commands/analyze.d.ts.map +1 -1
  6. package/dist/commands/analyze.js +5 -347
  7. package/dist/commands/analyze.js.map +1 -1
  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.js +2 -2
  13. package/dist/commands/check.js.map +1 -1
  14. package/dist/commands/context.d.ts +16 -0
  15. package/dist/commands/context.d.ts.map +1 -0
  16. package/dist/commands/context.js +238 -0
  17. package/dist/commands/context.js.map +1 -0
  18. package/dist/commands/doctor/checks.js +1 -1
  19. package/dist/commands/doctor/checks.js.map +1 -1
  20. package/dist/commands/explain.d.ts.map +1 -1
  21. package/dist/commands/explain.js +4 -3
  22. package/dist/commands/explain.js.map +1 -1
  23. package/dist/commands/file.d.ts +15 -0
  24. package/dist/commands/file.d.ts.map +1 -0
  25. package/dist/commands/file.js +144 -0
  26. package/dist/commands/file.js.map +1 -0
  27. package/dist/commands/impact.d.ts.map +1 -1
  28. package/dist/commands/impact.js +2 -3
  29. package/dist/commands/impact.js.map +1 -1
  30. package/dist/commands/init.d.ts.map +1 -1
  31. package/dist/commands/init.js +13 -1
  32. package/dist/commands/init.js.map +1 -1
  33. package/dist/commands/ls.d.ts.map +1 -1
  34. package/dist/commands/ls.js +3 -2
  35. package/dist/commands/ls.js.map +1 -1
  36. package/dist/commands/query.d.ts +8 -0
  37. package/dist/commands/query.d.ts.map +1 -1
  38. package/dist/commands/query.js +158 -51
  39. package/dist/commands/query.js.map +1 -1
  40. package/dist/commands/schema.d.ts.map +1 -1
  41. package/dist/commands/schema.js +3 -2
  42. package/dist/commands/schema.js.map +1 -1
  43. package/dist/commands/server.d.ts.map +1 -1
  44. package/dist/commands/server.js +8 -59
  45. package/dist/commands/server.js.map +1 -1
  46. package/dist/commands/setup-skill.d.ts +17 -0
  47. package/dist/commands/setup-skill.d.ts.map +1 -0
  48. package/dist/commands/setup-skill.js +131 -0
  49. package/dist/commands/setup-skill.js.map +1 -0
  50. package/dist/commands/trace.d.ts.map +1 -1
  51. package/dist/commands/trace.js +20 -10
  52. package/dist/commands/trace.js.map +1 -1
  53. package/dist/plugins/builtinPlugins.d.ts +10 -0
  54. package/dist/plugins/builtinPlugins.d.ts.map +1 -0
  55. package/dist/plugins/builtinPlugins.js +68 -0
  56. package/dist/plugins/builtinPlugins.js.map +1 -0
  57. package/dist/plugins/pluginLoader.d.ts +16 -0
  58. package/dist/plugins/pluginLoader.d.ts.map +1 -0
  59. package/dist/plugins/pluginLoader.js +101 -0
  60. package/dist/plugins/pluginLoader.js.map +1 -0
  61. package/dist/plugins/pluginResolver.js +38 -0
  62. package/dist/utils/codePreview.d.ts +1 -0
  63. package/dist/utils/codePreview.d.ts.map +1 -1
  64. package/dist/utils/codePreview.js +5 -3
  65. package/dist/utils/codePreview.js.map +1 -1
  66. package/dist/utils/formatNode.d.ts +1 -1
  67. package/dist/utils/formatNode.d.ts.map +1 -1
  68. package/dist/utils/formatNode.js +2 -2
  69. package/dist/utils/formatNode.js.map +1 -1
  70. package/dist/utils/pathUtils.d.ts +2 -0
  71. package/dist/utils/pathUtils.d.ts.map +1 -0
  72. package/dist/utils/pathUtils.js +9 -0
  73. package/dist/utils/pathUtils.js.map +1 -0
  74. package/dist/utils/progressRenderer.d.ts +4 -0
  75. package/dist/utils/progressRenderer.d.ts.map +1 -1
  76. package/dist/utils/progressRenderer.js +23 -4
  77. package/dist/utils/progressRenderer.js.map +1 -1
  78. package/package.json +7 -9
  79. package/skills/grafema-codebase-analysis/SKILL.md +295 -0
  80. package/skills/grafema-codebase-analysis/references/node-edge-types.md +123 -0
  81. package/skills/grafema-codebase-analysis/references/query-patterns.md +205 -0
  82. package/src/cli.ts +8 -2
  83. package/src/commands/analyze.ts +5 -435
  84. package/src/commands/analyzeAction.ts +284 -0
  85. package/src/commands/check.ts +2 -2
  86. package/src/commands/context.ts +309 -0
  87. package/src/commands/doctor/checks.ts +1 -1
  88. package/src/commands/explain.ts +4 -3
  89. package/src/commands/explore.tsx +7 -5
  90. package/src/commands/file.ts +179 -0
  91. package/src/commands/impact.ts +2 -3
  92. package/src/commands/init.ts +13 -1
  93. package/src/commands/ls.ts +3 -2
  94. package/src/commands/query.ts +167 -52
  95. package/src/commands/schema.ts +3 -2
  96. package/src/commands/server.ts +8 -64
  97. package/src/commands/setup-skill.ts +162 -0
  98. package/src/commands/trace.ts +18 -9
  99. package/src/plugins/builtinPlugins.ts +108 -0
  100. package/src/plugins/pluginLoader.ts +123 -0
  101. package/src/plugins/pluginResolver.js +38 -0
  102. package/src/utils/codePreview.ts +7 -3
  103. package/src/utils/formatNode.ts +3 -3
  104. package/src/utils/pathUtils.ts +9 -0
  105. package/src/utils/progressRenderer.ts +25 -4
@@ -10,13 +10,23 @@
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';
18
19
  import { Spinner } from '../utils/spinner.js';
19
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';
29
+
20
30
  interface QueryOptions {
21
31
  project: string;
22
32
  json?: boolean;
@@ -76,6 +86,9 @@ export const queryCommand = new Command('query')
76
86
  '--raw',
77
87
  `Execute raw Datalog query
78
88
 
89
+ Supports both direct queries and Datalog rules.
90
+ Rules (containing ":-") define a violation predicate and return matching nodes.
91
+
79
92
  Predicates:
80
93
  type(Id, Type) Find nodes by type or get type of node
81
94
  node(Id, Type) Alias for type
@@ -84,10 +97,14 @@ Predicates:
84
97
  path(Src, Dst) Check reachability between nodes
85
98
  incoming(Dst, Src, T) Find incoming edges
86
99
 
87
- Examples:
100
+ Direct queries:
88
101
  grafema query --raw 'type(X, "FUNCTION")'
89
102
  grafema query --raw 'type(X, "FUNCTION"), attr(X, "name", "main")'
90
- 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").'`
91
108
  )
92
109
  .option(
93
110
  '-t, --type <nodeType>',
@@ -135,15 +152,19 @@ Examples:
135
152
  spinner.start();
136
153
 
137
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
+
138
161
  // Raw Datalog mode
139
162
  if (options.raw) {
140
163
  spinner.stop();
141
- await executeRawQuery(backend, pattern, options);
164
+ await executeRawQuery(backend, pattern, limit, options.json);
142
165
  return;
143
166
  }
144
167
 
145
- const limit = parseInt(options.limit, 10);
146
-
147
168
  // Parse query with scope support
148
169
  let query: ParsedQuery;
149
170
 
@@ -245,28 +266,31 @@ function parsePattern(pattern: string): { type: string | null; name: string } {
245
266
  fn: 'FUNCTION',
246
267
  func: 'FUNCTION',
247
268
  class: 'CLASS',
269
+ interface: 'INTERFACE',
270
+ type: 'TYPE',
271
+ enum: 'ENUM',
248
272
  module: 'MODULE',
249
273
  variable: 'VARIABLE',
250
274
  var: 'VARIABLE',
251
275
  const: 'CONSTANT',
252
276
  constant: 'CONSTANT',
253
277
  // HTTP route aliases
254
- route: 'http:route',
255
- endpoint: 'http:route',
278
+ route: HTTP_ROUTE_TYPE,
279
+ endpoint: HTTP_ROUTE_TYPE,
256
280
  // HTTP request aliases
257
- request: 'http:request',
258
- fetch: 'http:request',
259
- api: 'http:request',
281
+ request: HTTP_REQUEST_TYPE,
282
+ fetch: HTTP_REQUEST_TYPE,
283
+ api: HTTP_REQUEST_TYPE,
260
284
  // Socket.IO aliases
261
- event: 'socketio:event',
262
- emit: 'socketio:emit',
263
- on: 'socketio:on',
264
- listener: 'socketio:on',
285
+ event: SOCKETIO_EVENT_TYPE,
286
+ emit: SOCKETIO_EMIT_TYPE,
287
+ on: SOCKETIO_ON_TYPE,
288
+ listener: SOCKETIO_ON_TYPE,
265
289
  // Grafema internal
266
- plugin: 'grafema:plugin',
290
+ plugin: GRAFEMA_PLUGIN_TYPE,
267
291
  // Property access aliases (REG-395)
268
- property: 'PROPERTY_ACCESS',
269
- prop: 'PROPERTY_ACCESS',
292
+ property: PROPERTY_ACCESS_TYPE,
293
+ prop: PROPERTY_ACCESS_TYPE,
270
294
  };
271
295
 
272
296
  if (typeMap[typeWord]) {
@@ -370,6 +394,36 @@ export function isFileScope(scope: string): boolean {
370
394
  * @returns true if ID matches all constraints
371
395
  */
372
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
373
427
  const parsed = parseSemanticId(semanticId);
374
428
  if (!parsed) return false;
375
429
 
@@ -424,6 +478,17 @@ export function matchesScope(semanticId: string, file: string | null, scopes: st
424
478
  * @returns Human-readable scope context or null
425
479
  */
426
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
427
492
  const parsed = parseSemanticId(semanticId);
428
493
  if (!parsed) return null;
429
494
 
@@ -475,7 +540,7 @@ function matchesSearchPattern(
475
540
  const lowerPattern = pattern.toLowerCase();
476
541
 
477
542
  // HTTP routes: search method and path
478
- if (nodeType === 'http:route') {
543
+ if (nodeType === HTTP_ROUTE_TYPE) {
479
544
  const method = (node.method || '').toLowerCase();
480
545
  const path = (node.path || '').toLowerCase();
481
546
 
@@ -501,7 +566,7 @@ function matchesSearchPattern(
501
566
  }
502
567
 
503
568
  // HTTP requests: search method and url
504
- if (nodeType === 'http:request') {
569
+ if (nodeType === HTTP_REQUEST_TYPE) {
505
570
  const method = (node.method || '').toLowerCase();
506
571
  const url = (node.url || '').toLowerCase();
507
572
 
@@ -527,13 +592,13 @@ function matchesSearchPattern(
527
592
  }
528
593
 
529
594
  // Socket.IO event channels: search name field (standard)
530
- if (nodeType === 'socketio:event') {
595
+ if (nodeType === SOCKETIO_EVENT_TYPE) {
531
596
  const nodeName = (node.name || '').toLowerCase();
532
597
  return nodeName.includes(lowerPattern);
533
598
  }
534
599
 
535
600
  // Socket.IO emit/on: search event field
536
- if (nodeType === 'socketio:emit' || nodeType === 'socketio:on') {
601
+ if (nodeType === SOCKETIO_EMIT_TYPE || nodeType === SOCKETIO_ON_TYPE) {
537
602
  const eventName = (node.event || '').toLowerCase();
538
603
  return eventName.includes(lowerPattern);
539
604
  }
@@ -557,19 +622,22 @@ async function findNodes(
557
622
  : [
558
623
  'FUNCTION',
559
624
  'CLASS',
625
+ 'INTERFACE',
626
+ 'TYPE',
627
+ 'ENUM',
560
628
  'MODULE',
561
629
  'VARIABLE',
562
630
  'CONSTANT',
563
- 'http:route',
564
- 'http:request',
565
- 'socketio:event',
566
- 'socketio:emit',
567
- 'socketio:on',
568
- 'PROPERTY_ACCESS'
631
+ HTTP_ROUTE_TYPE,
632
+ HTTP_REQUEST_TYPE,
633
+ SOCKETIO_EVENT_TYPE,
634
+ SOCKETIO_EMIT_TYPE,
635
+ SOCKETIO_ON_TYPE,
636
+ PROPERTY_ACCESS_TYPE,
569
637
  ];
570
638
 
571
639
  for (const nodeType of searchTypes) {
572
- for await (const node of backend.queryNodes({ nodeType: nodeType as any })) {
640
+ for await (const node of backend.queryNodes({ nodeType })) {
573
641
  // Type-aware field matching (name)
574
642
  const nameMatches = matchesSearchPattern(node, nodeType, query.name);
575
643
  if (!nameMatches) continue;
@@ -590,24 +658,24 @@ async function findNodes(
590
658
  nodeInfo.scopeContext = extractScopeContext(node.id);
591
659
 
592
660
  // Include method and path for http:route nodes
593
- if (nodeType === 'http:route') {
661
+ if (nodeType === HTTP_ROUTE_TYPE) {
594
662
  nodeInfo.method = node.method as string | undefined;
595
663
  nodeInfo.path = node.path as string | undefined;
596
664
  }
597
665
 
598
666
  // Include method and url for http:request nodes
599
- if (nodeType === 'http:request') {
667
+ if (nodeType === HTTP_REQUEST_TYPE) {
600
668
  nodeInfo.method = node.method as string | undefined;
601
669
  nodeInfo.url = node.url as string | undefined;
602
670
  }
603
671
 
604
672
  // Include event field for Socket.IO nodes
605
- if (nodeType === 'socketio:event' || nodeType === 'socketio:emit' || nodeType === 'socketio:on') {
673
+ if (nodeType === SOCKETIO_EVENT_TYPE || nodeType === SOCKETIO_EMIT_TYPE || nodeType === SOCKETIO_ON_TYPE) {
606
674
  nodeInfo.event = node.event as string | undefined;
607
675
  }
608
676
 
609
677
  // Include emit-specific fields
610
- if (nodeType === 'socketio:emit') {
678
+ if (nodeType === SOCKETIO_EMIT_TYPE) {
611
679
  nodeInfo.room = node.room as string | undefined;
612
680
  nodeInfo.namespace = node.namespace as string | undefined;
613
681
  nodeInfo.broadcast = node.broadcast as boolean | undefined;
@@ -615,13 +683,13 @@ async function findNodes(
615
683
  }
616
684
 
617
685
  // Include listener-specific fields
618
- if (nodeType === 'socketio:on') {
686
+ if (nodeType === SOCKETIO_ON_TYPE) {
619
687
  nodeInfo.objectName = node.objectName as string | undefined;
620
688
  nodeInfo.handlerName = node.handlerName as string | undefined;
621
689
  }
622
690
 
623
691
  // Include plugin-specific fields
624
- if (nodeType === 'grafema:plugin') {
692
+ if (nodeType === GRAFEMA_PLUGIN_TYPE) {
625
693
  nodeInfo.phase = node.phase as string | undefined;
626
694
  nodeInfo.priority = node.priority as number | undefined;
627
695
  nodeInfo.builtin = node.builtin as boolean | undefined;
@@ -631,7 +699,7 @@ async function findNodes(
631
699
  }
632
700
 
633
701
  // Include objectName for PROPERTY_ACCESS nodes (REG-395)
634
- if (nodeType === 'PROPERTY_ACCESS') {
702
+ if (nodeType === PROPERTY_ACCESS_TYPE) {
635
703
  nodeInfo.objectName = node.objectName as string | undefined;
636
704
  }
637
705
 
@@ -741,31 +809,31 @@ async function getCallees(
741
809
  */
742
810
  async function displayNode(node: NodeInfo, projectPath: string, backend: RFDBServerBackend): Promise<void> {
743
811
  // Special formatting for HTTP routes
744
- if (node.type === 'http:route' && node.method && node.path) {
812
+ if (node.type === HTTP_ROUTE_TYPE && node.method && node.path) {
745
813
  console.log(formatHttpRouteDisplay(node, projectPath));
746
814
  return;
747
815
  }
748
816
 
749
817
  // Special formatting for HTTP requests
750
- if (node.type === 'http:request') {
818
+ if (node.type === HTTP_REQUEST_TYPE) {
751
819
  console.log(formatHttpRequestDisplay(node, projectPath));
752
820
  return;
753
821
  }
754
822
 
755
823
  // Special formatting for Socket.IO event channels
756
- if (node.type === 'socketio:event') {
824
+ if (node.type === SOCKETIO_EVENT_TYPE) {
757
825
  console.log(await formatSocketEventDisplay(node, projectPath, backend));
758
826
  return;
759
827
  }
760
828
 
761
829
  // Special formatting for Socket.IO emit/on
762
- if (node.type === 'socketio:emit' || node.type === 'socketio:on') {
830
+ if (node.type === SOCKETIO_EMIT_TYPE || node.type === SOCKETIO_ON_TYPE) {
763
831
  console.log(formatSocketIONodeDisplay(node, projectPath));
764
832
  return;
765
833
  }
766
834
 
767
835
  // Special formatting for Grafema plugin nodes
768
- if (node.type === 'grafema:plugin') {
836
+ if (node.type === GRAFEMA_PLUGIN_TYPE) {
769
837
  console.log(formatPluginDisplay(node, projectPath));
770
838
  return;
771
839
  }
@@ -793,7 +861,7 @@ function formatHttpRouteDisplay(node: NodeInfo, projectPath: string): string {
793
861
 
794
862
  // Line 2: Location
795
863
  if (node.file) {
796
- const relPath = relative(projectPath, node.file);
864
+ const relPath = toRelativeDisplay(node.file, projectPath);
797
865
  const loc = node.line ? `${relPath}:${node.line}` : relPath;
798
866
  lines.push(` Location: ${loc}`);
799
867
  }
@@ -818,7 +886,7 @@ function formatHttpRequestDisplay(node: NodeInfo, projectPath: string): string {
818
886
 
819
887
  // Line 2: Location
820
888
  if (node.file) {
821
- const relPath = relative(projectPath, node.file);
889
+ const relPath = toRelativeDisplay(node.file, projectPath);
822
890
  const loc = node.line ? `${relPath}:${node.line}` : relPath;
823
891
  lines.push(` Location: ${loc}`);
824
892
  }
@@ -905,7 +973,7 @@ function formatSocketIONodeDisplay(node: NodeInfo, projectPath: string): string
905
973
  }
906
974
 
907
975
  // Emit-specific fields
908
- if (node.type === 'socketio:emit') {
976
+ if (node.type === SOCKETIO_EMIT_TYPE) {
909
977
  if (node.room) {
910
978
  lines.push(` Room: ${node.room}`);
911
979
  }
@@ -918,7 +986,7 @@ function formatSocketIONodeDisplay(node: NodeInfo, projectPath: string): string
918
986
  }
919
987
 
920
988
  // Listener-specific fields
921
- if (node.type === 'socketio:on' && node.handlerName) {
989
+ if (node.type === SOCKETIO_ON_TYPE && node.handlerName) {
922
990
  lines.push(` Handler: ${node.handlerName}`);
923
991
  }
924
992
 
@@ -959,26 +1027,63 @@ function formatPluginDisplay(node: NodeInfo, projectPath: string): string {
959
1027
  }
960
1028
 
961
1029
  if (node.file) {
962
- const relPath = relative(projectPath, node.file);
1030
+ const relPath = toRelativeDisplay(node.file, projectPath);
963
1031
  lines.push(` Source: ${relPath}`);
964
1032
  }
965
1033
 
966
1034
  return lines.join('\n');
967
1035
  }
968
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
+
969
1073
  /**
970
- * Execute raw Datalog query (backwards compat)
1074
+ * Execute raw Datalog query.
1075
+ * Uses unified executeDatalog endpoint which auto-detects rules vs direct queries.
971
1076
  */
972
1077
  async function executeRawQuery(
973
1078
  backend: RFDBServerBackend,
974
1079
  query: string,
975
- options: QueryOptions
1080
+ limit: number,
1081
+ json?: boolean
976
1082
  ): Promise<void> {
977
- const results = await backend.datalogQuery(query);
978
- const limit = parseInt(options.limit, 10);
1083
+ const results = await backend.executeDatalog(query);
979
1084
  const limited = results.slice(0, limit);
980
1085
 
981
- if (options.json) {
1086
+ if (json) {
982
1087
  console.log(JSON.stringify(limited, null, 2));
983
1088
  } else {
984
1089
  if (limited.length === 0) {
@@ -992,4 +1097,14 @@ async function executeRawQuery(
992
1097
  }
993
1098
  }
994
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
+ }
995
1110
  }
@@ -10,7 +10,8 @@
10
10
  */
11
11
 
12
12
  import { Command } from 'commander';
13
- import { resolve, join, relative } from 'path';
13
+ import { resolve, join } from 'path';
14
+ import { toRelativeDisplay } from '../utils/pathUtils.js';
14
15
  import { existsSync, writeFileSync } from 'fs';
15
16
  import {
16
17
  RFDBServerBackend,
@@ -82,7 +83,7 @@ function formatInterfaceYaml(schema: InterfaceSchema): string {
82
83
 
83
84
  function formatInterfaceMarkdown(schema: InterfaceSchema, projectPath: string): string {
84
85
  const lines: string[] = [];
85
- const relPath = relative(projectPath, schema.source.file);
86
+ const relPath = toRelativeDisplay(schema.source.file, projectPath);
86
87
 
87
88
  lines.push(`# Interface: ${schema.name}`);
88
89
  lines.push('');
@@ -9,84 +9,28 @@
9
9
  */
10
10
 
11
11
  import { Command } from 'commander';
12
- import { resolve, join, dirname } from 'path';
12
+ import { resolve, join } from 'path';
13
13
  import { existsSync, unlinkSync, writeFileSync, readFileSync } from 'fs';
14
14
  import { spawn } from 'child_process';
15
- import { fileURLToPath } from 'url';
16
15
  import { setTimeout as sleep } from 'timers/promises';
17
- import { RFDBClient, loadConfig, RFDBServerBackend } from '@grafema/core';
16
+ import { RFDBClient, loadConfig, RFDBServerBackend, findRfdbBinary } from '@grafema/core';
18
17
  import { exitWithError } from '../utils/errorFormatter.js';
19
18
 
20
- const __filename = fileURLToPath(import.meta.url);
21
- const __dirname = dirname(__filename);
22
-
23
19
  // Extend config type for server settings
24
20
  interface ServerConfig {
25
21
  binaryPath?: string;
26
22
  }
27
23
 
28
24
  /**
29
- * Find RFDB server binary in order of preference:
30
- * 1. Explicit path (from --binary flag or config)
31
- * 2. Monorepo development (target/release, target/debug)
32
- * 3. @grafema/rfdb npm package (prebuilt binaries)
33
- * 4. ~/.local/bin/rfdb-server (user-installed)
25
+ * Find RFDB server binary using shared utility.
26
+ * Wraps findRfdbBinary() with CLI-specific error logging.
34
27
  */
35
28
  function findServerBinary(explicitPath?: string): string | null {
36
- // 1. Explicit path from --binary flag or config
37
- if (explicitPath) {
38
- const resolved = resolve(explicitPath);
39
- if (existsSync(resolved)) {
40
- return resolved;
41
- }
42
- // Explicit path specified but not found - this is an error
43
- console.error(`Specified binary not found: ${resolved}`);
44
- return null;
29
+ const binaryPath = findRfdbBinary({ explicitPath });
30
+ if (!binaryPath && explicitPath) {
31
+ console.error(`Specified binary not found: ${explicitPath}`);
45
32
  }
46
-
47
- // 2. Check packages/rfdb-server in monorepo (for development)
48
- const projectRoot = join(__dirname, '../../../..');
49
- const releaseBinary = join(projectRoot, 'packages/rfdb-server/target/release/rfdb-server');
50
- if (existsSync(releaseBinary)) {
51
- return releaseBinary;
52
- }
53
-
54
- const debugBinary = join(projectRoot, 'packages/rfdb-server/target/debug/rfdb-server');
55
- if (existsSync(debugBinary)) {
56
- return debugBinary;
57
- }
58
-
59
- // 3. Check @grafema/rfdb npm package
60
- try {
61
- const rfdbPkg = require.resolve('@grafema/rfdb');
62
- const rfdbDir = dirname(rfdbPkg);
63
- const platform = process.platform;
64
- const arch = process.arch;
65
-
66
- let platformDir: string;
67
- if (platform === 'darwin') {
68
- platformDir = arch === 'arm64' ? 'darwin-arm64' : 'darwin-x64';
69
- } else if (platform === 'linux') {
70
- platformDir = arch === 'arm64' ? 'linux-arm64' : 'linux-x64';
71
- } else {
72
- platformDir = `${platform}-${arch}`;
73
- }
74
-
75
- const npmBinary = join(rfdbDir, 'prebuilt', platformDir, 'rfdb-server');
76
- if (existsSync(npmBinary)) {
77
- return npmBinary;
78
- }
79
- } catch {
80
- // @grafema/rfdb not installed
81
- }
82
-
83
- // 4. Check ~/.local/bin (user-installed binary for unsupported platforms)
84
- const homeBinary = join(process.env.HOME || '', '.local', 'bin', 'rfdb-server');
85
- if (existsSync(homeBinary)) {
86
- return homeBinary;
87
- }
88
-
89
- return null;
33
+ return binaryPath;
90
34
  }
91
35
 
92
36
  /**