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.
- package/LICENSE +21 -0
- package/README.md +463 -0
- package/dist/chunker.d.ts +3 -0
- package/dist/chunker.d.ts.map +1 -0
- package/dist/chunker.js +116 -0
- package/dist/chunker.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +821 -0
- package/dist/cli.js.map +1 -0
- package/dist/graph.d.ts +9 -0
- package/dist/graph.d.ts.map +1 -0
- package/dist/graph.js +97 -0
- package/dist/graph.js.map +1 -0
- package/dist/init.d.ts +27 -0
- package/dist/init.d.ts.map +1 -0
- package/dist/init.js +306 -0
- package/dist/init.js.map +1 -0
- package/dist/mcp.d.ts +13 -0
- package/dist/mcp.d.ts.map +1 -0
- package/dist/mcp.js +19 -0
- package/dist/mcp.js.map +1 -0
- package/dist/mcpServer.d.ts +14 -0
- package/dist/mcpServer.d.ts.map +1 -0
- package/dist/mcpServer.js +373 -0
- package/dist/mcpServer.js.map +1 -0
- package/dist/neuralEmbedder.d.ts +21 -0
- package/dist/neuralEmbedder.d.ts.map +1 -0
- package/dist/neuralEmbedder.js +98 -0
- package/dist/neuralEmbedder.js.map +1 -0
- package/dist/scanner.d.ts +3 -0
- package/dist/scanner.d.ts.map +1 -0
- package/dist/scanner.js +43 -0
- package/dist/scanner.js.map +1 -0
- package/dist/search.d.ts +37 -0
- package/dist/search.d.ts.map +1 -0
- package/dist/search.js +252 -0
- package/dist/search.js.map +1 -0
- package/dist/signatureExtractor.d.ts +25 -0
- package/dist/signatureExtractor.d.ts.map +1 -0
- package/dist/signatureExtractor.js +173 -0
- package/dist/signatureExtractor.js.map +1 -0
- package/dist/storage.d.ts +59 -0
- package/dist/storage.d.ts.map +1 -0
- package/dist/storage.js +322 -0
- package/dist/storage.js.map +1 -0
- package/dist/tokenBudget.d.ts +52 -0
- package/dist/tokenBudget.d.ts.map +1 -0
- package/dist/tokenBudget.js +175 -0
- package/dist/tokenBudget.js.map +1 -0
- package/dist/types.d.ts +62 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +6 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/hash.d.ts +6 -0
- package/dist/utils/hash.d.ts.map +1 -0
- package/dist/utils/hash.js +45 -0
- package/dist/utils/hash.js.map +1 -0
- 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
|