@grafema/cli 0.2.12-beta → 0.3.1-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 (131) hide show
  1. package/dist/cli.js +13 -0
  2. package/dist/cli.js.map +1 -1
  3. package/dist/commands/analyze.d.ts.map +1 -1
  4. package/dist/commands/analyze.js +2 -4
  5. package/dist/commands/analyze.js.map +1 -1
  6. package/dist/commands/analyzeAction.d.ts +5 -3
  7. package/dist/commands/analyzeAction.d.ts.map +1 -1
  8. package/dist/commands/analyzeAction.js +109 -151
  9. package/dist/commands/analyzeAction.js.map +1 -1
  10. package/dist/commands/check.d.ts +1 -1
  11. package/dist/commands/check.js +4 -4
  12. package/dist/commands/check.js.map +1 -1
  13. package/dist/commands/context.js +2 -2
  14. package/dist/commands/context.js.map +1 -1
  15. package/dist/commands/coverage.js +2 -2
  16. package/dist/commands/coverage.js.map +1 -1
  17. package/dist/commands/describe.d.ts +13 -0
  18. package/dist/commands/describe.d.ts.map +1 -0
  19. package/dist/commands/describe.js +131 -0
  20. package/dist/commands/describe.js.map +1 -0
  21. package/dist/commands/doctor/checks.d.ts +6 -1
  22. package/dist/commands/doctor/checks.d.ts.map +1 -1
  23. package/dist/commands/doctor/checks.js +128 -13
  24. package/dist/commands/doctor/checks.js.map +1 -1
  25. package/dist/commands/doctor.d.ts +10 -9
  26. package/dist/commands/doctor.d.ts.map +1 -1
  27. package/dist/commands/doctor.js +12 -10
  28. package/dist/commands/doctor.js.map +1 -1
  29. package/dist/commands/explain.js +2 -2
  30. package/dist/commands/explain.js.map +1 -1
  31. package/dist/commands/file.js +2 -2
  32. package/dist/commands/file.js.map +1 -1
  33. package/dist/commands/get.js +2 -2
  34. package/dist/commands/get.js.map +1 -1
  35. package/dist/commands/git-ingest.d.ts +6 -0
  36. package/dist/commands/git-ingest.d.ts.map +1 -0
  37. package/dist/commands/git-ingest.js +46 -0
  38. package/dist/commands/git-ingest.js.map +1 -0
  39. package/dist/commands/impact.d.ts.map +1 -1
  40. package/dist/commands/impact.js +276 -50
  41. package/dist/commands/impact.js.map +1 -1
  42. package/dist/commands/init.d.ts.map +1 -1
  43. package/dist/commands/init.js +20 -22
  44. package/dist/commands/init.js.map +1 -1
  45. package/dist/commands/ls.js +2 -2
  46. package/dist/commands/ls.js.map +1 -1
  47. package/dist/commands/overview.js +2 -2
  48. package/dist/commands/overview.js.map +1 -1
  49. package/dist/commands/query.d.ts +1 -1
  50. package/dist/commands/query.d.ts.map +1 -1
  51. package/dist/commands/query.js +169 -7
  52. package/dist/commands/query.js.map +1 -1
  53. package/dist/commands/schema.js +2 -2
  54. package/dist/commands/schema.js.map +1 -1
  55. package/dist/commands/server.js +11 -6
  56. package/dist/commands/server.js.map +1 -1
  57. package/dist/commands/stats.js +2 -2
  58. package/dist/commands/stats.js.map +1 -1
  59. package/dist/commands/tldr.d.ts +12 -0
  60. package/dist/commands/tldr.d.ts.map +1 -0
  61. package/dist/commands/tldr.js +81 -0
  62. package/dist/commands/tldr.js.map +1 -0
  63. package/dist/commands/trace.d.ts +1 -1
  64. package/dist/commands/trace.d.ts.map +1 -1
  65. package/dist/commands/trace.js +17 -133
  66. package/dist/commands/trace.js.map +1 -1
  67. package/dist/commands/types.js +2 -2
  68. package/dist/commands/types.js.map +1 -1
  69. package/dist/commands/who.d.ts +12 -0
  70. package/dist/commands/who.d.ts.map +1 -0
  71. package/dist/commands/who.js +184 -0
  72. package/dist/commands/who.js.map +1 -0
  73. package/dist/commands/why.d.ts +12 -0
  74. package/dist/commands/why.d.ts.map +1 -0
  75. package/dist/commands/why.js +118 -0
  76. package/dist/commands/why.js.map +1 -0
  77. package/dist/commands/wtf.d.ts +12 -0
  78. package/dist/commands/wtf.d.ts.map +1 -0
  79. package/dist/commands/wtf.js +117 -0
  80. package/dist/commands/wtf.js.map +1 -0
  81. package/dist/plugins/builtinPlugins.d.ts +1 -9
  82. package/dist/plugins/builtinPlugins.d.ts.map +1 -1
  83. package/dist/plugins/builtinPlugins.js +2 -67
  84. package/dist/plugins/builtinPlugins.js.map +1 -1
  85. package/dist/plugins/pluginLoader.d.ts +1 -15
  86. package/dist/plugins/pluginLoader.d.ts.map +1 -1
  87. package/dist/plugins/pluginLoader.js +2 -100
  88. package/dist/plugins/pluginLoader.js.map +1 -1
  89. package/dist/plugins/pluginResolver.js +3 -3
  90. package/dist/utils/progressRenderer.d.ts +15 -1
  91. package/dist/utils/progressRenderer.d.ts.map +1 -1
  92. package/dist/utils/progressRenderer.js.map +1 -1
  93. package/dist/utils/queryHints.d.ts +6 -0
  94. package/dist/utils/queryHints.d.ts.map +1 -0
  95. package/dist/utils/queryHints.js +36 -0
  96. package/dist/utils/queryHints.js.map +1 -0
  97. package/package.json +4 -4
  98. package/skills/grafema-codebase-analysis/SKILL.md +1 -1
  99. package/src/cli.ts +14 -0
  100. package/src/commands/analyze.ts +2 -4
  101. package/src/commands/analyzeAction.ts +122 -168
  102. package/src/commands/check.ts +5 -5
  103. package/src/commands/context.ts +3 -3
  104. package/src/commands/coverage.ts +2 -2
  105. package/src/commands/describe.ts +160 -0
  106. package/src/commands/doctor/checks.ts +153 -10
  107. package/src/commands/doctor.ts +13 -9
  108. package/src/commands/explain.ts +2 -2
  109. package/src/commands/explore.tsx +2 -2
  110. package/src/commands/file.ts +3 -3
  111. package/src/commands/get.ts +2 -2
  112. package/src/commands/git-ingest.ts +49 -0
  113. package/src/commands/impact.ts +318 -55
  114. package/src/commands/init.ts +20 -22
  115. package/src/commands/ls.ts +2 -2
  116. package/src/commands/overview.ts +2 -2
  117. package/src/commands/query.ts +197 -7
  118. package/src/commands/schema.ts +2 -2
  119. package/src/commands/server.ts +12 -6
  120. package/src/commands/stats.ts +2 -2
  121. package/src/commands/tldr.ts +103 -0
  122. package/src/commands/trace.ts +19 -161
  123. package/src/commands/types.ts +2 -2
  124. package/src/commands/who.ts +215 -0
  125. package/src/commands/why.ts +134 -0
  126. package/src/commands/wtf.ts +140 -0
  127. package/src/plugins/builtinPlugins.ts +1 -108
  128. package/src/plugins/pluginLoader.ts +1 -123
  129. package/src/plugins/pluginResolver.js +3 -3
  130. package/src/utils/progressRenderer.ts +15 -1
  131. package/src/utils/queryHints.ts +46 -0
@@ -13,10 +13,12 @@ import { Command } from 'commander';
13
13
  import { resolve, join, basename } from 'path';
14
14
  import { toRelativeDisplay } from '../utils/pathUtils.js';
15
15
  import { existsSync } from 'fs';
16
- import { RFDBServerBackend, parseSemanticId, parseSemanticIdV2, findCallsInFunction as findCallsInFunctionCore, findContainingFunction as findContainingFunctionCore } from '@grafema/core';
16
+ import { RFDBServerBackend, parseSemanticId, parseSemanticIdV2, findCallsInFunction as findCallsInFunctionCore, findContainingFunction as findContainingFunctionCore } from '@grafema/util';
17
17
  import { formatNodeDisplay, formatNodeInline, formatLocation } from '../utils/formatNode.js';
18
18
  import { exitWithError } from '../utils/errorFormatter.js';
19
19
  import { Spinner } from '../utils/spinner.js';
20
+ import { extractQueriedTypes, findSimilarTypes } from '../utils/queryHints.js';
21
+ import type { DatalogExplainResult, CypherResult } from '@grafema/types';
20
22
 
21
23
  // Node type constants to avoid magic string duplication
22
24
  const HTTP_ROUTE_TYPE = 'http:route';
@@ -32,6 +34,8 @@ interface QueryOptions {
32
34
  json?: boolean;
33
35
  limit: string;
34
36
  raw?: boolean;
37
+ cypher?: boolean;
38
+ explain?: boolean;
35
39
  type?: string; // Explicit node type (bypasses type aliases)
36
40
  }
37
41
 
@@ -106,6 +110,26 @@ Rules (must define violation/1):
106
110
  grafema query --raw 'violation(X) :- node(X, "FUNCTION").'
107
111
  grafema query --raw 'violation(X) :- node(X, "http:route"), attr(X, "method", "POST").'`
108
112
  )
113
+ .option(
114
+ '--cypher',
115
+ `Execute a Cypher query instead of Datalog
116
+
117
+ Cypher is a graph query language with pattern-matching syntax.
118
+
119
+ Examples:
120
+ grafema query --cypher 'MATCH (n:FUNCTION) RETURN n.name LIMIT 10'
121
+ grafema query --cypher 'MATCH (a)-[:CALLS]->(b) RETURN a.name, b.name'`
122
+ )
123
+ .option(
124
+ '--explain',
125
+ `Show step-by-step query execution (use with --raw)
126
+
127
+ Displays each predicate evaluation, result counts, and timing.
128
+ Useful when a query returns no results — shows where the funnel drops to zero.
129
+
130
+ Example:
131
+ grafema query --raw 'type(X, "FUNCTION"), attr(X, "name", "main")' --explain`
132
+ )
109
133
  .option(
110
134
  '-t, --type <nodeType>',
111
135
  `Filter by exact node type (bypasses type aliases)
@@ -135,6 +159,7 @@ Examples:
135
159
  grafema query --type FUNCTION "auth" Explicit type (no alias resolution)
136
160
  grafema query -t http:request "/api" Search custom node types
137
161
  grafema query --raw 'type(X, "FUNCTION")' Raw Datalog query
162
+ grafema query --cypher 'MATCH (n:FUNCTION) RETURN n.name' Cypher query
138
163
  `)
139
164
  .action(async (pattern: string, options: QueryOptions) => {
140
165
  const projectPath = resolve(options.project);
@@ -145,7 +170,7 @@ Examples:
145
170
  exitWithError('No graph database found', ['Run: grafema analyze']);
146
171
  }
147
172
 
148
- const backend = new RFDBServerBackend({ dbPath });
173
+ const backend = new RFDBServerBackend({ dbPath, clientName: 'cli' });
149
174
  await backend.connect();
150
175
 
151
176
  const spinner = new Spinner('Querying graph...');
@@ -158,10 +183,23 @@ Examples:
158
183
  exitWithError('Invalid limit', ['Use a positive number, e.g.: --limit 10']);
159
184
  }
160
185
 
186
+ // --explain only works with --raw
187
+ if (options.explain && !options.raw) {
188
+ spinner.stop();
189
+ console.error('Note: --explain requires --raw. Ignoring --explain.');
190
+ }
191
+
192
+ // Cypher mode
193
+ if (options.cypher) {
194
+ spinner.stop();
195
+ await executeCypherQuery(backend, pattern, limit, options.json);
196
+ return;
197
+ }
198
+
161
199
  // Raw Datalog mode
162
200
  if (options.raw) {
163
201
  spinner.stop();
164
- await executeRawQuery(backend, pattern, limit, options.json);
202
+ await executeRawQuery(backend, pattern, limit, options.json, options.explain);
165
203
  return;
166
204
  }
167
205
 
@@ -373,7 +411,7 @@ export function isFileScope(scope: string): boolean {
373
411
  /**
374
412
  * Check if a semantic ID matches the given scope constraints.
375
413
  *
376
- * Uses parseSemanticId from @grafema/core for robust ID parsing.
414
+ * Uses parseSemanticId from @grafema/util for robust ID parsing.
377
415
  *
378
416
  * Scope matching rules:
379
417
  * - File scope: semantic ID must match the file path (full or basename)
@@ -737,7 +775,7 @@ async function getCallers(
737
775
  const callNode = await backend.getNode(edge.src);
738
776
  if (!callNode) continue;
739
777
 
740
- // Find the FUNCTION that contains this CALL (use shared utility from @grafema/core)
778
+ // Find the FUNCTION that contains this CALL (use shared utility from @grafema/util)
741
779
  const containingFunc = await findContainingFunctionCore(backend, callNode.id);
742
780
 
743
781
  if (containingFunc && !seen.has(containingFunc.id)) {
@@ -763,7 +801,7 @@ async function getCallers(
763
801
  /**
764
802
  * Get functions that this node calls
765
803
  *
766
- * Uses shared utility from @grafema/core which:
804
+ * Uses shared utility from @grafema/util which:
767
805
  * - Follows HAS_SCOPE -> SCOPE -> CONTAINS pattern correctly
768
806
  * - Finds both CALL and METHOD_CALL nodes
769
807
  * - Only returns resolved calls (those with CALLS edges to targets)
@@ -1070,6 +1108,58 @@ export function getUnknownPredicates(query: string): string[] {
1070
1108
  return predicates.filter(p => !BUILTIN_PREDICATES.has(p) && !ruleHeads.has(p));
1071
1109
  }
1072
1110
 
1111
+ /**
1112
+ * Execute Cypher query and display results in tabular format.
1113
+ */
1114
+ async function executeCypherQuery(
1115
+ backend: RFDBServerBackend,
1116
+ query: string,
1117
+ limit: number,
1118
+ json?: boolean,
1119
+ ): Promise<void> {
1120
+ const result: CypherResult = await backend.cypherQuery(query);
1121
+
1122
+ if (json) {
1123
+ console.log(JSON.stringify(result, null, 2));
1124
+ return;
1125
+ }
1126
+
1127
+ if (result.rowCount === 0) {
1128
+ console.log('No results.');
1129
+ return;
1130
+ }
1131
+
1132
+ const limited = result.rows.slice(0, limit);
1133
+
1134
+ // Calculate column widths for tabular display
1135
+ const colWidths = result.columns.map((col, i) => {
1136
+ let maxWidth = col.length;
1137
+ for (const row of limited) {
1138
+ const cellLen = String(row[i] ?? '').length;
1139
+ if (cellLen > maxWidth) maxWidth = cellLen;
1140
+ }
1141
+ return Math.min(maxWidth, 60); // cap at 60 chars
1142
+ });
1143
+
1144
+ // Header
1145
+ const header = result.columns.map((col, i) => col.padEnd(colWidths[i])).join(' ');
1146
+ const separator = colWidths.map(w => '-'.repeat(w)).join(' ');
1147
+ console.log(header);
1148
+ console.log(separator);
1149
+
1150
+ // Rows
1151
+ for (const row of limited) {
1152
+ const line = row.map((cell, i) => {
1153
+ const s = String(cell ?? '');
1154
+ return s.length > colWidths[i] ? s.slice(0, colWidths[i] - 1) + '\u2026' : s.padEnd(colWidths[i]);
1155
+ }).join(' ');
1156
+ console.log(line);
1157
+ }
1158
+
1159
+ console.log('');
1160
+ console.log(`${limited.length}${result.rowCount > limit ? ` of ${result.rowCount}` : ''} row(s)`);
1161
+ }
1162
+
1073
1163
  /**
1074
1164
  * Execute raw Datalog query.
1075
1165
  * Uses unified executeDatalog endpoint which auto-detects rules vs direct queries.
@@ -1078,8 +1168,19 @@ async function executeRawQuery(
1078
1168
  backend: RFDBServerBackend,
1079
1169
  query: string,
1080
1170
  limit: number,
1081
- json?: boolean
1171
+ json?: boolean,
1172
+ explain?: boolean,
1082
1173
  ): Promise<void> {
1174
+ if (explain) {
1175
+ const result = await backend.executeDatalog(query, true);
1176
+ if (json) {
1177
+ console.log(JSON.stringify(result, null, 2));
1178
+ return;
1179
+ }
1180
+ renderExplainOutput(result, limit);
1181
+ return;
1182
+ }
1183
+
1083
1184
  const results = await backend.executeDatalog(query);
1084
1185
  const limited = results.slice(0, limit);
1085
1186
 
@@ -1106,5 +1207,94 @@ async function executeRawQuery(
1106
1207
  const builtinList = [...BUILTIN_PREDICATES].join(', ');
1107
1208
  console.error(`Note: unknown predicate ${unknownList}. Built-in predicates: ${builtinList}`);
1108
1209
  }
1210
+
1211
+ // Type suggestions: only if there are type literals in the query
1212
+ const { nodeTypes, edgeTypes } = extractQueriedTypes(query);
1213
+ if (nodeTypes.length > 0 || edgeTypes.length > 0) {
1214
+ const nodeCounts = nodeTypes.length > 0 ? await backend.countNodesByType() : {};
1215
+ const edgeCounts = edgeTypes.length > 0 ? await backend.countEdgesByType() : {};
1216
+ const availableNodeTypes = Object.keys(nodeCounts);
1217
+ const availableEdgeTypes = Object.keys(edgeCounts);
1218
+
1219
+ if (nodeTypes.length > 0 && availableNodeTypes.length === 0) {
1220
+ console.error('Note: graph has no nodes');
1221
+ } else {
1222
+ for (const queriedType of nodeTypes) {
1223
+ if (!nodeCounts[queriedType]) {
1224
+ const similar = findSimilarTypes(queriedType, availableNodeTypes);
1225
+ if (similar.length > 0) {
1226
+ console.error(`Note: unknown node type "${queriedType}". Did you mean: ${similar.join(', ')}?`);
1227
+ } else {
1228
+ const typeList = availableNodeTypes.slice(0, 10).join(', ');
1229
+ const more = availableNodeTypes.length > 10 ? '...' : '';
1230
+ console.error(`Note: unknown node type "${queriedType}". Available: ${typeList}${more}`);
1231
+ }
1232
+ }
1233
+ }
1234
+ }
1235
+
1236
+ if (edgeTypes.length > 0 && availableEdgeTypes.length === 0) {
1237
+ console.error('Note: graph has no edges');
1238
+ } else {
1239
+ for (const queriedType of edgeTypes) {
1240
+ if (!edgeCounts[queriedType]) {
1241
+ const similar = findSimilarTypes(queriedType, availableEdgeTypes);
1242
+ if (similar.length > 0) {
1243
+ console.error(`Note: unknown edge type "${queriedType}". Did you mean: ${similar.join(', ')}?`);
1244
+ } else {
1245
+ const typeList = availableEdgeTypes.slice(0, 10).join(', ');
1246
+ const more = availableEdgeTypes.length > 10 ? '...' : '';
1247
+ console.error(`Note: unknown edge type "${queriedType}". Available: ${typeList}${more}`);
1248
+ }
1249
+ }
1250
+ }
1251
+ }
1252
+ }
1253
+ }
1254
+ }
1255
+
1256
+ function renderExplainOutput(result: DatalogExplainResult, limit: number): void {
1257
+ // Print warnings to stderr first so they're immediately visible
1258
+ if (result.warnings && result.warnings.length > 0) {
1259
+ console.error('Warnings:');
1260
+ for (const warning of result.warnings) {
1261
+ console.error(` ${warning}`);
1262
+ }
1263
+ console.error('');
1264
+ }
1265
+
1266
+ console.log('Explain mode \u2014 step-by-step execution:\n');
1267
+
1268
+ for (const step of result.explainSteps) {
1269
+ const args = step.args.join(', ');
1270
+ console.log(` Step ${step.step}: [${step.operation}] ${step.predicate}(${args})`);
1271
+ console.log(` \u2192 ${step.resultCount} result(s) in ${step.durationUs} \u00b5s`);
1272
+ if (step.details) {
1273
+ console.log(` ${step.details}`);
1274
+ }
1275
+ console.log('');
1276
+ }
1277
+
1278
+ console.log('Query statistics:');
1279
+ console.log(` Nodes visited: ${result.stats.nodesVisited}`);
1280
+ console.log(` Edges traversed: ${result.stats.edgesTraversed}`);
1281
+ console.log(` Rule evaluations: ${result.stats.ruleEvaluations}`);
1282
+ console.log(` Total results: ${result.stats.totalResults}`);
1283
+ console.log(` Total duration: ${result.profile.totalDurationUs} \u00b5s`);
1284
+ if (result.profile.ruleEvalTimeUs === 0 && result.profile.projectionTimeUs === 0) {
1285
+ console.log(' (rule_eval_time and projection_time: not yet tracked)');
1286
+ }
1287
+ console.log('');
1288
+
1289
+ const bindingsToShow = result.bindings.slice(0, limit);
1290
+ if (bindingsToShow.length === 0) {
1291
+ console.log('No results.');
1292
+ } else {
1293
+ console.log(`Results (${bindingsToShow.length}${result.bindings.length > limit ? ` of ${result.bindings.length}` : ''}):`);
1294
+ console.log('');
1295
+ for (const row of bindingsToShow) {
1296
+ const pairs = Object.entries(row).map(([k, v]) => `${k}=${v}`).join(', ');
1297
+ console.log(` { ${pairs} }`);
1298
+ }
1109
1299
  }
1110
1300
  }
@@ -21,7 +21,7 @@ import {
21
21
  type GraphSchema,
22
22
  type NodeTypeSchema,
23
23
  type EdgeTypeSchema,
24
- } from '@grafema/core';
24
+ } from '@grafema/util';
25
25
  import { exitWithError } from '../utils/errorFormatter.js';
26
26
 
27
27
  interface ExportOptions {
@@ -262,7 +262,7 @@ const exportSubcommand = new Command('export')
262
262
  exitWithError('No graph database found', ['Run: grafema analyze']);
263
263
  }
264
264
 
265
- const backend = new RFDBServerBackend({ dbPath });
265
+ const backend = new RFDBServerBackend({ dbPath, clientName: 'cli' });
266
266
  await backend.connect();
267
267
 
268
268
  try {
@@ -12,7 +12,7 @@ import { Command } from 'commander';
12
12
  import { resolve, join } from 'path';
13
13
  import { existsSync, unlinkSync, readFileSync } from 'fs';
14
14
  import { setTimeout as sleep } from 'timers/promises';
15
- import { RFDBClient, loadConfig, RFDBServerBackend, findRfdbBinary, startRfdbServer } from '@grafema/core';
15
+ import { RFDBClient, loadConfig, RFDBServerBackend, findRfdbBinary, startRfdbServer } from '@grafema/util';
16
16
  import { exitWithError } from '../utils/errorFormatter.js';
17
17
 
18
18
  // Extend config type for server settings
@@ -40,7 +40,7 @@ async function isServerRunning(socketPath: string): Promise<{ running: boolean;
40
40
  return { running: false };
41
41
  }
42
42
 
43
- const client = new RFDBClient(socketPath);
43
+ const client = new RFDBClient(socketPath, 'cli');
44
44
  // Suppress error events (we handle via try/catch)
45
45
  client.on('error', () => {});
46
46
 
@@ -92,7 +92,7 @@ function resolveBinaryPath(projectPath: string, explicitBinary?: string): string
92
92
  * Stop a running RFDB server: send shutdown, wait for socket removal, clean PID
93
93
  */
94
94
  async function stopRunningServer(socketPath: string, pidPath: string): Promise<void> {
95
- const client = new RFDBClient(socketPath);
95
+ const client = new RFDBClient(socketPath, 'cli');
96
96
  client.on('error', () => {});
97
97
 
98
98
  try {
@@ -186,6 +186,12 @@ serverCommand
186
186
  waitTimeoutMs: 10000,
187
187
  });
188
188
 
189
+ if (serverProcess === null) {
190
+ // Existing server detected via PID file
191
+ console.log('Server already running (detected via PID file)');
192
+ return;
193
+ }
194
+
189
195
  // Verify server is responsive
190
196
  const verifyStatus = await isServerRunning(socketPath);
191
197
  if (!verifyStatus.running) {
@@ -262,7 +268,7 @@ serverCommand
262
268
  let nodeCount: number | undefined;
263
269
  let edgeCount: number | undefined;
264
270
  if (status.running) {
265
- const client = new RFDBClient(socketPath);
271
+ const client = new RFDBClient(socketPath, 'cli');
266
272
  client.on('error', () => {}); // Suppress error events
267
273
 
268
274
  try {
@@ -382,7 +388,7 @@ serverCommand
382
388
  if (verifyStatus.version) {
383
389
  console.log(` Version: ${verifyStatus.version}`);
384
390
  }
385
- if (serverProcess.pid) {
391
+ if (serverProcess?.pid) {
386
392
  console.log(` PID: ${serverProcess.pid}`);
387
393
  }
388
394
  });
@@ -408,7 +414,7 @@ serverCommand
408
414
  }
409
415
 
410
416
  // Create backend connection
411
- const backend = new RFDBServerBackend({ socketPath });
417
+ const backend = new RFDBServerBackend({ socketPath, clientName: 'cli' });
412
418
  await backend.connect();
413
419
 
414
420
  // Import and start GraphQL server
@@ -5,7 +5,7 @@
5
5
  import { Command } from 'commander';
6
6
  import { resolve, join } from 'path';
7
7
  import { existsSync } from 'fs';
8
- import { RFDBServerBackend } from '@grafema/core';
8
+ import { RFDBServerBackend } from '@grafema/util';
9
9
  import { exitWithError } from '../utils/errorFormatter.js';
10
10
 
11
11
  export const statsCommand = new Command('stats')
@@ -29,7 +29,7 @@ Examples:
29
29
  exitWithError('No graph database found', ['Run: grafema analyze']);
30
30
  }
31
31
 
32
- const backend = new RFDBServerBackend({ dbPath });
32
+ const backend = new RFDBServerBackend({ dbPath, clientName: 'cli' });
33
33
  await backend.connect();
34
34
 
35
35
  try {
@@ -0,0 +1,103 @@
1
+ /**
2
+ * tldr command — "What's in this file?"
3
+ *
4
+ * Human-first alias for `describe`. Shows notation DSL for a file.
5
+ *
6
+ * Usage:
7
+ * grafema tldr src/auth.ts # File overview in DSL
8
+ * grafema tldr src/auth.ts --save # Save as src/auth.ts.tldr
9
+ */
10
+
11
+ import { Command } from 'commander';
12
+ import { resolve, join } from 'path';
13
+ import { existsSync, writeFileSync } from 'fs';
14
+ import {
15
+ RFDBServerBackend,
16
+ renderNotation,
17
+ extractSubgraph,
18
+ } from '@grafema/util';
19
+ import type { DescribeOptions } from '@grafema/util';
20
+ import { exitWithError } from '../utils/errorFormatter.js';
21
+ import { Spinner } from '../utils/spinner.js';
22
+
23
+ interface TldrCommandOptions {
24
+ project: string;
25
+ save?: boolean;
26
+ ext: string;
27
+ }
28
+
29
+ export const tldrCommand = new Command('tldr')
30
+ .description("What's in this file? — compact DSL overview")
31
+ .argument('<file>', 'File path to describe')
32
+ .option('-p, --project <path>', 'Project path', '.')
33
+ .option('-s, --save', 'Save output to <file>.<ext>')
34
+ .option('--ext <ext>', 'File extension for --save', '.tldr')
35
+ .addHelpText('after', `
36
+ Examples:
37
+ grafema tldr src/auth.ts File overview in DSL notation
38
+ grafema tldr src/auth.ts --save Save as src/auth.ts.tldr
39
+ grafema tldr src/app.ts --save --ext .md Save as src/app.ts.md
40
+ `)
41
+ .action(async (file: string, options: TldrCommandOptions) => {
42
+ const projectPath = resolve(options.project);
43
+ const grafemaDir = join(projectPath, '.grafema');
44
+ const dbPath = join(grafemaDir, 'graph.rfdb');
45
+
46
+ if (!existsSync(dbPath)) {
47
+ exitWithError('No graph database found', ['Run: grafema analyze']);
48
+ }
49
+
50
+ const backend = new RFDBServerBackend({ dbPath, clientName: 'cli' });
51
+ await backend.connect();
52
+
53
+ const spinner = new Spinner('Loading file overview...');
54
+ spinner.start();
55
+
56
+ try {
57
+ // Find MODULE node for the file
58
+ let node = null;
59
+ for await (const n of backend.queryNodes({ file, type: 'MODULE' })) {
60
+ node = n;
61
+ break;
62
+ }
63
+
64
+ if (!node) {
65
+ spinner.stop();
66
+ exitWithError(`File not found in graph: "${file}"`, [
67
+ 'Check that the file was included in analysis',
68
+ 'Run: grafema analyze',
69
+ ]);
70
+ return;
71
+ }
72
+
73
+ // Extract subgraph at depth 2 (nested + fold)
74
+ const subgraph = await extractSubgraph(backend, node.id, 2);
75
+
76
+ // Render notation
77
+ const describeOptions: DescribeOptions = {
78
+ depth: 2,
79
+ includeLocations: true,
80
+ };
81
+ const notation = renderNotation(subgraph, describeOptions);
82
+
83
+ spinner.stop();
84
+
85
+ const output = notation.trim()
86
+ ? notation
87
+ : `[${node.type}] ${node.name ?? node.id}\nNo relationships found.`;
88
+
89
+ console.log(output);
90
+
91
+ // Save to file if --save flag
92
+ if (options.save) {
93
+ const ext = options.ext.startsWith('.') ? options.ext : `.${options.ext}`;
94
+ const outputPath = `${file}${ext}`;
95
+ writeFileSync(outputPath, output + '\n', 'utf-8');
96
+ console.log('');
97
+ console.log(`Saved to ${outputPath}`);
98
+ }
99
+ } finally {
100
+ spinner.stop();
101
+ await backend.close();
102
+ }
103
+ });