aidex-graphra 1.0.0

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 (59) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +463 -0
  3. package/dist/chunker.d.ts +3 -0
  4. package/dist/chunker.d.ts.map +1 -0
  5. package/dist/chunker.js +116 -0
  6. package/dist/chunker.js.map +1 -0
  7. package/dist/cli.d.ts +3 -0
  8. package/dist/cli.d.ts.map +1 -0
  9. package/dist/cli.js +821 -0
  10. package/dist/cli.js.map +1 -0
  11. package/dist/graph.d.ts +9 -0
  12. package/dist/graph.d.ts.map +1 -0
  13. package/dist/graph.js +97 -0
  14. package/dist/graph.js.map +1 -0
  15. package/dist/init.d.ts +27 -0
  16. package/dist/init.d.ts.map +1 -0
  17. package/dist/init.js +306 -0
  18. package/dist/init.js.map +1 -0
  19. package/dist/mcp.d.ts +13 -0
  20. package/dist/mcp.d.ts.map +1 -0
  21. package/dist/mcp.js +19 -0
  22. package/dist/mcp.js.map +1 -0
  23. package/dist/mcpServer.d.ts +14 -0
  24. package/dist/mcpServer.d.ts.map +1 -0
  25. package/dist/mcpServer.js +373 -0
  26. package/dist/mcpServer.js.map +1 -0
  27. package/dist/neuralEmbedder.d.ts +21 -0
  28. package/dist/neuralEmbedder.d.ts.map +1 -0
  29. package/dist/neuralEmbedder.js +98 -0
  30. package/dist/neuralEmbedder.js.map +1 -0
  31. package/dist/scanner.d.ts +3 -0
  32. package/dist/scanner.d.ts.map +1 -0
  33. package/dist/scanner.js +43 -0
  34. package/dist/scanner.js.map +1 -0
  35. package/dist/search.d.ts +37 -0
  36. package/dist/search.d.ts.map +1 -0
  37. package/dist/search.js +252 -0
  38. package/dist/search.js.map +1 -0
  39. package/dist/signatureExtractor.d.ts +25 -0
  40. package/dist/signatureExtractor.d.ts.map +1 -0
  41. package/dist/signatureExtractor.js +173 -0
  42. package/dist/signatureExtractor.js.map +1 -0
  43. package/dist/storage.d.ts +59 -0
  44. package/dist/storage.d.ts.map +1 -0
  45. package/dist/storage.js +322 -0
  46. package/dist/storage.js.map +1 -0
  47. package/dist/tokenBudget.d.ts +52 -0
  48. package/dist/tokenBudget.d.ts.map +1 -0
  49. package/dist/tokenBudget.js +175 -0
  50. package/dist/tokenBudget.js.map +1 -0
  51. package/dist/types.d.ts +62 -0
  52. package/dist/types.d.ts.map +1 -0
  53. package/dist/types.js +6 -0
  54. package/dist/types.js.map +1 -0
  55. package/dist/utils/hash.d.ts +6 -0
  56. package/dist/utils/hash.d.ts.map +1 -0
  57. package/dist/utils/hash.js +45 -0
  58. package/dist/utils/hash.js.map +1 -0
  59. package/package.json +69 -0
package/dist/cli.js ADDED
@@ -0,0 +1,821 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
4
+ if (k2 === undefined) k2 = k;
5
+ var desc = Object.getOwnPropertyDescriptor(m, k);
6
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
7
+ desc = { enumerable: true, get: function() { return m[k]; } };
8
+ }
9
+ Object.defineProperty(o, k2, desc);
10
+ }) : (function(o, m, k, k2) {
11
+ if (k2 === undefined) k2 = k;
12
+ o[k2] = m[k];
13
+ }));
14
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
15
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
16
+ }) : function(o, v) {
17
+ o["default"] = v;
18
+ });
19
+ var __importStar = (this && this.__importStar) || (function () {
20
+ var ownKeys = function(o) {
21
+ ownKeys = Object.getOwnPropertyNames || function (o) {
22
+ var ar = [];
23
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
24
+ return ar;
25
+ };
26
+ return ownKeys(o);
27
+ };
28
+ return function (mod) {
29
+ if (mod && mod.__esModule) return mod;
30
+ var result = {};
31
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
32
+ __setModuleDefault(result, mod);
33
+ return result;
34
+ };
35
+ })();
36
+ Object.defineProperty(exports, "__esModule", { value: true });
37
+ const commander_1 = require("commander");
38
+ const path = __importStar(require("path"));
39
+ const fs = __importStar(require("fs"));
40
+ const scanner_1 = require("./scanner");
41
+ const chunker_1 = require("./chunker");
42
+ const signatureExtractor_1 = require("./signatureExtractor");
43
+ const neuralEmbedder_1 = require("./neuralEmbedder");
44
+ const graph_1 = require("./graph");
45
+ const search_1 = require("./search");
46
+ const storage_1 = require("./storage");
47
+ const program = new commander_1.Command();
48
+ program
49
+ .name("graphra")
50
+ .description("Graphra — the universal code context engine for AI tools")
51
+ .version("1.0.0");
52
+ // ============================================
53
+ // Init — auto-detect and configure
54
+ // ============================================
55
+ program
56
+ .command("init")
57
+ .description("Auto-detect project language, framework, and structure")
58
+ .action(() => {
59
+ const { initProject } = require("./init");
60
+ const config = initProject(".");
61
+ console.log("\nšŸ”§ Graphra initialized!\n");
62
+ console.log(` Language: ${config.language.join(", ") || "unknown"}`);
63
+ console.log(` Framework: ${config.framework.join(", ") || "none detected"}`);
64
+ console.log(` Structure: ${config.structure}`);
65
+ console.log(` Include: ${config.include.join(", ")}`);
66
+ console.log(` Entry points: ${config.entryPoints.join(", ") || "none"}`);
67
+ console.log(`\n Config saved to .graphra/config.json`);
68
+ console.log(` Run \`Graphra generate\` to index your codebase.\n`);
69
+ });
70
+ // ============================================
71
+ // Generate
72
+ // ============================================
73
+ program
74
+ .command("generate")
75
+ .description("Scan, chunk, extract signatures, embed (neural), and build graph")
76
+ .option("-i, --include <patterns...>", "Glob patterns to include")
77
+ .option("-x, --ignore <patterns...>", "Glob patterns to ignore")
78
+ .option("--force", "Full rebuild (ignore cache)")
79
+ .action(async (opts) => {
80
+ try {
81
+ const startTime = Date.now();
82
+ const db = (0, storage_1.getDb)();
83
+ // Force mode: wipe everything
84
+ if (opts.force) {
85
+ console.log("šŸ—‘ļø Force mode: clearing all data...");
86
+ (0, storage_1.clearAll)();
87
+ }
88
+ console.log("šŸ” Scanning files...");
89
+ const files = await (0, scanner_1.scanFiles)({
90
+ include: opts.include,
91
+ ignore: opts.ignore,
92
+ });
93
+ console.log(` Found ${files.length} files`);
94
+ // --- Incremental: detect changed/new/deleted files ---
95
+ const currentFileSet = new Set(files.map((f) => path.resolve(f)));
96
+ const trackedFiles = new Set((0, storage_1.getTrackedFiles)());
97
+ // Files that were deleted since last run
98
+ const deletedFiles = [];
99
+ for (const tracked of trackedFiles) {
100
+ if (!currentFileSet.has(tracked)) {
101
+ deletedFiles.push(tracked);
102
+ }
103
+ }
104
+ // Files that are new or changed (mtime differs)
105
+ const changedFiles = [];
106
+ const unchangedFiles = [];
107
+ for (const file of files) {
108
+ const resolved = path.resolve(file);
109
+ const currentMtime = fs.statSync(file).mtimeMs;
110
+ const storedMtime = (0, storage_1.getFileMtime)(resolved);
111
+ if (storedMtime === null || currentMtime !== storedMtime) {
112
+ changedFiles.push(file);
113
+ }
114
+ else {
115
+ unchangedFiles.push(file);
116
+ }
117
+ }
118
+ // Remove deleted files
119
+ if (deletedFiles.length > 0) {
120
+ console.log(`šŸ—‘ļø Removing ${deletedFiles.length} deleted files...`);
121
+ for (const f of deletedFiles)
122
+ (0, storage_1.removeFile)(f);
123
+ }
124
+ if (changedFiles.length === 0) {
125
+ console.log("āœ… No changes detected — everything is up to date!");
126
+ // Still rebuild graph in case file relationships changed
127
+ console.log("šŸ”— Rebuilding dependency graph...");
128
+ const graph = (0, graph_1.buildGraph)(files);
129
+ (0, storage_1.saveGraph)(graph);
130
+ (0, storage_1.closeDb)();
131
+ const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
132
+ console.log(`āœ… Done in ${elapsed}s`);
133
+ return;
134
+ }
135
+ console.log(` šŸ“Š ${changedFiles.length} changed, ${unchangedFiles.length} cached, ${deletedFiles.length} deleted`);
136
+ // --- Chunk only changed files ---
137
+ console.log(`🧩 Chunking ${changedFiles.length} changed files...`);
138
+ const newChunks = [];
139
+ for (const file of changedFiles) {
140
+ const resolved = path.resolve(file);
141
+ try {
142
+ // Remove old chunks for this file
143
+ (0, storage_1.removeChunksForFile)(resolved);
144
+ const chunks = (0, chunker_1.chunkFile)(file);
145
+ for (const chunk of chunks) {
146
+ const signature = (0, signatureExtractor_1.extractSignature)(chunk);
147
+ const searchText = (0, signatureExtractor_1.buildSearchableText)(chunk, signature);
148
+ newChunks.push({
149
+ ...chunk,
150
+ signature,
151
+ summary: searchText,
152
+ });
153
+ }
154
+ // Update file tracking
155
+ const mtime = fs.statSync(file).mtimeMs;
156
+ (0, storage_1.upsertFile)(resolved, mtime, "");
157
+ }
158
+ catch (err) {
159
+ console.warn(` ⚠ Skipping ${path.basename(file)}: ${err}`);
160
+ }
161
+ }
162
+ if (newChunks.length > 0) {
163
+ (0, storage_1.upsertChunks)(newChunks);
164
+ }
165
+ const totalChunks = (0, storage_1.getChunkCount)();
166
+ console.log(` ${newChunks.length} new/updated chunks (${totalChunks} total)`);
167
+ // --- Embed only chunks that need it ---
168
+ const chunksNeedingEmbedding = (0, storage_1.getChunksWithoutEmbeddings)();
169
+ if (chunksNeedingEmbedding.length > 0) {
170
+ console.log(`🧠 Embedding ${chunksNeedingEmbedding.length} new chunks...`);
171
+ const embItems = [];
172
+ for (let i = 0; i < chunksNeedingEmbedding.length; i++) {
173
+ const chunkId = chunksNeedingEmbedding[i];
174
+ const chunk = (0, storage_1.getChunk)(chunkId);
175
+ if (!chunk)
176
+ continue;
177
+ const text = chunk.summary || chunk.signature || chunk.name;
178
+ try {
179
+ const vector = await (0, neuralEmbedder_1.embed)(text);
180
+ embItems.push({ chunkId, vector });
181
+ }
182
+ catch {
183
+ // Skip failed
184
+ }
185
+ if ((i + 1) % 50 === 0 || i === chunksNeedingEmbedding.length - 1) {
186
+ process.stdout.write(`\r Embedded ${i + 1}/${chunksNeedingEmbedding.length}`);
187
+ }
188
+ }
189
+ console.log("");
190
+ (0, storage_1.upsertEmbeddings)(embItems);
191
+ console.log(` ${embItems.length} embeddings stored`);
192
+ }
193
+ else {
194
+ console.log("🧠 All embeddings up to date");
195
+ }
196
+ // --- Dependency graph (always rebuild — it's fast) ---
197
+ console.log("šŸ”— Building dependency graph + PageRank...");
198
+ const graph = (0, graph_1.buildGraph)(files);
199
+ (0, storage_1.saveGraph)(graph);
200
+ const pageRank = (0, search_1.computePageRank)(graph);
201
+ const topFiles = Array.from(pageRank.entries())
202
+ .sort((a, b) => b[1] - a[1])
203
+ .slice(0, 5);
204
+ if (topFiles.length > 0) {
205
+ console.log(" Top files by importance:");
206
+ for (const [file, rank] of topFiles) {
207
+ const short = file.split(/[/\\]/).slice(-2).join("/");
208
+ console.log(` ${short} (${rank.toFixed(4)})`);
209
+ }
210
+ }
211
+ // DB size
212
+ const dbPath = path.join(".graphra", "graphra.db");
213
+ if (fs.existsSync(dbPath)) {
214
+ const size = fs.statSync(dbPath).size;
215
+ console.log(`\nšŸ’¾ DB size: ${(size / 1024 / 1024).toFixed(1)}MB`);
216
+ }
217
+ const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
218
+ console.log(`āœ… Done in ${elapsed}s`);
219
+ (0, storage_1.closeDb)();
220
+ }
221
+ catch (err) {
222
+ console.error("āŒ Generate failed:", err);
223
+ (0, storage_1.closeDb)();
224
+ process.exit(1);
225
+ }
226
+ });
227
+ // ============================================
228
+ // Search
229
+ // ============================================
230
+ program
231
+ .command("search <query>")
232
+ .description("Hybrid search: BM25 + neural embeddings + PageRank")
233
+ .option("-k, --top <number>", "Number of results", "10")
234
+ .action(async (queryText, opts) => {
235
+ try {
236
+ if ((0, storage_1.getChunkCount)() === 0) {
237
+ console.log("No data. Run `Graphra generate` first.");
238
+ (0, storage_1.closeDb)();
239
+ return;
240
+ }
241
+ const graph = (0, storage_1.loadGraph)();
242
+ const queryEmbedding = await (0, neuralEmbedder_1.embed)(queryText);
243
+ const results = (0, search_1.hybridSearch)(queryText, queryEmbedding, graph, {
244
+ topK: parseInt(opts.top),
245
+ });
246
+ if (results.length === 0) {
247
+ console.log("No results found.");
248
+ (0, storage_1.closeDb)();
249
+ return;
250
+ }
251
+ console.log(`\nšŸ”Ž Top ${results.length} results:\n`);
252
+ for (const r of results) {
253
+ const short = r.chunk.file.split(/[/\\]/).slice(-2).join("/");
254
+ const sig = r.chunk.signature || r.chunk.name;
255
+ console.log(` [${r.score.toFixed(3)}] ${r.chunk.name}`);
256
+ console.log(` ${short}`);
257
+ console.log(` ${sig}\n`);
258
+ }
259
+ (0, storage_1.closeDb)();
260
+ }
261
+ catch (err) {
262
+ console.error("āŒ Search failed:", err);
263
+ (0, storage_1.closeDb)();
264
+ process.exit(1);
265
+ }
266
+ });
267
+ // ============================================
268
+ // Context
269
+ // ============================================
270
+ program
271
+ .command("context <file>")
272
+ .description("Build context for a file + task, export in any format")
273
+ .requiredOption("-t, --task <task>", "Task description")
274
+ .option("-k, --top <number>", "Max context entries", "15")
275
+ .option("-f, --format <format>", "Output format: text, json, markdown, clipboard", "text")
276
+ .option("--tokens <number>", "Max token budget (auto-packs best context)")
277
+ .action(async (file, opts) => {
278
+ try {
279
+ if ((0, storage_1.getChunkCount)() === 0) {
280
+ console.log("No data. Run `Graphra generate` first.");
281
+ (0, storage_1.closeDb)();
282
+ return;
283
+ }
284
+ const graph = (0, storage_1.loadGraph)();
285
+ const resolvedFile = path.resolve(file);
286
+ const queryEmbedding = await (0, neuralEmbedder_1.embed)(opts.task);
287
+ // Get graph neighbors
288
+ const neighbors = new Set();
289
+ for (const [src, targets] of Object.entries(graph)) {
290
+ if (src === resolvedFile)
291
+ targets.forEach((t) => neighbors.add(t));
292
+ if (targets.includes(resolvedFile))
293
+ neighbors.add(src);
294
+ }
295
+ neighbors.add(resolvedFile);
296
+ // Hybrid search for task-relevant chunks
297
+ const results = (0, search_1.hybridSearch)(opts.task, queryEmbedding, graph, {
298
+ topK: parseInt(opts.top),
299
+ });
300
+ // Collect all context entries
301
+ const allChunks = (0, storage_1.getAllChunks)();
302
+ const neighborChunks = allChunks.filter((c) => neighbors.has(c.file));
303
+ const entries = [];
304
+ const seen = new Set();
305
+ // Graph neighbors first (architecture)
306
+ for (const c of neighborChunks.slice(0, 15)) {
307
+ seen.add(c.id);
308
+ entries.push({
309
+ file: c.file.split(/[/\\]/).slice(-2).join("/"),
310
+ name: c.name,
311
+ signature: c.signature,
312
+ score: 1.0,
313
+ source: "graph",
314
+ });
315
+ }
316
+ // Then search results
317
+ for (const r of results) {
318
+ if (seen.has(r.chunk.id))
319
+ continue;
320
+ seen.add(r.chunk.id);
321
+ entries.push({
322
+ file: r.chunk.file.split(/[/\\]/).slice(-2).join("/"),
323
+ name: r.chunk.name,
324
+ signature: r.chunk.signature || r.chunk.name,
325
+ score: r.score,
326
+ source: "search",
327
+ });
328
+ }
329
+ // --- Token budget packing ---
330
+ let packedEntries = entries;
331
+ if (opts.tokens) {
332
+ const maxTokens = parseInt(opts.tokens);
333
+ packedEntries = [];
334
+ let tokenCount = 0;
335
+ for (const e of entries) {
336
+ const entryTokens = Math.ceil(e.signature.length / 4) + 10; // rough estimate
337
+ if (tokenCount + entryTokens > maxTokens)
338
+ break;
339
+ packedEntries.push(e);
340
+ tokenCount += entryTokens;
341
+ }
342
+ }
343
+ // --- Format output ---
344
+ const format = opts.format.toLowerCase();
345
+ if (format === "json") {
346
+ const output = {
347
+ task: opts.task,
348
+ file: resolvedFile,
349
+ entries: packedEntries,
350
+ totalEntries: packedEntries.length,
351
+ };
352
+ console.log(JSON.stringify(output, null, 2));
353
+ }
354
+ else if (format === "markdown" || format === "md") {
355
+ console.log(`# Context for: ${opts.task}\n`);
356
+ console.log(`**Target file:** \`${file}\`\n`);
357
+ console.log("## Architecture (dependencies)\n");
358
+ for (const e of packedEntries.filter((e) => e.source === "graph")) {
359
+ console.log(`- \`${e.file}\`: \`${e.signature}\``);
360
+ }
361
+ console.log("\n## Relevant code\n");
362
+ for (const e of packedEntries.filter((e) => e.source === "search")) {
363
+ console.log(`- **${e.name}** (\`${e.file}\`): \`${e.signature}\``);
364
+ }
365
+ console.log(`\n## Task\n\n${opts.task}`);
366
+ }
367
+ else if (format === "clipboard") {
368
+ // Build a clean prompt ready to paste into any AI
369
+ let prompt = `I'm working on the file \`${file}\` in a codebase. Here's the relevant context:\n\n`;
370
+ prompt += "## Codebase Architecture\n\n";
371
+ for (const e of packedEntries.filter((e) => e.source === "graph")) {
372
+ prompt += `${e.file}: ${e.signature}\n`;
373
+ }
374
+ prompt += "\n## Related Code\n\n";
375
+ for (const e of packedEntries.filter((e) => e.source === "search")) {
376
+ prompt += `${e.file}: ${e.signature}\n`;
377
+ }
378
+ prompt += `\n## Task\n\n${opts.task}`;
379
+ // Copy to clipboard
380
+ try {
381
+ const { execSync } = require("child_process");
382
+ if (process.platform === "win32") {
383
+ execSync("clip", { input: prompt });
384
+ }
385
+ else if (process.platform === "darwin") {
386
+ execSync("pbcopy", { input: prompt });
387
+ }
388
+ else {
389
+ execSync("xclip -selection clipboard", { input: prompt });
390
+ }
391
+ console.log(`šŸ“‹ Context copied to clipboard! (${packedEntries.length} entries)`);
392
+ console.log(" Paste into ChatGPT, Claude, Cursor, or any AI tool.");
393
+ }
394
+ catch {
395
+ // Fallback: print to stdout
396
+ console.log(prompt);
397
+ }
398
+ }
399
+ else {
400
+ // Default: text format
401
+ console.log("ARCH:");
402
+ for (const e of packedEntries.filter((e) => e.source === "graph")) {
403
+ console.log(` ${e.file}: ${e.signature}`);
404
+ }
405
+ console.log("\nCONTEXT:");
406
+ for (const e of packedEntries.filter((e) => e.source === "search")) {
407
+ console.log(` ${e.file}: ${e.signature}`);
408
+ }
409
+ console.log(`\nTASK:\n ${opts.task}`);
410
+ }
411
+ (0, storage_1.closeDb)();
412
+ }
413
+ catch (err) {
414
+ console.error("āŒ Context failed:", err);
415
+ (0, storage_1.closeDb)();
416
+ process.exit(1);
417
+ }
418
+ });
419
+ // ============================================
420
+ // Diff — context for a PR/branch
421
+ // ============================================
422
+ program
423
+ .command("diff")
424
+ .description("Generate context for changed files in current branch/PR")
425
+ .option("-b, --base <branch>", "Base branch to diff against", "main")
426
+ .option("-t, --task <task>", "Task description", "Review these changes")
427
+ .option("-f, --format <format>", "Output format: text, json, markdown, clipboard", "markdown")
428
+ .action(async (opts) => {
429
+ try {
430
+ if ((0, storage_1.getChunkCount)() === 0) {
431
+ console.log("No data. Run `Graphra generate` first.");
432
+ (0, storage_1.closeDb)();
433
+ return;
434
+ }
435
+ // Get changed files from git
436
+ const { execSync } = require("child_process");
437
+ let changedFiles = [];
438
+ try {
439
+ const diffOutput = execSync(`git diff --name-only ${opts.base}...HEAD`, { encoding: "utf-8", timeout: 5000 }).trim();
440
+ if (diffOutput)
441
+ changedFiles = diffOutput.split("\n").filter((f) => f.endsWith(".ts") || f.endsWith(".js"));
442
+ }
443
+ catch {
444
+ // Fallback: uncommitted changes
445
+ try {
446
+ const statusOutput = execSync("git diff --name-only", { encoding: "utf-8", timeout: 5000 }).trim();
447
+ if (statusOutput)
448
+ changedFiles = statusOutput.split("\n").filter((f) => f.endsWith(".ts") || f.endsWith(".js"));
449
+ }
450
+ catch {
451
+ console.log("āŒ Not a git repository or git not available.");
452
+ (0, storage_1.closeDb)();
453
+ return;
454
+ }
455
+ }
456
+ if (changedFiles.length === 0) {
457
+ console.log("No changed .ts/.js files found.");
458
+ (0, storage_1.closeDb)();
459
+ return;
460
+ }
461
+ console.log(`šŸ“ ${changedFiles.length} changed files:\n`);
462
+ changedFiles.forEach((f) => console.log(` ${f}`));
463
+ // Find chunks in changed files + their neighbors
464
+ const allChunks = (0, storage_1.getAllChunks)();
465
+ const graph = (0, storage_1.loadGraph)();
466
+ const changedSet = new Set(changedFiles.map((f) => path.resolve(f)));
467
+ const changedChunks = allChunks.filter((c) => {
468
+ for (const cf of changedSet) {
469
+ if (c.file === cf || c.file.endsWith(cf.replace(/\\/g, "/")) || cf.endsWith(c.file.replace(/\\/g, "/")))
470
+ return true;
471
+ }
472
+ return false;
473
+ });
474
+ // Get neighbor files for context
475
+ const neighborFiles = new Set();
476
+ for (const cf of changedSet) {
477
+ for (const [src, targets] of Object.entries(graph)) {
478
+ if (src === cf)
479
+ targets.forEach((t) => neighborFiles.add(t));
480
+ if (targets.includes(cf))
481
+ neighborFiles.add(src);
482
+ }
483
+ }
484
+ const neighborChunks = allChunks.filter((c) => neighborFiles.has(c.file) && !changedSet.has(c.file));
485
+ // Also do a semantic search for the task
486
+ const queryEmbedding = await (0, neuralEmbedder_1.embed)(opts.task);
487
+ const searchResults = (0, search_1.hybridSearch)(opts.task, queryEmbedding, graph, { topK: 10 });
488
+ // Format output
489
+ const format = opts.format.toLowerCase();
490
+ if (format === "markdown" || format === "md") {
491
+ console.log(`\n# PR Context: ${opts.task}\n`);
492
+ console.log(`## Changed files (${changedChunks.length} chunks)\n`);
493
+ for (const c of changedChunks) {
494
+ const short = c.file.split(/[/\\]/).slice(-2).join("/");
495
+ console.log(`- \`${short}\`: \`${c.signature}\``);
496
+ }
497
+ console.log(`\n## Dependencies (${neighborChunks.length} chunks)\n`);
498
+ for (const c of neighborChunks.slice(0, 20)) {
499
+ const short = c.file.split(/[/\\]/).slice(-2).join("/");
500
+ console.log(`- \`${short}\`: \`${c.signature}\``);
501
+ }
502
+ console.log(`\n## Related code\n`);
503
+ const seen = new Set([...changedChunks, ...neighborChunks].map((c) => c.id));
504
+ for (const r of searchResults) {
505
+ if (seen.has(r.chunk.id))
506
+ continue;
507
+ const short = r.chunk.file.split(/[/\\]/).slice(-2).join("/");
508
+ console.log(`- **${r.chunk.name}** (\`${short}\`): \`${r.chunk.signature}\``);
509
+ }
510
+ }
511
+ else if (format === "json") {
512
+ console.log(JSON.stringify({
513
+ task: opts.task,
514
+ base: opts.base,
515
+ changedFiles,
516
+ changedChunks: changedChunks.map((c) => ({ name: c.name, file: c.file, signature: c.signature })),
517
+ neighborChunks: neighborChunks.slice(0, 20).map((c) => ({ name: c.name, file: c.file, signature: c.signature })),
518
+ }, null, 2));
519
+ }
520
+ else {
521
+ console.log(`\nCHANGED:\n`);
522
+ for (const c of changedChunks) {
523
+ console.log(` ${c.file.split(/[/\\]/).slice(-2).join("/")}: ${c.signature}`);
524
+ }
525
+ console.log(`\nDEPENDENCIES:\n`);
526
+ for (const c of neighborChunks.slice(0, 20)) {
527
+ console.log(` ${c.file.split(/[/\\]/).slice(-2).join("/")}: ${c.signature}`);
528
+ }
529
+ }
530
+ (0, storage_1.closeDb)();
531
+ }
532
+ catch (err) {
533
+ console.error("āŒ Diff failed:", err);
534
+ (0, storage_1.closeDb)();
535
+ process.exit(1);
536
+ }
537
+ });
538
+ // ============================================
539
+ // Stats
540
+ // ============================================
541
+ program
542
+ .command("stats")
543
+ .description("Show database statistics")
544
+ .action(() => {
545
+ try {
546
+ const db = (0, storage_1.getDb)();
547
+ const chunks = db.prepare("SELECT COUNT(*) as c FROM chunks").get().c;
548
+ const embs = db.prepare("SELECT COUNT(*) as c FROM embeddings").get().c;
549
+ const edges = db.prepare("SELECT COUNT(*) as c FROM graph").get().c;
550
+ const files = db.prepare("SELECT COUNT(DISTINCT file) as c FROM chunks").get().c;
551
+ console.log("\nšŸ“Š Graphra v2 Stats:\n");
552
+ console.log(` Files: ${files}`);
553
+ console.log(` Chunks: ${chunks}`);
554
+ console.log(` Embeddings: ${embs} (384-dim neural)`);
555
+ console.log(` Graph edges: ${edges}`);
556
+ const dbPath = path.join(".graphra", "graphra.db");
557
+ if (fs.existsSync(dbPath)) {
558
+ console.log(` DB size: ${(fs.statSync(dbPath).size / 1024 / 1024).toFixed(1)}MB`);
559
+ }
560
+ const types = db.prepare("SELECT type, COUNT(*) as c FROM chunks GROUP BY type ORDER BY c DESC").all();
561
+ console.log("\n By type:");
562
+ for (const t of types)
563
+ console.log(` ${t.type}: ${t.c}`);
564
+ (0, storage_1.closeDb)();
565
+ }
566
+ catch (err) {
567
+ console.error("āŒ Stats failed:", err);
568
+ (0, storage_1.closeDb)();
569
+ }
570
+ });
571
+ // ============================================
572
+ // Explain — auto-generate architecture overview
573
+ // ============================================
574
+ program
575
+ .command("explain")
576
+ .description("Auto-generate a natural language architecture overview")
577
+ .action(() => {
578
+ try {
579
+ if ((0, storage_1.getChunkCount)() === 0) {
580
+ console.log("No data. Run `Graphra generate` first.");
581
+ (0, storage_1.closeDb)();
582
+ return;
583
+ }
584
+ const db = (0, storage_1.getDb)();
585
+ const graph = (0, storage_1.loadGraph)();
586
+ const allChunks = (0, storage_1.getAllChunks)();
587
+ // Group chunks by file
588
+ const fileMap = new Map();
589
+ for (const c of allChunks) {
590
+ const short = c.file.split(/[/\\]/).slice(-2).join("/");
591
+ if (!fileMap.has(short))
592
+ fileMap.set(short, []);
593
+ fileMap.get(short).push(c);
594
+ }
595
+ // Detect layers/patterns
596
+ const layers = {
597
+ controllers: [],
598
+ services: [],
599
+ "data access (DAL/models)": [],
600
+ routes: [],
601
+ middleware: [],
602
+ utilities: [],
603
+ types: [],
604
+ config: [],
605
+ tests: [],
606
+ other: [],
607
+ };
608
+ for (const [file, chunks] of fileMap) {
609
+ const lower = file.toLowerCase();
610
+ if (lower.includes("controller"))
611
+ layers.controllers.push(file);
612
+ else if (lower.includes("service"))
613
+ layers.services.push(file);
614
+ else if (lower.includes("dal") || lower.includes("model") || lower.includes("repository"))
615
+ layers["data access (DAL/models)"].push(file);
616
+ else if (lower.includes("route"))
617
+ layers.routes.push(file);
618
+ else if (lower.includes("middleware") || lower.includes("validator") || lower.includes("validation"))
619
+ layers.middleware.push(file);
620
+ else if (lower.includes("util") || lower.includes("helper") || lower.includes("lib"))
621
+ layers.utilities.push(file);
622
+ else if (lower.includes("type") || lower.includes("interface"))
623
+ layers.types.push(file);
624
+ else if (lower.includes("config") || lower.includes("constant"))
625
+ layers.config.push(file);
626
+ else if (lower.includes("test") || lower.includes("spec"))
627
+ layers.tests.push(file);
628
+ else
629
+ layers.other.push(file);
630
+ }
631
+ // PageRank for importance
632
+ const { computePageRank } = require("./search");
633
+ const pageRank = computePageRank(graph);
634
+ const topFiles = Array.from(pageRank.entries())
635
+ .sort((a, b) => b[1] - a[1])
636
+ .slice(0, 10);
637
+ // Chunk type stats
638
+ const typeStats = db.prepare("SELECT type, COUNT(*) as c FROM chunks GROUP BY type ORDER BY c DESC").all();
639
+ const fileCount = db.prepare("SELECT COUNT(DISTINCT file) as c FROM chunks").get().c;
640
+ // Output
641
+ console.log("# šŸ—ļø Architecture Overview\n");
642
+ console.log(`This codebase has **${fileCount} files** with **${allChunks.length} code elements**.\n`);
643
+ console.log("## Code composition\n");
644
+ for (const t of typeStats) {
645
+ console.log(`- ${t.c} ${t.type}s`);
646
+ }
647
+ console.log("\n## Layers detected\n");
648
+ for (const [layer, files] of Object.entries(layers)) {
649
+ if (files.length === 0)
650
+ continue;
651
+ console.log(`### ${layer.charAt(0).toUpperCase() + layer.slice(1)} (${files.length} files)\n`);
652
+ for (const f of files.slice(0, 8)) {
653
+ const chunks = fileMap.get(f) ?? [];
654
+ const names = chunks.slice(0, 5).map((c) => c.name).join(", ");
655
+ console.log(`- \`${f}\`: ${names}${chunks.length > 5 ? ` (+${chunks.length - 5} more)` : ""}`);
656
+ }
657
+ if (files.length > 8)
658
+ console.log(`- ... and ${files.length - 8} more files`);
659
+ console.log("");
660
+ }
661
+ console.log("## Most important files (PageRank)\n");
662
+ for (const [file, rank] of topFiles) {
663
+ const short = file.split(/[/\\]/).slice(-2).join("/");
664
+ const chunks = allChunks.filter((c) => c.file === file);
665
+ console.log(`- \`${short}\` (importance: ${rank.toFixed(4)}) — ${chunks.length} chunks`);
666
+ }
667
+ console.log("\n## Dependency flow\n");
668
+ const edgeCount = db.prepare("SELECT COUNT(*) as c FROM graph").get().c;
669
+ console.log(`${edgeCount} import/require edges connecting ${fileCount} files.\n`);
670
+ // Show top dependency chains
671
+ const topImporters = Object.entries(graph)
672
+ .sort((a, b) => b[1].length - a[1].length)
673
+ .slice(0, 5);
674
+ for (const [file, deps] of topImporters) {
675
+ const short = file.split(/[/\\]/).slice(-2).join("/");
676
+ console.log(`- \`${short}\` imports ${deps.length} files`);
677
+ }
678
+ (0, storage_1.closeDb)();
679
+ }
680
+ catch (err) {
681
+ console.error("āŒ Explain failed:", err);
682
+ (0, storage_1.closeDb)();
683
+ }
684
+ });
685
+ // ============================================
686
+ // Serve — background server mode (like ESLint daemon)
687
+ // ============================================
688
+ program
689
+ .command("serve")
690
+ .description("Start Graphra MCP server (alias for `Graphra mcp`)")
691
+ .action(async () => {
692
+ if ((0, storage_1.getChunkCount)() === 0) {
693
+ console.log("No data. Run `Graphra generate` first.");
694
+ (0, storage_1.closeDb)();
695
+ return;
696
+ }
697
+ const { startMcpServer } = await Promise.resolve().then(() => __importStar(require("./mcpServer")));
698
+ await startMcpServer();
699
+ });
700
+ // ============================================
701
+ // MCP — start as MCP stdio server
702
+ // ============================================
703
+ program
704
+ .command("mcp")
705
+ .description("Start as an MCP server (stdio transport) for Claude/Cursor/VS Code")
706
+ .action(async () => {
707
+ const { startMcpServer } = await Promise.resolve().then(() => __importStar(require("./mcpServer")));
708
+ await startMcpServer();
709
+ });
710
+ // ============================================
711
+ // Setup — generate config for AI tools
712
+ // ============================================
713
+ program
714
+ .command("setup")
715
+ .description("Generate configuration for Claude Desktop, Cursor, or VS Code")
716
+ .option("--claude", "Generate Claude Desktop config")
717
+ .option("--cursor", "Generate Cursor config")
718
+ .option("--vscode", "Generate VS Code MCP config")
719
+ .option("--all", "Generate config for all tools")
720
+ .action((opts) => {
721
+ const cwd = process.cwd();
722
+ const nodeExe = process.execPath;
723
+ // Use the built JS file if available, otherwise ts-node
724
+ const mcpScript = fs.existsSync(path.join(__dirname, "../dist/mcp.js"))
725
+ ? path.join(__dirname, "../dist/mcp.js")
726
+ : path.join(__dirname, "mcp.ts");
727
+ const isTs = mcpScript.endsWith(".ts");
728
+ const command = isTs ? "npx" : nodeExe;
729
+ const args = isTs ? ["ts-node", mcpScript] : [mcpScript];
730
+ const showAll = opts.all || (!opts.claude && !opts.cursor && !opts.vscode);
731
+ if (opts.claude || showAll) {
732
+ console.log("\nšŸ“‹ Claude Desktop — add to claude_desktop_config.json:\n");
733
+ const config = {
734
+ mcpServers: {
735
+ Graphra: {
736
+ command,
737
+ args,
738
+ cwd,
739
+ },
740
+ },
741
+ };
742
+ console.log(JSON.stringify(config, null, 2));
743
+ console.log(`\n Config file location:`);
744
+ console.log(` Windows: %APPDATA%\\Claude\\claude_desktop_config.json`);
745
+ console.log(` macOS: ~/Library/Application Support/Claude/claude_desktop_config.json\n`);
746
+ }
747
+ if (opts.cursor || showAll) {
748
+ console.log("\nšŸ“‹ Cursor — add to .cursor/mcp.json in your project:\n");
749
+ const cursorConfig = {
750
+ mcpServers: {
751
+ Graphra: {
752
+ command,
753
+ args,
754
+ cwd,
755
+ },
756
+ },
757
+ };
758
+ const cursorDir = path.join(cwd, ".cursor");
759
+ if (!fs.existsSync(cursorDir))
760
+ fs.mkdirSync(cursorDir, { recursive: true });
761
+ fs.writeFileSync(path.join(cursorDir, "mcp.json"), JSON.stringify(cursorConfig, null, 2));
762
+ console.log(` āœ… Written to .cursor/mcp.json\n`);
763
+ }
764
+ if (opts.vscode || showAll) {
765
+ console.log("\nšŸ“‹ VS Code — add to .vscode/mcp.json:\n");
766
+ const vscodeConfig = {
767
+ servers: {
768
+ Graphra: {
769
+ command,
770
+ args,
771
+ cwd,
772
+ },
773
+ },
774
+ };
775
+ const vscodeDir = path.join(cwd, ".vscode");
776
+ if (!fs.existsSync(vscodeDir))
777
+ fs.mkdirSync(vscodeDir, { recursive: true });
778
+ fs.writeFileSync(path.join(vscodeDir, "mcp.json"), JSON.stringify(vscodeConfig, null, 2));
779
+ console.log(` āœ… Written to .vscode/mcp.json\n`);
780
+ }
781
+ // Always generate .github/copilot-instructions.md — Copilot reads this on EVERY message
782
+ try {
783
+ const allChunks = (0, storage_1.getAllChunks)();
784
+ if (allChunks.length > 0) {
785
+ const githubDir = path.join(cwd, ".github");
786
+ if (!fs.existsSync(githubDir))
787
+ fs.mkdirSync(githubDir, { recursive: true });
788
+ // Build a compact architecture summary
789
+ const fileMap = new Map();
790
+ for (const c of allChunks) {
791
+ const short = c.file.split(/[/\\]/).slice(-2).join("/");
792
+ if (!fileMap.has(short))
793
+ fileMap.set(short, []);
794
+ fileMap.get(short).push(c.name);
795
+ }
796
+ let instructions = `# Codebase Context (auto-generated by Graphra)\n\n`;
797
+ instructions += `This project has ${allChunks.length} code elements across ${fileMap.size} files.\n\n`;
798
+ instructions += `## Key files and their exports\n\n`;
799
+ // Show top 30 files with their exports
800
+ const sorted = Array.from(fileMap.entries())
801
+ .sort((a, b) => b[1].length - a[1].length)
802
+ .slice(0, 30);
803
+ for (const [file, names] of sorted) {
804
+ const display = names.slice(0, 8).join(", ");
805
+ const more = names.length > 8 ? ` (+${names.length - 8} more)` : "";
806
+ instructions += `- \`${file}\`: ${display}${more}\n`;
807
+ }
808
+ instructions += `\n## Rules\n\n`;
809
+ instructions += `- Before writing code, check if a similar function already exists above\n`;
810
+ instructions += `- Follow existing patterns — use the same helpers, services, and types\n`;
811
+ instructions += `- This project uses Graphra MCP tools — call Graphra_auto for detailed context\n`;
812
+ fs.writeFileSync(path.join(githubDir, "copilot-instructions.md"), instructions);
813
+ console.log("šŸ“‹ Generated .github/copilot-instructions.md (Copilot reads this on every message)");
814
+ }
815
+ (0, storage_1.closeDb)();
816
+ }
817
+ catch { /* non-critical */ }
818
+ console.log("\nšŸŽ‰ After adding the config, restart your AI tool and Graphra tools will appear automatically!");
819
+ });
820
+ program.parse();
821
+ //# sourceMappingURL=cli.js.map