@grafema/mcp 0.3.28 → 0.3.31

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 (46) hide show
  1. package/dist/definitions/enox-tools.d.ts +10 -0
  2. package/dist/definitions/enox-tools.d.ts.map +1 -0
  3. package/dist/definitions/enox-tools.js +451 -0
  4. package/dist/definitions/enox-tools.js.map +1 -0
  5. package/dist/definitions/index.js +2 -2
  6. package/dist/definitions/index.js.map +1 -1
  7. package/dist/handlers/coverage-handlers.d.ts.map +1 -1
  8. package/dist/handlers/coverage-handlers.js +9 -0
  9. package/dist/handlers/coverage-handlers.js.map +1 -1
  10. package/dist/handlers/documentation-handlers.d.ts.map +1 -1
  11. package/dist/handlers/documentation-handlers.js +26 -5
  12. package/dist/handlers/documentation-handlers.js.map +1 -1
  13. package/dist/handlers/enox-handlers.d.ts +113 -0
  14. package/dist/handlers/enox-handlers.d.ts.map +1 -0
  15. package/dist/handlers/enox-handlers.js +877 -0
  16. package/dist/handlers/enox-handlers.js.map +1 -0
  17. package/dist/handlers/index.d.ts +1 -1
  18. package/dist/handlers/index.d.ts.map +1 -1
  19. package/dist/handlers/index.js +1 -1
  20. package/dist/handlers/index.js.map +1 -1
  21. package/dist/handlers/query-handlers.d.ts +7 -0
  22. package/dist/handlers/query-handlers.d.ts.map +1 -1
  23. package/dist/handlers/query-handlers.js +28 -14
  24. package/dist/handlers/query-handlers.js.map +1 -1
  25. package/dist/server.js +45 -17
  26. package/dist/server.js.map +1 -1
  27. package/dist/types.d.ts +64 -0
  28. package/dist/types.d.ts.map +1 -1
  29. package/package.json +12 -12
  30. package/src/definitions/enox-tools.ts +454 -0
  31. package/src/definitions/index.ts +2 -2
  32. package/src/handlers/coverage-handlers.ts +10 -0
  33. package/src/handlers/documentation-handlers.ts +26 -5
  34. package/src/handlers/enox-handlers.ts +1125 -0
  35. package/src/handlers/index.ts +1 -1
  36. package/src/handlers/query-handlers.ts +25 -12
  37. package/src/server.ts +86 -29
  38. package/src/types.ts +78 -0
  39. package/dist/definitions.d.ts +0 -23
  40. package/dist/definitions.d.ts.map +0 -1
  41. package/dist/definitions.js +0 -644
  42. package/dist/definitions.js.map +0 -1
  43. package/dist/handlers.d.ts +0 -61
  44. package/dist/handlers.d.ts.map +0 -1
  45. package/dist/handlers.js +0 -1310
  46. package/dist/handlers.js.map +0 -1
package/dist/handlers.js DELETED
@@ -1,1310 +0,0 @@
1
- /**
2
- * MCP Tool Handlers
3
- */
4
- import { ensureAnalyzed } from './analysis.js';
5
- import { getProjectPath, getAnalysisStatus, getOrCreateBackend, getGuaranteeManager, getGuaranteeAPI, isAnalysisRunning } from './state.js';
6
- import { CoverageAnalyzer, findCallsInFunction, findContainingFunction, validateServices, validatePatterns, validateWorkspace, getOnboardingInstruction, GRAFEMA_VERSION, getSchemaVersion, FileOverview, buildNodeContext, getNodeDisplayName, formatEdgeMetadata, STRUCTURAL_EDGE_TYPES } from '@grafema/core';
7
- import { existsSync, readFileSync, readdirSync, statSync, writeFileSync, mkdirSync, realpathSync } from 'fs';
8
- import { join, basename, relative } from 'path';
9
- import { stringify as stringifyYAML } from 'yaml';
10
- import { normalizeLimit, formatPaginationInfo, guardResponseSize, serializeBigInt, findSimilarTypes, textResult, errorResult, } from './utils.js';
11
- import { isGuaranteeType } from '@grafema/core';
12
- // === QUERY HANDLERS ===
13
- export async function handleQueryGraph(args) {
14
- const db = await ensureAnalyzed();
15
- const { query, limit: requestedLimit, offset: requestedOffset, format: _format, explain: _explain } = args;
16
- const limit = normalizeLimit(requestedLimit);
17
- const offset = Math.max(0, requestedOffset || 0);
18
- try {
19
- // Check if backend supports Datalog queries
20
- if (!('checkGuarantee' in db)) {
21
- return errorResult('Backend does not support Datalog queries');
22
- }
23
- const checkFn = db.checkGuarantee;
24
- const results = await checkFn(query);
25
- const total = results.length;
26
- if (total === 0) {
27
- const nodeCounts = await db.countNodesByType();
28
- const totalNodes = Object.values(nodeCounts).reduce((a, b) => a + b, 0);
29
- const typeMatch = query.match(/node\([^,]+,\s*"([^"]+)"\)/);
30
- const queriedType = typeMatch ? typeMatch[1] : null;
31
- let hint = '';
32
- if (queriedType && !nodeCounts[queriedType]) {
33
- const availableTypes = Object.keys(nodeCounts);
34
- const similar = findSimilarTypes(queriedType, availableTypes);
35
- if (similar.length > 0) {
36
- hint = `\nšŸ’” Did you mean: ${similar.join(', ')}?`;
37
- }
38
- else {
39
- hint = `\nšŸ’” Available types: ${availableTypes.slice(0, 10).join(', ')}${availableTypes.length > 10 ? '...' : ''}`;
40
- }
41
- }
42
- return textResult(`Query returned no results.${hint}\nšŸ“Š Graph: ${totalNodes.toLocaleString()} nodes`);
43
- }
44
- const paginatedResults = results.slice(offset, offset + limit);
45
- const hasMore = offset + limit < total;
46
- const enrichedResults = [];
47
- for (const result of paginatedResults) {
48
- const nodeId = result.bindings?.find((b) => b.name === 'X')?.value;
49
- if (nodeId) {
50
- const node = await db.getNode(nodeId);
51
- if (node) {
52
- enrichedResults.push({
53
- ...node,
54
- id: nodeId,
55
- file: node.file,
56
- line: node.line,
57
- });
58
- }
59
- }
60
- }
61
- const paginationInfo = formatPaginationInfo({
62
- limit,
63
- offset,
64
- returned: enrichedResults.length,
65
- total,
66
- hasMore,
67
- });
68
- const responseText = `Found ${total} result(s):${paginationInfo}\n\n${JSON.stringify(serializeBigInt(enrichedResults), null, 2)}`;
69
- return textResult(guardResponseSize(responseText));
70
- }
71
- catch (error) {
72
- const message = error instanceof Error ? error.message : String(error);
73
- return errorResult(message);
74
- }
75
- }
76
- export async function handleFindCalls(args) {
77
- const db = await ensureAnalyzed();
78
- const { target: name, limit: requestedLimit, offset: requestedOffset, className } = args;
79
- const limit = normalizeLimit(requestedLimit);
80
- const offset = Math.max(0, requestedOffset || 0);
81
- const calls = [];
82
- let skipped = 0;
83
- let totalMatched = 0;
84
- for await (const node of db.queryNodes({ type: 'CALL' })) {
85
- if (node.name !== name && node['method'] !== name)
86
- continue;
87
- if (className && node['object'] !== className)
88
- continue;
89
- totalMatched++;
90
- if (skipped < offset) {
91
- skipped++;
92
- continue;
93
- }
94
- if (calls.length >= limit)
95
- continue;
96
- const callsEdges = await db.getOutgoingEdges(node.id, ['CALLS']);
97
- const isResolved = callsEdges.length > 0;
98
- let target = null;
99
- if (isResolved) {
100
- const targetNode = await db.getNode(callsEdges[0].dst);
101
- target = targetNode
102
- ? {
103
- type: targetNode.type,
104
- name: targetNode.name ?? '',
105
- file: targetNode.file,
106
- line: targetNode.line,
107
- }
108
- : null;
109
- }
110
- calls.push({
111
- id: node.id,
112
- name: node.name,
113
- object: node['object'],
114
- file: node.file,
115
- line: node.line,
116
- resolved: isResolved,
117
- target,
118
- });
119
- }
120
- if (totalMatched === 0) {
121
- return textResult(`No calls found for "${className ? className + '.' : ''}${name}"`);
122
- }
123
- const resolved = calls.filter(c => c.resolved).length;
124
- const unresolved = calls.length - resolved;
125
- const hasMore = offset + calls.length < totalMatched;
126
- const paginationInfo = formatPaginationInfo({
127
- limit,
128
- offset,
129
- returned: calls.length,
130
- total: totalMatched,
131
- hasMore,
132
- });
133
- const responseText = `Found ${totalMatched} call(s) to "${className ? className + '.' : ''}${name}":${paginationInfo}\n` +
134
- `- Resolved: ${resolved}\n` +
135
- `- Unresolved: ${unresolved}\n\n` +
136
- JSON.stringify(serializeBigInt(calls), null, 2);
137
- return textResult(guardResponseSize(responseText));
138
- }
139
- export async function handleFindNodes(args) {
140
- const db = await ensureAnalyzed();
141
- const { type, name, file, limit: requestedLimit, offset: requestedOffset } = args;
142
- const limit = normalizeLimit(requestedLimit);
143
- const offset = Math.max(0, requestedOffset || 0);
144
- const filter = {};
145
- if (type)
146
- filter.type = type;
147
- if (name)
148
- filter.name = name;
149
- if (file)
150
- filter.file = file;
151
- const nodes = [];
152
- let skipped = 0;
153
- let totalMatched = 0;
154
- for await (const node of db.queryNodes(filter)) {
155
- totalMatched++;
156
- if (skipped < offset) {
157
- skipped++;
158
- continue;
159
- }
160
- if (nodes.length < limit) {
161
- nodes.push(node);
162
- }
163
- }
164
- if (totalMatched === 0) {
165
- return textResult('No nodes found matching criteria');
166
- }
167
- const hasMore = offset + nodes.length < totalMatched;
168
- const paginationInfo = formatPaginationInfo({
169
- limit,
170
- offset,
171
- returned: nodes.length,
172
- total: totalMatched,
173
- hasMore,
174
- });
175
- return textResult(`Found ${totalMatched} node(s):${paginationInfo}\n\n${JSON.stringify(serializeBigInt(nodes), null, 2)}`);
176
- }
177
- // === TRACE HANDLERS ===
178
- export async function handleTraceAlias(args) {
179
- const db = await ensureAnalyzed();
180
- const { variableName, file } = args;
181
- const _projectPath = getProjectPath();
182
- let varNode = null;
183
- for await (const node of db.queryNodes({ type: 'VARIABLE' })) {
184
- if (node.name === variableName && node.file?.includes(file || '')) {
185
- varNode = node;
186
- break;
187
- }
188
- }
189
- if (!varNode) {
190
- for await (const node of db.queryNodes({ type: 'CONSTANT' })) {
191
- if (node.name === variableName && node.file?.includes(file || '')) {
192
- varNode = node;
193
- break;
194
- }
195
- }
196
- }
197
- if (!varNode) {
198
- return errorResult(`Variable "${variableName}" not found in ${file || 'project'}`);
199
- }
200
- const chain = [];
201
- const visited = new Set();
202
- let current = varNode;
203
- const MAX_DEPTH = 20;
204
- while (current && chain.length < MAX_DEPTH) {
205
- if (visited.has(current.id)) {
206
- chain.push({ type: 'CYCLE_DETECTED', id: current.id });
207
- break;
208
- }
209
- visited.add(current.id);
210
- chain.push({
211
- type: current.type,
212
- name: current.name,
213
- file: current.file,
214
- line: current.line,
215
- });
216
- const edges = await db.getOutgoingEdges(current.id, ['ASSIGNED_FROM']);
217
- if (edges.length === 0)
218
- break;
219
- current = await db.getNode(edges[0].dst);
220
- }
221
- return textResult(`Alias chain for "${variableName}" (${chain.length} steps):\n\n${JSON.stringify(serializeBigInt(chain), null, 2)}`);
222
- }
223
- export async function handleTraceDataFlow(args) {
224
- const db = await ensureAnalyzed();
225
- const { source, direction = 'forward', max_depth = 10 } = args;
226
- // Find source node
227
- let sourceNode = null;
228
- // Try to find by ID first
229
- sourceNode = await db.getNode(source);
230
- // If not found, search by name
231
- if (!sourceNode) {
232
- for await (const node of db.queryNodes({ name: source })) {
233
- sourceNode = node;
234
- break;
235
- }
236
- }
237
- if (!sourceNode) {
238
- return errorResult(`Source "${source}" not found`);
239
- }
240
- const visited = new Set();
241
- const paths = [];
242
- async function trace(nodeId, depth, path) {
243
- if (depth > max_depth || visited.has(nodeId))
244
- return;
245
- visited.add(nodeId);
246
- const newPath = [...path, nodeId];
247
- if (direction === 'forward' || direction === 'both') {
248
- const outEdges = await db.getOutgoingEdges(nodeId, [
249
- 'ASSIGNED_FROM',
250
- 'DERIVES_FROM',
251
- 'PASSES_ARGUMENT',
252
- ]);
253
- for (const edge of outEdges) {
254
- await trace(edge.dst, depth + 1, newPath);
255
- }
256
- }
257
- if (direction === 'backward' || direction === 'both') {
258
- const inEdges = await db.getIncomingEdges(nodeId, [
259
- 'ASSIGNED_FROM',
260
- 'DERIVES_FROM',
261
- 'PASSES_ARGUMENT',
262
- ]);
263
- for (const edge of inEdges) {
264
- await trace(edge.src, depth + 1, newPath);
265
- }
266
- }
267
- if (depth > 0) {
268
- paths.push(newPath);
269
- }
270
- }
271
- await trace(sourceNode.id, 0, []);
272
- return textResult(`Data flow from "${source}" (${paths.length} paths):\n\n${JSON.stringify(paths, null, 2)}`);
273
- }
274
- export async function handleCheckInvariant(args) {
275
- const db = await ensureAnalyzed();
276
- const { rule, name: description } = args;
277
- if (!('checkGuarantee' in db)) {
278
- return errorResult('Backend does not support Datalog queries');
279
- }
280
- try {
281
- const checkFn = db.checkGuarantee;
282
- const violations = await checkFn(rule);
283
- const total = violations.length;
284
- if (total === 0) {
285
- return textResult(`āœ… Invariant holds: ${description || 'No violations found'}`);
286
- }
287
- const enrichedViolations = [];
288
- for (const v of violations.slice(0, 20)) {
289
- const nodeId = v.bindings?.find((b) => b.name === 'X')?.value;
290
- if (nodeId) {
291
- const node = await db.getNode(nodeId);
292
- if (node) {
293
- enrichedViolations.push({
294
- id: nodeId,
295
- type: node.type,
296
- name: node.name,
297
- file: node.file,
298
- line: node.line,
299
- });
300
- }
301
- }
302
- }
303
- return textResult(`āŒ ${total} violation(s) found:\n\n${JSON.stringify(serializeBigInt(enrichedViolations), null, 2)}${total > 20 ? `\n\n... and ${total - 20} more` : ''}`);
304
- }
305
- catch (error) {
306
- const message = error instanceof Error ? error.message : String(error);
307
- return errorResult(message);
308
- }
309
- }
310
- // === ANALYSIS HANDLERS ===
311
- export async function handleAnalyzeProject(args) {
312
- const { service, force } = args;
313
- // Early check: return error for force=true if analysis is already running
314
- // This provides immediate feedback instead of waiting or causing corruption
315
- if (force && isAnalysisRunning()) {
316
- return errorResult('Cannot force re-analysis: analysis is already in progress. ' +
317
- 'Use get_analysis_status to check current status, or wait for completion.');
318
- }
319
- // Note: setIsAnalyzed(false) is now handled inside ensureAnalyzed() within the lock
320
- // to prevent race conditions where multiple calls could both clear the database
321
- try {
322
- await ensureAnalyzed(service || null, force || false);
323
- const status = getAnalysisStatus();
324
- return textResult(`Analysis complete!\n` +
325
- `- Services discovered: ${status.servicesDiscovered}\n` +
326
- `- Services analyzed: ${status.servicesAnalyzed}\n` +
327
- `- Total time: ${status.timings.total || 'N/A'}s`);
328
- }
329
- catch (error) {
330
- const message = error instanceof Error ? error.message : String(error);
331
- return errorResult(message);
332
- }
333
- }
334
- export async function handleGetAnalysisStatus() {
335
- const status = getAnalysisStatus();
336
- return textResult(`Analysis Status:\n` +
337
- `- Running: ${status.running}\n` +
338
- `- Phase: ${status.phase || 'N/A'}\n` +
339
- `- Message: ${status.message || 'N/A'}\n` +
340
- `- Services discovered: ${status.servicesDiscovered}\n` +
341
- `- Services analyzed: ${status.servicesAnalyzed}\n` +
342
- (status.error ? `- Error: ${status.error}\n` : ''));
343
- }
344
- export async function handleGetStats() {
345
- const db = await getOrCreateBackend();
346
- const nodeCount = await db.nodeCount();
347
- const edgeCount = await db.edgeCount();
348
- const nodesByType = await db.countNodesByType();
349
- const edgesByType = await db.countEdgesByType();
350
- return textResult(`Graph Statistics:\n\n` +
351
- `Total nodes: ${nodeCount.toLocaleString()}\n` +
352
- `Total edges: ${edgeCount.toLocaleString()}\n\n` +
353
- `Nodes by type:\n${JSON.stringify(nodesByType, null, 2)}\n\n` +
354
- `Edges by type:\n${JSON.stringify(edgesByType, null, 2)}`);
355
- }
356
- export async function handleGetSchema(args) {
357
- const db = await getOrCreateBackend();
358
- const { type = 'all' } = args;
359
- const nodesByType = await db.countNodesByType();
360
- const edgesByType = await db.countEdgesByType();
361
- let output = '';
362
- if (type === 'nodes' || type === 'all') {
363
- output += `Node Types (${Object.keys(nodesByType).length}):\n`;
364
- for (const [t, count] of Object.entries(nodesByType)) {
365
- output += ` - ${t}: ${count}\n`;
366
- }
367
- }
368
- if (type === 'edges' || type === 'all') {
369
- output += `\nEdge Types (${Object.keys(edgesByType).length}):\n`;
370
- for (const [t, count] of Object.entries(edgesByType)) {
371
- output += ` - ${t}: ${count}\n`;
372
- }
373
- }
374
- return textResult(output);
375
- }
376
- // === GUARANTEE HANDLERS ===
377
- /**
378
- * Create a new guarantee (Datalog-based or contract-based)
379
- */
380
- export async function handleCreateGuarantee(args) {
381
- await getOrCreateBackend(); // Ensure managers are initialized
382
- const { name, rule, type, priority, status, owner, schema, condition, description, governs, severity } = args;
383
- try {
384
- // Determine if this is a contract-based guarantee
385
- if (type && isGuaranteeType(type)) {
386
- // Contract-based guarantee
387
- const api = getGuaranteeAPI();
388
- if (!api) {
389
- return errorResult('GuaranteeAPI not initialized');
390
- }
391
- const guarantee = await api.createGuarantee({
392
- type,
393
- name,
394
- priority,
395
- status,
396
- owner,
397
- schema,
398
- condition,
399
- description,
400
- governs,
401
- });
402
- return textResult(`āœ… Created contract-based guarantee: ${guarantee.id}\n` +
403
- `Type: ${guarantee.type}\n` +
404
- `Priority: ${guarantee.priority}\n` +
405
- `Status: ${guarantee.status}` +
406
- (guarantee.description ? `\nDescription: ${guarantee.description}` : ''));
407
- }
408
- else {
409
- // Datalog-based guarantee
410
- if (!rule) {
411
- return errorResult('Datalog-based guarantee requires "rule" field');
412
- }
413
- const manager = getGuaranteeManager();
414
- if (!manager) {
415
- return errorResult('GuaranteeManager not initialized');
416
- }
417
- const guarantee = await manager.create({
418
- id: name,
419
- name,
420
- rule,
421
- severity: severity || 'warning',
422
- governs: governs || ['**/*.js'],
423
- });
424
- return textResult(`āœ… Created Datalog-based guarantee: ${guarantee.id}\n` +
425
- `Rule: ${guarantee.rule}\n` +
426
- `Severity: ${guarantee.severity}`);
427
- }
428
- }
429
- catch (error) {
430
- const message = error instanceof Error ? error.message : String(error);
431
- return errorResult(`Failed to create guarantee: ${message}`);
432
- }
433
- }
434
- /**
435
- * List all guarantees (both Datalog-based and contract-based)
436
- */
437
- export async function handleListGuarantees() {
438
- await getOrCreateBackend(); // Ensure managers are initialized
439
- const results = [];
440
- try {
441
- // List Datalog-based guarantees
442
- const manager = getGuaranteeManager();
443
- if (manager) {
444
- const datalogGuarantees = await manager.list();
445
- if (datalogGuarantees.length > 0) {
446
- results.push('## Datalog-based Guarantees\n');
447
- for (const g of datalogGuarantees) {
448
- results.push(`- **${g.id}** (${g.severity})`);
449
- results.push(` Rule: ${g.rule.substring(0, 80)}${g.rule.length > 80 ? '...' : ''}`);
450
- }
451
- }
452
- }
453
- // List contract-based guarantees
454
- const api = getGuaranteeAPI();
455
- if (api) {
456
- const contractGuarantees = await api.findGuarantees();
457
- if (contractGuarantees.length > 0) {
458
- if (results.length > 0)
459
- results.push('\n');
460
- results.push('## Contract-based Guarantees\n');
461
- for (const g of contractGuarantees) {
462
- results.push(`- **${g.id}** [${g.priority}] (${g.status})`);
463
- if (g.description)
464
- results.push(` ${g.description}`);
465
- }
466
- }
467
- }
468
- if (results.length === 0) {
469
- return textResult('No guarantees defined yet.');
470
- }
471
- return textResult(results.join('\n'));
472
- }
473
- catch (error) {
474
- const message = error instanceof Error ? error.message : String(error);
475
- return errorResult(`Failed to list guarantees: ${message}`);
476
- }
477
- }
478
- /**
479
- * Check guarantees (both Datalog-based and contract-based)
480
- */
481
- export async function handleCheckGuarantees(args) {
482
- await getOrCreateBackend(); // Ensure managers are initialized
483
- const { names } = args;
484
- const results = [];
485
- let totalPassed = 0;
486
- let totalFailed = 0;
487
- try {
488
- const manager = getGuaranteeManager();
489
- const api = getGuaranteeAPI();
490
- if (names && names.length > 0) {
491
- // Check specific guarantees
492
- for (const name of names) {
493
- // Try Datalog-based first
494
- if (manager) {
495
- try {
496
- const result = await manager.check(name);
497
- if (result.passed) {
498
- totalPassed++;
499
- results.push(`āœ… ${result.guaranteeId}: PASSED`);
500
- }
501
- else {
502
- totalFailed++;
503
- results.push(`āŒ ${result.guaranteeId}: FAILED (${result.violationCount} violations)`);
504
- for (const v of result.violations.slice(0, 5)) {
505
- results.push(` - ${v.file}:${v.line} (${v.type})`);
506
- }
507
- if (result.violationCount > 5) {
508
- results.push(` ... and ${result.violationCount - 5} more`);
509
- }
510
- }
511
- continue;
512
- }
513
- catch {
514
- // Not a Datalog guarantee, try contract-based
515
- }
516
- }
517
- // Try contract-based
518
- if (api) {
519
- try {
520
- const result = await api.checkGuarantee(name);
521
- if (result.passed) {
522
- totalPassed++;
523
- results.push(`āœ… ${result.id}: PASSED`);
524
- }
525
- else {
526
- totalFailed++;
527
- results.push(`āŒ ${result.id}: FAILED`);
528
- for (const err of result.errors.slice(0, 5)) {
529
- results.push(` - ${err}`);
530
- }
531
- }
532
- }
533
- catch {
534
- results.push(`āš ļø ${name}: Not found`);
535
- }
536
- }
537
- }
538
- }
539
- else {
540
- // Check all guarantees
541
- if (manager) {
542
- const datalogResult = await manager.checkAll();
543
- totalPassed += datalogResult.passed;
544
- totalFailed += datalogResult.failed;
545
- if (datalogResult.total > 0) {
546
- results.push('## Datalog Guarantees\n');
547
- for (const r of datalogResult.results) {
548
- if (r.passed) {
549
- results.push(`āœ… ${r.guaranteeId}: PASSED`);
550
- }
551
- else {
552
- results.push(`āŒ ${r.guaranteeId}: FAILED (${r.violationCount} violations)`);
553
- }
554
- }
555
- }
556
- }
557
- if (api) {
558
- const contractResult = await api.checkAllGuarantees();
559
- totalPassed += contractResult.passed;
560
- totalFailed += contractResult.failed;
561
- if (contractResult.total > 0) {
562
- if (results.length > 0)
563
- results.push('\n');
564
- results.push('## Contract Guarantees\n');
565
- for (const r of contractResult.results) {
566
- if (r.passed) {
567
- results.push(`āœ… ${r.id}: PASSED`);
568
- }
569
- else {
570
- results.push(`āŒ ${r.id}: FAILED`);
571
- }
572
- }
573
- }
574
- }
575
- }
576
- if (results.length === 0) {
577
- return textResult('No guarantees to check.');
578
- }
579
- const summary = `\n---\nTotal: ${totalPassed + totalFailed} | āœ… Passed: ${totalPassed} | āŒ Failed: ${totalFailed}`;
580
- return textResult(results.join('\n') + summary);
581
- }
582
- catch (error) {
583
- const message = error instanceof Error ? error.message : String(error);
584
- return errorResult(`Failed to check guarantees: ${message}`);
585
- }
586
- }
587
- /**
588
- * Delete a guarantee
589
- */
590
- export async function handleDeleteGuarantee(args) {
591
- await getOrCreateBackend(); // Ensure managers are initialized
592
- const { name } = args;
593
- try {
594
- // Try Datalog-based first
595
- const manager = getGuaranteeManager();
596
- if (manager) {
597
- try {
598
- await manager.delete(name);
599
- return textResult(`āœ… Deleted Datalog guarantee: ${name}`);
600
- }
601
- catch {
602
- // Not found in Datalog, try contract-based
603
- }
604
- }
605
- // Try contract-based
606
- const api = getGuaranteeAPI();
607
- if (api) {
608
- const deleted = await api.deleteGuarantee(name);
609
- if (deleted) {
610
- return textResult(`āœ… Deleted contract guarantee: ${name}`);
611
- }
612
- }
613
- return errorResult(`Guarantee not found: ${name}`);
614
- }
615
- catch (error) {
616
- const message = error instanceof Error ? error.message : String(error);
617
- return errorResult(`Failed to delete guarantee: ${message}`);
618
- }
619
- }
620
- // === COVERAGE & DOCS ===
621
- export async function handleGetCoverage(args) {
622
- const db = await getOrCreateBackend();
623
- const projectPath = getProjectPath();
624
- const { path: targetPath = projectPath } = args;
625
- try {
626
- const analyzer = new CoverageAnalyzer(db, targetPath);
627
- const result = await analyzer.analyze();
628
- // Format output for AI agents
629
- let output = `Analysis Coverage for ${targetPath}\n`;
630
- output += `==============================\n\n`;
631
- output += `File breakdown:\n`;
632
- output += ` Total files: ${result.total}\n`;
633
- output += ` Analyzed: ${result.analyzed.count} (${result.percentages.analyzed}%) - in graph\n`;
634
- output += ` Unsupported: ${result.unsupported.count} (${result.percentages.unsupported}%) - no indexer available\n`;
635
- output += ` Unreachable: ${result.unreachable.count} (${result.percentages.unreachable}%) - not imported from entrypoints\n`;
636
- if (result.unsupported.count > 0) {
637
- output += `\nUnsupported files by extension:\n`;
638
- for (const [ext, files] of Object.entries(result.unsupported.byExtension)) {
639
- output += ` ${ext}: ${files.length} files\n`;
640
- }
641
- }
642
- if (result.unreachable.count > 0) {
643
- output += `\nUnreachable source files:\n`;
644
- for (const [ext, files] of Object.entries(result.unreachable.byExtension)) {
645
- output += ` ${ext}: ${files.length} files\n`;
646
- }
647
- }
648
- return textResult(output);
649
- }
650
- catch (error) {
651
- const message = error instanceof Error ? error.message : String(error);
652
- return errorResult(`Failed to calculate coverage: ${message}`);
653
- }
654
- }
655
- export async function handleGetDocumentation(args) {
656
- const { topic = 'overview' } = args;
657
- const docs = {
658
- onboarding: getOnboardingInstruction(),
659
- overview: `
660
- # Grafema Code Analysis
661
-
662
- Grafema is a static code analyzer that builds a graph of your codebase.
663
-
664
- ## Key Tools
665
- - query_graph: Execute Datalog queries
666
- - find_calls: Find function/method calls
667
- - trace_alias: Trace variable aliases
668
- - check_invariant: Verify code invariants
669
-
670
- ## Quick Start
671
- 1. Use get_stats to see graph size
672
- 2. Use find_nodes to explore the codebase
673
- 3. Use query_graph for complex queries
674
- `,
675
- queries: `
676
- # Datalog Queries
677
-
678
- ## Syntax
679
- violation(X) :- node(X, "TYPE"), attr(X, "name", "value").
680
-
681
- ## Available Predicates
682
- - node(Id, Type) - match nodes
683
- - edge(Src, Dst, Type) - match edges
684
- - attr(Id, Name, Value) - match attributes
685
- - \\+ - negation (not)
686
-
687
- ## Examples
688
- Find all functions:
689
- violation(X) :- node(X, "FUNCTION").
690
-
691
- Find unresolved calls:
692
- violation(X) :- node(X, "CALL"), \\+ edge(X, _, "CALLS").
693
- `,
694
- types: `
695
- # Node & Edge Types
696
-
697
- ## Core Node Types
698
- - MODULE, FUNCTION, CLASS, METHOD, VARIABLE
699
- - CALL, PROPERTY_ACCESS, IMPORT, EXPORT, PARAMETER
700
-
701
- ## HTTP/Network
702
- - http:route, http:request, db:query
703
-
704
- ## Edge Types
705
- - CONTAINS, CALLS, DEPENDS_ON
706
- - ASSIGNED_FROM, INSTANCE_OF, PASSES_ARGUMENT
707
- `,
708
- guarantees: `
709
- # Code Guarantees
710
-
711
- Guarantees are persistent code invariants.
712
-
713
- ## Create
714
- Use create_guarantee with a name and Datalog rule.
715
-
716
- ## Check
717
- Use check_guarantees to verify all guarantees.
718
-
719
- ## Example
720
- Name: no-eval
721
- Rule: violation(X) :- node(X, "CALL"), attr(X, "name", "eval").
722
- `,
723
- };
724
- const content = docs[topic] || docs.overview;
725
- return textResult(content.trim());
726
- }
727
- // === FIND GUARDS (REG-274) ===
728
- /**
729
- * Find conditional guards protecting a node.
730
- *
731
- * Walks up the containment tree via CONTAINS edges, collecting
732
- * SCOPE nodes that have conditional=true (if_statement, else_statement, etc.).
733
- *
734
- * Returns guards in inner-to-outer order.
735
- */
736
- export async function handleFindGuards(args) {
737
- const db = await getOrCreateBackend();
738
- const { nodeId } = args;
739
- // Verify target node exists
740
- const targetNode = await db.getNode(nodeId);
741
- if (!targetNode) {
742
- return errorResult(`Node not found: ${nodeId}`);
743
- }
744
- const guards = [];
745
- const visited = new Set();
746
- let currentId = nodeId;
747
- // Walk up the containment tree
748
- while (true) {
749
- if (visited.has(currentId))
750
- break;
751
- visited.add(currentId);
752
- // Get parent via incoming CONTAINS edge
753
- const incomingEdges = await db.getIncomingEdges(currentId, ['CONTAINS']);
754
- if (incomingEdges.length === 0)
755
- break;
756
- const parentId = incomingEdges[0].src;
757
- const parentNode = await db.getNode(parentId);
758
- if (!parentNode)
759
- break;
760
- // Check if this is a conditional scope
761
- if (parentNode.conditional) {
762
- // Parse constraints if stored as string
763
- let constraints = parentNode.constraints;
764
- if (typeof constraints === 'string') {
765
- try {
766
- constraints = JSON.parse(constraints);
767
- }
768
- catch {
769
- // Keep as string if not valid JSON
770
- }
771
- }
772
- guards.push({
773
- scopeId: parentNode.id,
774
- scopeType: parentNode.scopeType || 'unknown',
775
- condition: parentNode.condition,
776
- constraints: constraints,
777
- file: parentNode.file || '',
778
- line: parentNode.line || 0,
779
- });
780
- }
781
- currentId = parentId;
782
- }
783
- if (guards.length === 0) {
784
- return textResult(`No guards found for node: ${nodeId}\n` +
785
- `The node is not protected by any conditional scope (if/else/switch/etc.).`);
786
- }
787
- const summary = guards.map((g, i) => {
788
- const indent = ' '.repeat(i);
789
- return `${indent}${i + 1}. ${g.scopeType} at ${g.file}:${g.line}` +
790
- (g.condition ? `\n${indent} condition: ${g.condition}` : '');
791
- }).join('\n');
792
- return textResult(`Found ${guards.length} guard(s) for node: ${nodeId}\n` +
793
- `(inner to outer order)\n\n` +
794
- summary +
795
- `\n\n` +
796
- JSON.stringify(serializeBigInt(guards), null, 2));
797
- }
798
- // === GET FUNCTION DETAILS (REG-254) ===
799
- /**
800
- * Get comprehensive function details including calls made and callers.
801
- *
802
- * Graph structure:
803
- * ```
804
- * FUNCTION -[HAS_SCOPE]-> SCOPE -[CONTAINS]-> CALL/METHOD_CALL
805
- * SCOPE -[CONTAINS]-> SCOPE (nested blocks)
806
- * CALL -[CALLS]-> FUNCTION (target)
807
- * ```
808
- *
809
- * This is the core tool for understanding function behavior.
810
- * Use transitive=true to follow call chains (A -> B -> C).
811
- */
812
- export async function handleGetFunctionDetails(args) {
813
- const db = await ensureAnalyzed();
814
- const { name, file, transitive = false } = args;
815
- // Step 1: Find the function
816
- const candidates = [];
817
- for await (const node of db.queryNodes({ type: 'FUNCTION' })) {
818
- if (node.name !== name)
819
- continue;
820
- if (file && !node.file?.includes(file))
821
- continue;
822
- candidates.push(node);
823
- }
824
- if (candidates.length === 0) {
825
- return errorResult(`Function "${name}" not found.` +
826
- (file ? ` (searched in files matching "${file}")` : ''));
827
- }
828
- if (candidates.length > 1 && !file) {
829
- const locations = candidates.map(f => `${f.file}:${f.line}`).join(', ');
830
- return errorResult(`Multiple functions named "${name}" found: ${locations}. ` +
831
- `Use the "file" parameter to disambiguate.`);
832
- }
833
- const targetFunction = candidates[0];
834
- // Step 2: Find calls using shared utility
835
- const calls = await findCallsInFunction(db, targetFunction.id, {
836
- transitive,
837
- transitiveDepth: 5,
838
- });
839
- // Step 3: Find callers
840
- const calledBy = [];
841
- const incomingCalls = await db.getIncomingEdges(targetFunction.id, ['CALLS']);
842
- const seenCallers = new Set();
843
- for (const edge of incomingCalls) {
844
- const caller = await findContainingFunction(db, edge.src);
845
- if (caller && !seenCallers.has(caller.id)) {
846
- seenCallers.add(caller.id);
847
- calledBy.push(caller);
848
- }
849
- }
850
- // Step 4: Build result
851
- const result = {
852
- id: targetFunction.id,
853
- name: targetFunction.name,
854
- file: targetFunction.file,
855
- line: targetFunction.line,
856
- async: targetFunction.async,
857
- calls,
858
- calledBy,
859
- };
860
- // Format output
861
- const summary = [
862
- `Function: ${result.name}`,
863
- `File: ${result.file || 'unknown'}:${result.line || '?'}`,
864
- `Async: ${result.async || false}`,
865
- `Transitive: ${transitive}`,
866
- '',
867
- `Calls (${calls.length}):`,
868
- ...formatCallsForDisplay(calls),
869
- '',
870
- `Called by (${calledBy.length}):`,
871
- ...calledBy.map(c => ` - ${c.name} (${c.file}:${c.line})`),
872
- ].join('\n');
873
- return textResult(summary + '\n\n' +
874
- JSON.stringify(serializeBigInt(result), null, 2));
875
- }
876
- /**
877
- * Format calls for display, grouped by depth if transitive
878
- */
879
- function formatCallsForDisplay(calls) {
880
- const directCalls = calls.filter(c => (c.depth || 0) === 0);
881
- const transitiveCalls = calls.filter(c => (c.depth || 0) > 0);
882
- const lines = [];
883
- // Direct calls
884
- for (const c of directCalls) {
885
- const target = c.resolved
886
- ? ` -> ${c.target?.name} (${c.target?.file}:${c.target?.line})`
887
- : ' (unresolved)';
888
- const prefix = c.type === 'METHOD_CALL' ? `${c.object}.` : '';
889
- lines.push(` - ${prefix}${c.name}()${target}`);
890
- }
891
- // Transitive calls (grouped by depth)
892
- if (transitiveCalls.length > 0) {
893
- lines.push('');
894
- lines.push(' Transitive calls:');
895
- const byDepth = new Map();
896
- for (const c of transitiveCalls) {
897
- const depth = c.depth || 1;
898
- if (!byDepth.has(depth))
899
- byDepth.set(depth, []);
900
- byDepth.get(depth).push(c);
901
- }
902
- for (const [depth, depthCalls] of Array.from(byDepth.entries()).sort((a, b) => a[0] - b[0])) {
903
- for (const c of depthCalls) {
904
- const indent = ' '.repeat(depth + 1);
905
- const prefix = c.type === 'METHOD_CALL' ? `${c.object}.` : '';
906
- const target = c.resolved ? ` -> ${c.target?.name}` : '';
907
- lines.push(`${indent}[depth=${depth}] ${prefix}${c.name}()${target}`);
908
- }
909
- }
910
- }
911
- return lines;
912
- }
913
- // === NODE CONTEXT (REG-406) ===
914
- export async function handleGetContext(args) {
915
- const db = await ensureAnalyzed();
916
- const { semanticId, contextLines: ctxLines = 3, edgeType } = args;
917
- // 1. Look up node
918
- const node = await db.getNode(semanticId);
919
- if (!node) {
920
- return errorResult(`Node not found: "${semanticId}"\n` +
921
- `Use find_nodes or query_graph to find the correct semantic ID.`);
922
- }
923
- const edgeTypeFilter = edgeType
924
- ? new Set(edgeType.split(',').map(t => t.trim().toUpperCase()))
925
- : null;
926
- // 2. Build context using shared logic
927
- const ctx = await buildNodeContext(db, node, {
928
- contextLines: ctxLines,
929
- edgeTypeFilter,
930
- });
931
- // 3. Format text output
932
- const projectPath = getProjectPath();
933
- const relFile = node.file ? relative(projectPath, node.file) : undefined;
934
- const lines = [];
935
- lines.push(`[${node.type}] ${getNodeDisplayName(node)}`);
936
- lines.push(` ID: ${node.id}`);
937
- if (relFile) {
938
- lines.push(` Location: ${relFile}${node.line ? `:${node.line}` : ''}`);
939
- }
940
- // Source
941
- if (ctx.source) {
942
- lines.push('');
943
- lines.push(` Source (lines ${ctx.source.startLine}-${ctx.source.endLine}):`);
944
- const maxLineNum = ctx.source.endLine;
945
- const lineNumWidth = String(maxLineNum).length;
946
- for (let i = 0; i < ctx.source.lines.length; i++) {
947
- const lineNum = ctx.source.startLine + i;
948
- const paddedNum = String(lineNum).padStart(lineNumWidth, ' ');
949
- const prefix = lineNum === node.line ? '>' : ' ';
950
- const displayLine = ctx.source.lines[i].length > 120
951
- ? ctx.source.lines[i].slice(0, 117) + '...'
952
- : ctx.source.lines[i];
953
- lines.push(` ${prefix}${paddedNum} | ${displayLine}`);
954
- }
955
- }
956
- const formatEdgeSection = (groups, dir) => {
957
- for (const group of groups) {
958
- const isStructural = STRUCTURAL_EDGE_TYPES.has(group.edgeType);
959
- lines.push(` ${group.edgeType} (${group.edges.length}):`);
960
- for (const { edge, node: connNode } of group.edges) {
961
- if (!connNode) {
962
- const danglingId = dir === '->' ? edge.dst : edge.src;
963
- lines.push(` ${dir} [dangling] ${danglingId}`);
964
- continue;
965
- }
966
- const nFile = connNode.file ? relative(projectPath, connNode.file) : '';
967
- const nLoc = nFile ? (connNode.line ? `${nFile}:${connNode.line}` : nFile) : '';
968
- const locStr = nLoc ? ` (${nLoc})` : '';
969
- const metaStr = formatEdgeMetadata(edge);
970
- lines.push(` ${dir} [${connNode.type}] ${getNodeDisplayName(connNode)}${locStr}${metaStr}`);
971
- // Code context for non-structural edges
972
- if (!isStructural && connNode.file && connNode.line && ctxLines > 0) {
973
- if (existsSync(connNode.file)) {
974
- try {
975
- const content = readFileSync(connNode.file, 'utf-8');
976
- const allFileLines = content.split('\n');
977
- const nLine = connNode.line;
978
- const sLine = Math.max(1, nLine - Math.min(ctxLines, 2));
979
- const eLine = Math.min(allFileLines.length, nLine + Math.min(ctxLines, 2));
980
- const w = String(eLine).length;
981
- for (let i = sLine; i <= eLine; i++) {
982
- const p = i === nLine ? '>' : ' ';
983
- const ln = String(i).padStart(w, ' ');
984
- const displayLn = allFileLines[i - 1].length > 120
985
- ? allFileLines[i - 1].slice(0, 117) + '...'
986
- : allFileLines[i - 1];
987
- lines.push(` ${p}${ln} | ${displayLn}`);
988
- }
989
- }
990
- catch { /* ignore */ }
991
- }
992
- }
993
- }
994
- }
995
- };
996
- if (ctx.outgoing.length > 0) {
997
- lines.push('');
998
- lines.push(' Outgoing edges:');
999
- formatEdgeSection(ctx.outgoing, '->');
1000
- }
1001
- if (ctx.incoming.length > 0) {
1002
- lines.push('');
1003
- lines.push(' Incoming edges:');
1004
- formatEdgeSection(ctx.incoming, '<-');
1005
- }
1006
- if (ctx.outgoing.length === 0 && ctx.incoming.length === 0) {
1007
- lines.push('');
1008
- lines.push(' No edges found.');
1009
- }
1010
- // Build JSON result alongside text
1011
- const jsonResult = {
1012
- node: { id: node.id, type: node.type, name: node.name, file: relFile, line: node.line },
1013
- source: ctx.source ? {
1014
- startLine: ctx.source.startLine,
1015
- endLine: ctx.source.endLine,
1016
- lines: ctx.source.lines,
1017
- } : null,
1018
- outgoing: Object.fromEntries(ctx.outgoing.map(g => [g.edgeType, g.edges])),
1019
- incoming: Object.fromEntries(ctx.incoming.map(g => [g.edgeType, g.edges])),
1020
- };
1021
- return textResult(lines.join('\n') + '\n\n' + JSON.stringify(serializeBigInt(jsonResult), null, 2));
1022
- }
1023
- // === BUG REPORTING ===
1024
- export async function handleReportIssue(args) {
1025
- const { title, description, context, labels = ['bug'] } = args;
1026
- // Use user's token if provided, otherwise fall back to project's issue-only token
1027
- const GRAFEMA_ISSUE_TOKEN = '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';
1028
- const githubToken = process.env.GITHUB_TOKEN || GRAFEMA_ISSUE_TOKEN;
1029
- const repo = 'Disentinel/grafema';
1030
- // Build issue body
1031
- const body = `## Description
1032
- ${description}
1033
-
1034
- ${context ? `## Context\n\`\`\`\n${context}\n\`\`\`\n` : ''}
1035
- ## Environment
1036
- - Grafema version: 0.1.0-alpha.1
1037
- - Reported via: MCP tool
1038
-
1039
- ---
1040
- *This issue was automatically created via Grafema MCP server.*`;
1041
- // Try GitHub API if token is available
1042
- if (githubToken) {
1043
- try {
1044
- const response = await fetch(`https://api.github.com/repos/${repo}/issues`, {
1045
- method: 'POST',
1046
- headers: {
1047
- 'Authorization': `token ${githubToken}`,
1048
- 'Content-Type': 'application/json',
1049
- 'Accept': 'application/vnd.github.v3+json',
1050
- },
1051
- body: JSON.stringify({
1052
- title,
1053
- body,
1054
- labels: labels.filter(l => ['bug', 'enhancement', 'documentation', 'question'].includes(l)),
1055
- }),
1056
- });
1057
- if (response.ok) {
1058
- const issue = await response.json();
1059
- return textResult(`āœ… Issue created successfully!\n\n` +
1060
- `**Issue #${issue.number}**: ${issue.html_url}\n\n` +
1061
- `Thank you for reporting this issue.`);
1062
- }
1063
- else {
1064
- const error = await response.text();
1065
- throw new Error(`GitHub API error: ${response.status} - ${error}`);
1066
- }
1067
- }
1068
- catch (error) {
1069
- // Fall through to manual template if API fails
1070
- console.error('[report_issue] GitHub API failed:', error);
1071
- }
1072
- }
1073
- // Fallback: return template for manual submission
1074
- const issueUrl = `https://github.com/${repo}/issues/new`;
1075
- const encodedTitle = encodeURIComponent(title);
1076
- const encodedBody = encodeURIComponent(body);
1077
- const encodedLabels = encodeURIComponent(labels.join(','));
1078
- const directUrl = `${issueUrl}?title=${encodedTitle}&body=${encodedBody}&labels=${encodedLabels}`;
1079
- return textResult(`āš ļø Failed to create issue automatically. Please create it manually:\n\n` +
1080
- `**Quick link** (may truncate long descriptions):\n${directUrl}\n\n` +
1081
- `**Or copy this template to** ${issueUrl}:\n\n` +
1082
- `---\n**Title:** ${title}\n\n${body}\n---`);
1083
- }
1084
- // === PROJECT STRUCTURE (REG-173) ===
1085
- export async function handleReadProjectStructure(args) {
1086
- const projectPath = getProjectPath();
1087
- const subPath = args.path || '.';
1088
- const maxDepth = Math.min(Math.max(1, args.depth || 3), 5);
1089
- const includeFiles = args.include_files !== false;
1090
- const targetPath = join(projectPath, subPath);
1091
- if (!existsSync(targetPath)) {
1092
- return errorResult(`Path does not exist: ${subPath}`);
1093
- }
1094
- if (!statSync(targetPath).isDirectory()) {
1095
- return errorResult(`Path is not a directory: ${subPath}`);
1096
- }
1097
- const EXCLUDED = new Set([
1098
- 'node_modules', '.git', 'dist', 'build', '.grafema',
1099
- 'coverage', '.next', '.nuxt', '.cache', '.output',
1100
- '__pycache__', '.tox', 'target',
1101
- ]);
1102
- const lines = [];
1103
- function walk(dir, prefix, depth) {
1104
- if (depth > maxDepth)
1105
- return;
1106
- let entries;
1107
- try {
1108
- entries = readdirSync(dir, { withFileTypes: true });
1109
- }
1110
- catch {
1111
- return;
1112
- }
1113
- const dirs = [];
1114
- const files = [];
1115
- for (const entry of entries) {
1116
- if (EXCLUDED.has(entry.name))
1117
- continue;
1118
- if (entry.isDirectory()) {
1119
- dirs.push(entry.name);
1120
- }
1121
- else if (includeFiles) {
1122
- files.push(entry.name);
1123
- }
1124
- }
1125
- dirs.sort();
1126
- files.sort();
1127
- const allEntries = [
1128
- ...dirs.map(d => ({ name: d, isDir: true })),
1129
- ...files.map(f => ({ name: f, isDir: false })),
1130
- ];
1131
- for (let i = 0; i < allEntries.length; i++) {
1132
- const entry = allEntries[i];
1133
- const isLast = i === allEntries.length - 1;
1134
- const connector = isLast ? '└── ' : 'ā”œā”€ā”€ ';
1135
- const childPrefix = isLast ? ' ' : '│ ';
1136
- if (entry.isDir) {
1137
- lines.push(`${prefix}${connector}${entry.name}/`);
1138
- walk(join(dir, entry.name), prefix + childPrefix, depth + 1);
1139
- }
1140
- else {
1141
- lines.push(`${prefix}${connector}${entry.name}`);
1142
- }
1143
- }
1144
- }
1145
- lines.push(subPath === '.' ? basename(projectPath) + '/' : subPath + '/');
1146
- walk(targetPath, '', 1);
1147
- if (lines.length === 1) {
1148
- return textResult(`Directory is empty or contains only excluded entries: ${subPath}`);
1149
- }
1150
- return textResult(lines.join('\n'));
1151
- }
1152
- // === WRITE CONFIG (REG-173) ===
1153
- export async function handleWriteConfig(args) {
1154
- const projectPath = getProjectPath();
1155
- const grafemaDir = join(projectPath, '.grafema');
1156
- const configPath = join(grafemaDir, 'config.yaml');
1157
- try {
1158
- if (args.services) {
1159
- validateServices(args.services, projectPath);
1160
- }
1161
- if (args.include !== undefined || args.exclude !== undefined) {
1162
- const warnings = [];
1163
- validatePatterns(args.include, args.exclude, {
1164
- warn: (msg) => warnings.push(msg),
1165
- });
1166
- }
1167
- if (args.workspace) {
1168
- validateWorkspace(args.workspace, projectPath);
1169
- }
1170
- const config = {
1171
- version: getSchemaVersion(GRAFEMA_VERSION),
1172
- };
1173
- if (args.services && args.services.length > 0) {
1174
- config.services = args.services;
1175
- }
1176
- if (args.plugins) {
1177
- config.plugins = args.plugins;
1178
- }
1179
- if (args.include) {
1180
- config.include = args.include;
1181
- }
1182
- if (args.exclude) {
1183
- config.exclude = args.exclude;
1184
- }
1185
- if (args.workspace) {
1186
- config.workspace = args.workspace;
1187
- }
1188
- const yaml = stringifyYAML(config, { lineWidth: 0 });
1189
- const content = '# Grafema Configuration\n' +
1190
- '# Generated by Grafema onboarding\n' +
1191
- '# Documentation: https://github.com/grafema/grafema#configuration\n\n' +
1192
- yaml;
1193
- if (!existsSync(grafemaDir)) {
1194
- mkdirSync(grafemaDir, { recursive: true });
1195
- }
1196
- writeFileSync(configPath, content);
1197
- const summary = ['Configuration written to .grafema/config.yaml'];
1198
- if (args.services && args.services.length > 0) {
1199
- summary.push(`Services: ${args.services.map(s => s.name).join(', ')}`);
1200
- }
1201
- else {
1202
- summary.push('Services: using auto-discovery (none explicitly configured)');
1203
- }
1204
- if (args.plugins) {
1205
- summary.push('Plugins: custom configuration');
1206
- }
1207
- else {
1208
- summary.push('Plugins: using defaults');
1209
- }
1210
- if (args.include) {
1211
- summary.push(`Include patterns: ${args.include.join(', ')}`);
1212
- }
1213
- if (args.exclude) {
1214
- summary.push(`Exclude patterns: ${args.exclude.join(', ')}`);
1215
- }
1216
- if (args.workspace?.roots) {
1217
- summary.push(`Workspace roots: ${args.workspace.roots.join(', ')}`);
1218
- }
1219
- summary.push('\nNext step: run analyze_project to build the graph.');
1220
- return textResult(summary.join('\n'));
1221
- }
1222
- catch (error) {
1223
- const message = error instanceof Error ? error.message : String(error);
1224
- return errorResult(`Failed to write config: ${message}`);
1225
- }
1226
- }
1227
- // === FILE OVERVIEW (REG-412) ===
1228
- export async function handleGetFileOverview(args) {
1229
- const db = await ensureAnalyzed();
1230
- const projectPath = getProjectPath();
1231
- const { file, include_edges: includeEdges = true } = args;
1232
- let filePath = file;
1233
- if (!filePath.startsWith('/')) {
1234
- filePath = join(projectPath, filePath);
1235
- }
1236
- if (!existsSync(filePath)) {
1237
- return errorResult(`File not found: ${file}\n` +
1238
- `Resolved to: ${filePath}\n` +
1239
- `Project root: ${projectPath}`);
1240
- }
1241
- const absolutePath = realpathSync(filePath);
1242
- const relativePath = relative(projectPath, absolutePath);
1243
- try {
1244
- const overview = new FileOverview(db);
1245
- const result = await overview.getOverview(absolutePath, {
1246
- includeEdges,
1247
- });
1248
- result.file = relativePath;
1249
- if (result.status === 'NOT_ANALYZED') {
1250
- return textResult(`File not analyzed: ${relativePath}\n` +
1251
- `Run analyze_project to build the graph.`);
1252
- }
1253
- const lines = [];
1254
- lines.push(`Module: ${result.file}`);
1255
- if (result.imports.length > 0) {
1256
- const sources = result.imports.map(i => i.source);
1257
- lines.push(`Imports: ${sources.join(', ')}`);
1258
- }
1259
- if (result.exports.length > 0) {
1260
- const names = result.exports.map(e => e.isDefault ? `${e.name} (default)` : e.name);
1261
- lines.push(`Exports: ${names.join(', ')}`);
1262
- }
1263
- if (result.classes.length > 0) {
1264
- lines.push('');
1265
- lines.push('Classes:');
1266
- for (const cls of result.classes) {
1267
- const ext = cls.extends ? ` extends ${cls.extends}` : '';
1268
- lines.push(` ${cls.name}${ext} (line ${cls.line ?? '?'})`);
1269
- for (const m of cls.methods) {
1270
- const calls = m.calls.length > 0
1271
- ? ` -> ${m.calls.join(', ')}`
1272
- : '';
1273
- const params = m.params
1274
- ? `(${m.params.join(', ')})`
1275
- : '()';
1276
- lines.push(` ${m.name}${params}${calls}`);
1277
- }
1278
- }
1279
- }
1280
- if (result.functions.length > 0) {
1281
- lines.push('');
1282
- lines.push('Functions:');
1283
- for (const fn of result.functions) {
1284
- const calls = fn.calls.length > 0
1285
- ? ` -> ${fn.calls.join(', ')}`
1286
- : '';
1287
- const params = fn.params
1288
- ? `(${fn.params.join(', ')})`
1289
- : '()';
1290
- const asyncStr = fn.async ? 'async ' : '';
1291
- lines.push(` ${asyncStr}${fn.name}${params}${calls} (line ${fn.line ?? '?'})`);
1292
- }
1293
- }
1294
- if (result.variables.length > 0) {
1295
- lines.push('');
1296
- lines.push('Variables:');
1297
- for (const v of result.variables) {
1298
- const assign = v.assignedFrom ? ` = ${v.assignedFrom}` : '';
1299
- lines.push(` ${v.kind} ${v.name}${assign} (line ${v.line ?? '?'})`);
1300
- }
1301
- }
1302
- return textResult(lines.join('\n') + '\n\n' +
1303
- JSON.stringify(serializeBigInt(result), null, 2));
1304
- }
1305
- catch (error) {
1306
- const message = error instanceof Error ? error.message : String(error);
1307
- return errorResult(`Failed to get file overview: ${message}`);
1308
- }
1309
- }
1310
- //# sourceMappingURL=handlers.js.map