@leanlabsinnov/codegraph 0.1.2 → 0.1.4
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/README.md +87 -21
- package/dist/bin.js +4 -4
- package/dist/{chunk-36AWRLQ6.js → chunk-64OLVRK3.js} +1682 -211
- package/dist/chunk-64OLVRK3.js.map +1 -0
- package/dist/{chunk-2TORJYBO.js → chunk-AVQNLB4I.js} +22 -4
- package/dist/chunk-AVQNLB4I.js.map +1 -0
- package/dist/{chunk-XGPZDCQ4.js → chunk-POGEFB23.js} +17 -2
- package/dist/chunk-POGEFB23.js.map +1 -0
- package/dist/{chunk-B2TIVKUB.js → chunk-WXBYICTK.js} +10 -7
- package/dist/chunk-WXBYICTK.js.map +1 -0
- package/dist/index.js +4 -4
- package/dist/{src-PDNTANJD.js → src-BTVJBGZ5.js} +3 -3
- package/dist/src-DBJQ22XR.js +10 -0
- package/package.json +2 -1
- package/viewer/index.html +1466 -0
- package/dist/chunk-2TORJYBO.js.map +0 -1
- package/dist/chunk-36AWRLQ6.js.map +0 -1
- package/dist/chunk-B2TIVKUB.js.map +0 -1
- package/dist/chunk-XGPZDCQ4.js.map +0 -1
- package/dist/src-UVET6JHH.js +0 -10
- /package/dist/{src-PDNTANJD.js.map → src-BTVJBGZ5.js.map} +0 -0
- /package/dist/{src-UVET6JHH.js.map → src-DBJQ22XR.js.map} +0 -0
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
import {
|
|
2
2
|
createLlmRouter,
|
|
3
3
|
namespaceLabel
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-WXBYICTK.js";
|
|
5
5
|
import {
|
|
6
6
|
GraphDb,
|
|
7
7
|
defaultDbPath
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-AVQNLB4I.js";
|
|
9
9
|
import {
|
|
10
10
|
DEFAULT_CONFIG,
|
|
11
|
+
EDGE_KINDS,
|
|
11
12
|
LLM_PRESETS,
|
|
12
13
|
makeFileId,
|
|
13
14
|
makeNodeId
|
|
14
|
-
} from "./chunk-
|
|
15
|
+
} from "./chunk-POGEFB23.js";
|
|
15
16
|
|
|
16
17
|
// src/config-store.ts
|
|
17
18
|
import { mkdir, readFile, writeFile } from "fs/promises";
|
|
@@ -186,14 +187,15 @@ function friendlyHint(message) {
|
|
|
186
187
|
}
|
|
187
188
|
return null;
|
|
188
189
|
}
|
|
189
|
-
function renderServeBanner(url, tokenHint) {
|
|
190
|
-
const
|
|
191
|
-
`${kleur.green("\u2713")} codegraph mcp listening on ${kleur.cyan(url)}
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
kleur.
|
|
195
|
-
|
|
196
|
-
|
|
190
|
+
function renderServeBanner(url, tokenHint, viewerUrl) {
|
|
191
|
+
const lines = [
|
|
192
|
+
`${kleur.green("\u2713")} codegraph mcp listening on ${kleur.cyan(url)}`
|
|
193
|
+
];
|
|
194
|
+
if (viewerUrl) {
|
|
195
|
+
lines.push(`${kleur.green("\u2713")} graph viewer on ${kleur.cyan(viewerUrl)}`);
|
|
196
|
+
}
|
|
197
|
+
lines.push("", kleur.dim(tokenHint), kleur.dim("Ctrl-C to stop."));
|
|
198
|
+
return boxen(lines.join("\n"), {
|
|
197
199
|
padding: { top: 0, bottom: 0, left: 2, right: 2 },
|
|
198
200
|
margin: { top: 1, bottom: 1, left: 0, right: 0 },
|
|
199
201
|
borderColor: "green",
|
|
@@ -205,7 +207,7 @@ function renderServeBanner(url, tokenHint) {
|
|
|
205
207
|
import { Command } from "commander";
|
|
206
208
|
|
|
207
209
|
// src/commands/config.ts
|
|
208
|
-
import { password, select } from "@inquirer/prompts";
|
|
210
|
+
import { input, password, select } from "@inquirer/prompts";
|
|
209
211
|
import kleur2 from "kleur";
|
|
210
212
|
async function runConfigShow() {
|
|
211
213
|
const config = await loadConfig();
|
|
@@ -231,10 +233,17 @@ async function runConfigLlmSet(presetArg) {
|
|
|
231
233
|
}
|
|
232
234
|
const config = await loadConfig();
|
|
233
235
|
config.llm = { ...config.llm, ...lookup };
|
|
236
|
+
if (preset !== "local-openai-compatible") {
|
|
237
|
+
config.llm.baseUrl = void 0;
|
|
238
|
+
}
|
|
239
|
+
if (!presetArg && preset === "local-openai-compatible") {
|
|
240
|
+
const url = await promptBaseUrl();
|
|
241
|
+
if (url) config.llm.baseUrl = url;
|
|
242
|
+
}
|
|
234
243
|
await saveConfig(config);
|
|
235
244
|
console.log(kleur2.green(`\u2713 saved preset "${preset}" to ${configPath()}`));
|
|
236
245
|
console.log(kleur2.dim(`embedding namespace: ${namespaceLabel(config.llm)}`));
|
|
237
|
-
if (!presetArg) {
|
|
246
|
+
if (!presetArg && preset !== "local-openai-compatible") {
|
|
238
247
|
await maybePromptForApiKey(preset);
|
|
239
248
|
}
|
|
240
249
|
}
|
|
@@ -280,11 +289,28 @@ function describePreset(id) {
|
|
|
280
289
|
case "byo-google":
|
|
281
290
|
return "Gemini for gen + Google embeddings";
|
|
282
291
|
case "local-ollama":
|
|
283
|
-
return "Fully local via Ollama (qwen2.5-coder + nomic-embed-text)";
|
|
292
|
+
return "Fully local via Ollama (qwen2.5-coder:1.5b + nomic-embed-text)";
|
|
293
|
+
case "local-openai-compatible":
|
|
294
|
+
return "Any OpenAI-compatible server (llama.cpp, LM Studio, vLLM) \u2014 you supply the URL";
|
|
284
295
|
default:
|
|
285
296
|
return "";
|
|
286
297
|
}
|
|
287
298
|
}
|
|
299
|
+
async function promptBaseUrl() {
|
|
300
|
+
const url = await input({
|
|
301
|
+
message: "Server base URL (e.g. http://localhost:8080/v1):",
|
|
302
|
+
validate: (v) => v.trim() === "" || v.trim().startsWith("http") ? true : "Must be an http(s) URL (or leave empty to set later)"
|
|
303
|
+
});
|
|
304
|
+
const trimmed = url.trim();
|
|
305
|
+
if (!trimmed) {
|
|
306
|
+
console.log(
|
|
307
|
+
kleur2.yellow(
|
|
308
|
+
"! No URL saved. Edit ~/.codegraph/config.json to set llm.baseUrl before indexing."
|
|
309
|
+
)
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
return trimmed;
|
|
313
|
+
}
|
|
288
314
|
async function runConfigLlmTest() {
|
|
289
315
|
const config = await loadConfig();
|
|
290
316
|
console.log(kleur2.dim("testing llm config:"));
|
|
@@ -314,6 +340,43 @@ async function runConfigLlmTest() {
|
|
|
314
340
|
import { access, constants, mkdir as mkdir2 } from "fs/promises";
|
|
315
341
|
import { dirname } from "path";
|
|
316
342
|
import kleur3 from "kleur";
|
|
343
|
+
|
|
344
|
+
// src/ollama.ts
|
|
345
|
+
import { execFile } from "child_process";
|
|
346
|
+
import { promisify } from "util";
|
|
347
|
+
var execFileAsync = promisify(execFile);
|
|
348
|
+
var LOCAL_MODELS = {
|
|
349
|
+
generation: "qwen2.5-coder:1.5b",
|
|
350
|
+
embeddings: "nomic-embed-text"
|
|
351
|
+
};
|
|
352
|
+
async function detectOllama() {
|
|
353
|
+
try {
|
|
354
|
+
const res = await fetch("http://localhost:11434/api/tags", {
|
|
355
|
+
signal: AbortSignal.timeout(800)
|
|
356
|
+
});
|
|
357
|
+
if (res.ok) {
|
|
358
|
+
const data = await res.json();
|
|
359
|
+
const models = (data.models ?? []).map((m) => m.name);
|
|
360
|
+
const neededPrefixes = [
|
|
361
|
+
LOCAL_MODELS.generation.split(":")[0],
|
|
362
|
+
LOCAL_MODELS.embeddings
|
|
363
|
+
];
|
|
364
|
+
const hasAll = neededPrefixes.every(
|
|
365
|
+
(prefix) => models.some((m) => m.startsWith(prefix))
|
|
366
|
+
);
|
|
367
|
+
return hasAll ? { status: "ready", models } : { status: "running-no-models", models };
|
|
368
|
+
}
|
|
369
|
+
} catch {
|
|
370
|
+
}
|
|
371
|
+
try {
|
|
372
|
+
await execFileAsync("ollama", ["--version"], { timeout: 2e3 });
|
|
373
|
+
return { status: "installed-not-running" };
|
|
374
|
+
} catch {
|
|
375
|
+
}
|
|
376
|
+
return { status: "not-installed" };
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// src/commands/doctor.ts
|
|
317
380
|
async function runDoctorCommand() {
|
|
318
381
|
const checks = [];
|
|
319
382
|
checks.push(checkNodeVersion());
|
|
@@ -336,6 +399,9 @@ async function runDoctorCommand() {
|
|
|
336
399
|
checks.push(checkApiKey(config.llm.embeddings.provider));
|
|
337
400
|
}
|
|
338
401
|
}
|
|
402
|
+
if (config && config.llm.generation.provider === "ollama") {
|
|
403
|
+
checks.push(await checkOllamaDaemon());
|
|
404
|
+
}
|
|
339
405
|
if (config) {
|
|
340
406
|
checks.push(await selfTestLlm(config));
|
|
341
407
|
checks.push(await selfTestKuzu(dbPath, config.llm.embeddings.dimension));
|
|
@@ -374,20 +440,20 @@ function checkNodeVersion() {
|
|
|
374
440
|
detail: `v${version} (codegraph requires >= 20)`
|
|
375
441
|
};
|
|
376
442
|
}
|
|
377
|
-
async function checkWritable(name,
|
|
443
|
+
async function checkWritable(name, path8) {
|
|
378
444
|
try {
|
|
379
|
-
await mkdir2(dirname(
|
|
445
|
+
await mkdir2(dirname(path8), { recursive: true });
|
|
380
446
|
try {
|
|
381
|
-
await access(
|
|
447
|
+
await access(path8, constants.W_OK);
|
|
382
448
|
} catch {
|
|
383
|
-
await access(dirname(
|
|
449
|
+
await access(dirname(path8), constants.W_OK);
|
|
384
450
|
}
|
|
385
|
-
return { name, status: "ok", detail:
|
|
451
|
+
return { name, status: "ok", detail: path8 };
|
|
386
452
|
} catch (err) {
|
|
387
453
|
return {
|
|
388
454
|
name,
|
|
389
455
|
status: "fail",
|
|
390
|
-
detail: `${
|
|
456
|
+
detail: `${path8} (${err instanceof Error ? err.message : String(err)})`
|
|
391
457
|
};
|
|
392
458
|
}
|
|
393
459
|
}
|
|
@@ -411,6 +477,31 @@ function providerEnvVar(provider) {
|
|
|
411
477
|
if (provider === "google") return "GOOGLE_GENERATIVE_AI_API_KEY";
|
|
412
478
|
return null;
|
|
413
479
|
}
|
|
480
|
+
async function checkOllamaDaemon() {
|
|
481
|
+
const s = await detectOllama();
|
|
482
|
+
switch (s.status) {
|
|
483
|
+
case "ready":
|
|
484
|
+
return { name: "ollama daemon", status: "ok", detail: "running, models present" };
|
|
485
|
+
case "running-no-models":
|
|
486
|
+
return {
|
|
487
|
+
name: "ollama daemon",
|
|
488
|
+
status: "warn",
|
|
489
|
+
detail: `running \u2014 pull missing models: ollama pull ${LOCAL_MODELS.generation} && ollama pull ${LOCAL_MODELS.embeddings}`
|
|
490
|
+
};
|
|
491
|
+
case "installed-not-running":
|
|
492
|
+
return {
|
|
493
|
+
name: "ollama daemon",
|
|
494
|
+
status: "fail",
|
|
495
|
+
detail: "installed but not running \u2014 start with `ollama serve`"
|
|
496
|
+
};
|
|
497
|
+
case "not-installed":
|
|
498
|
+
return {
|
|
499
|
+
name: "ollama daemon",
|
|
500
|
+
status: "fail",
|
|
501
|
+
detail: "not installed \u2014 see https://ollama.com"
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
}
|
|
414
505
|
async function selfTestLlm(config) {
|
|
415
506
|
try {
|
|
416
507
|
const router = await createLlmRouter({ config: config.llm });
|
|
@@ -458,7 +549,8 @@ async function selfTestKuzu(dbPath, embeddingDimension) {
|
|
|
458
549
|
}
|
|
459
550
|
|
|
460
551
|
// src/commands/index.ts
|
|
461
|
-
import
|
|
552
|
+
import { rm } from "fs/promises";
|
|
553
|
+
import path6 from "path";
|
|
462
554
|
|
|
463
555
|
// ../ingestion/src/parser.ts
|
|
464
556
|
import { readFile as readFile2 } from "fs/promises";
|
|
@@ -470,7 +562,8 @@ var GRAMMAR_FILE = {
|
|
|
470
562
|
typescript: "tree-sitter-typescript.wasm",
|
|
471
563
|
tsx: "tree-sitter-tsx.wasm",
|
|
472
564
|
javascript: "tree-sitter-javascript.wasm",
|
|
473
|
-
jsx: "tree-sitter-javascript.wasm"
|
|
565
|
+
jsx: "tree-sitter-javascript.wasm",
|
|
566
|
+
python: "tree-sitter-python.wasm"
|
|
474
567
|
};
|
|
475
568
|
var initPromise = null;
|
|
476
569
|
var languageCache = /* @__PURE__ */ new Map();
|
|
@@ -509,6 +602,8 @@ function detectLanguage(filePath) {
|
|
|
509
602
|
return "javascript";
|
|
510
603
|
case ".jsx":
|
|
511
604
|
return "jsx";
|
|
605
|
+
case ".py":
|
|
606
|
+
return "python";
|
|
512
607
|
default:
|
|
513
608
|
return null;
|
|
514
609
|
}
|
|
@@ -530,8 +625,8 @@ async function parseSource(source, language) {
|
|
|
530
625
|
}
|
|
531
626
|
|
|
532
627
|
// ../ingestion/src/extractors/extract.ts
|
|
533
|
-
import { createHash } from "crypto";
|
|
534
|
-
import { basename } from "path";
|
|
628
|
+
import { createHash as createHash2 } from "crypto";
|
|
629
|
+
import { basename as basename2 } from "path";
|
|
535
630
|
|
|
536
631
|
// ../ingestion/src/walker.ts
|
|
537
632
|
function* walk(node) {
|
|
@@ -590,6 +685,325 @@ function isPascalCase(name) {
|
|
|
590
685
|
return /^[A-Z][A-Za-z0-9]*$/.test(name);
|
|
591
686
|
}
|
|
592
687
|
|
|
688
|
+
// ../ingestion/src/extractors/extract-python.ts
|
|
689
|
+
import { createHash } from "crypto";
|
|
690
|
+
import { basename } from "path";
|
|
691
|
+
async function extractPythonFile(input3) {
|
|
692
|
+
const parsed = await parseSource(input3.source, input3.language);
|
|
693
|
+
const fileId = makeFileId({ repoId: input3.repoId, path: input3.relativePath });
|
|
694
|
+
const file = {
|
|
695
|
+
id: fileId,
|
|
696
|
+
kind: "File",
|
|
697
|
+
repoId: input3.repoId,
|
|
698
|
+
name: basename(input3.relativePath),
|
|
699
|
+
path: input3.relativePath,
|
|
700
|
+
lineStart: 1,
|
|
701
|
+
lineEnd: Math.max(1, parsed.rootNode.endPosition.row + 1),
|
|
702
|
+
language: input3.language,
|
|
703
|
+
sizeBytes: Buffer.byteLength(input3.source, "utf8"),
|
|
704
|
+
contentHash: sha1(input3.source)
|
|
705
|
+
};
|
|
706
|
+
const nodes = [];
|
|
707
|
+
const edges = [];
|
|
708
|
+
const localSymbols = /* @__PURE__ */ new Map();
|
|
709
|
+
for (const node of walk(parsed.rootNode)) {
|
|
710
|
+
const symbol = extractPythonSymbol(node, input3, parsed.source);
|
|
711
|
+
if (!symbol) continue;
|
|
712
|
+
nodes.push(symbol.node);
|
|
713
|
+
localSymbols.set(symbol.node.name, symbol.node.id);
|
|
714
|
+
edges.push({ kind: "DEFINES", fromId: fileId, toId: symbol.node.id });
|
|
715
|
+
if (symbol.node.isExported) {
|
|
716
|
+
edges.push({ kind: "EXPORTS", fromId: fileId, toId: symbol.node.id });
|
|
717
|
+
}
|
|
718
|
+
for (const parent of symbol.parentClasses ?? []) {
|
|
719
|
+
edges.push({
|
|
720
|
+
kind: "INHERITS",
|
|
721
|
+
fromId: symbol.node.id,
|
|
722
|
+
toId: "",
|
|
723
|
+
unresolvedTargetName: parent
|
|
724
|
+
});
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
for (const node of walk(parsed.rootNode)) {
|
|
728
|
+
if (node.type === "import_statement") {
|
|
729
|
+
for (const moduleNode of moduleNamesInImport(node)) {
|
|
730
|
+
const spec = dottedNameToPath(moduleNode, parsed.source);
|
|
731
|
+
if (!spec) continue;
|
|
732
|
+
edges.push({
|
|
733
|
+
kind: "IMPORTS",
|
|
734
|
+
fromId: fileId,
|
|
735
|
+
toId: "",
|
|
736
|
+
line: startLine(node),
|
|
737
|
+
fromPath: input3.relativePath,
|
|
738
|
+
toPath: spec,
|
|
739
|
+
unresolvedTargetName: spec
|
|
740
|
+
});
|
|
741
|
+
}
|
|
742
|
+
} else if (node.type === "import_from_statement") {
|
|
743
|
+
const moduleNode = node.childForFieldName("module_name");
|
|
744
|
+
if (!moduleNode) continue;
|
|
745
|
+
const spec = moduleSpecFromFromImport(moduleNode, parsed.source);
|
|
746
|
+
if (!spec) continue;
|
|
747
|
+
edges.push({
|
|
748
|
+
kind: "IMPORTS",
|
|
749
|
+
fromId: fileId,
|
|
750
|
+
toId: "",
|
|
751
|
+
line: startLine(node),
|
|
752
|
+
fromPath: input3.relativePath,
|
|
753
|
+
toPath: spec,
|
|
754
|
+
unresolvedTargetName: spec
|
|
755
|
+
});
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
for (const node of walk(parsed.rootNode)) {
|
|
759
|
+
if (node.type !== "call") continue;
|
|
760
|
+
const callee = node.childForFieldName("function");
|
|
761
|
+
if (!callee) continue;
|
|
762
|
+
const calleeName = extractCalleeName(callee, parsed.source);
|
|
763
|
+
if (!calleeName) continue;
|
|
764
|
+
const enclosing = findEnclosingPythonSymbolId(node, localSymbols, parsed.source);
|
|
765
|
+
if (!enclosing) continue;
|
|
766
|
+
edges.push({
|
|
767
|
+
kind: "CALLS",
|
|
768
|
+
fromId: enclosing,
|
|
769
|
+
toId: "",
|
|
770
|
+
line: startLine(node),
|
|
771
|
+
unresolvedTargetName: calleeName
|
|
772
|
+
});
|
|
773
|
+
}
|
|
774
|
+
return { file, nodes, edges };
|
|
775
|
+
}
|
|
776
|
+
function extractPythonSymbol(node, input3, source) {
|
|
777
|
+
switch (node.type) {
|
|
778
|
+
case "function_definition":
|
|
779
|
+
return pythonFunction(node, input3, source);
|
|
780
|
+
case "class_definition":
|
|
781
|
+
return pythonClass(node, input3, source);
|
|
782
|
+
case "assignment":
|
|
783
|
+
return pythonModuleVariable(node, input3, source);
|
|
784
|
+
default:
|
|
785
|
+
return null;
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
function pythonFunction(node, input3, source) {
|
|
789
|
+
const nameNode = node.childForFieldName("name");
|
|
790
|
+
if (!nameNode) return null;
|
|
791
|
+
const name = nodeText(nameNode, source);
|
|
792
|
+
const line = startLine(node);
|
|
793
|
+
const id = makeNodeId({
|
|
794
|
+
repoId: input3.repoId,
|
|
795
|
+
kind: "Function",
|
|
796
|
+
path: input3.relativePath,
|
|
797
|
+
name,
|
|
798
|
+
line
|
|
799
|
+
});
|
|
800
|
+
return {
|
|
801
|
+
node: {
|
|
802
|
+
id,
|
|
803
|
+
kind: "Function",
|
|
804
|
+
repoId: input3.repoId,
|
|
805
|
+
name,
|
|
806
|
+
path: input3.relativePath,
|
|
807
|
+
lineStart: decoratedLineStart(node),
|
|
808
|
+
lineEnd: endLine(node),
|
|
809
|
+
signature: pythonDefSignature(node, source, "def"),
|
|
810
|
+
leadingComment: pythonDocstring(node, source),
|
|
811
|
+
isExported: isPythonExported(name),
|
|
812
|
+
isAsync: isAsyncFunctionDef(node, source)
|
|
813
|
+
}
|
|
814
|
+
};
|
|
815
|
+
}
|
|
816
|
+
function pythonClass(node, input3, source) {
|
|
817
|
+
const nameNode = node.childForFieldName("name");
|
|
818
|
+
if (!nameNode) return null;
|
|
819
|
+
const name = nodeText(nameNode, source);
|
|
820
|
+
const line = startLine(node);
|
|
821
|
+
const id = makeNodeId({
|
|
822
|
+
repoId: input3.repoId,
|
|
823
|
+
kind: "Class",
|
|
824
|
+
path: input3.relativePath,
|
|
825
|
+
name,
|
|
826
|
+
line
|
|
827
|
+
});
|
|
828
|
+
const parents = extractPythonBaseClasses(node, source);
|
|
829
|
+
return {
|
|
830
|
+
node: {
|
|
831
|
+
id,
|
|
832
|
+
kind: "Class",
|
|
833
|
+
repoId: input3.repoId,
|
|
834
|
+
name,
|
|
835
|
+
path: input3.relativePath,
|
|
836
|
+
lineStart: decoratedLineStart(node),
|
|
837
|
+
lineEnd: endLine(node),
|
|
838
|
+
signature: pythonDefSignature(node, source, "class"),
|
|
839
|
+
leadingComment: pythonDocstring(node, source),
|
|
840
|
+
isExported: isPythonExported(name)
|
|
841
|
+
},
|
|
842
|
+
...parents.length > 0 ? { parentClasses: parents } : {}
|
|
843
|
+
};
|
|
844
|
+
}
|
|
845
|
+
function pythonModuleVariable(node, input3, source) {
|
|
846
|
+
const stmtParent = node.parent;
|
|
847
|
+
if (!stmtParent || stmtParent.type !== "expression_statement") return null;
|
|
848
|
+
const moduleParent = stmtParent.parent;
|
|
849
|
+
if (!moduleParent || moduleParent.type !== "module") return null;
|
|
850
|
+
const left = node.childForFieldName("left");
|
|
851
|
+
if (!left || left.type !== "identifier") return null;
|
|
852
|
+
const name = nodeText(left, source);
|
|
853
|
+
const line = startLine(node);
|
|
854
|
+
const id = makeNodeId({
|
|
855
|
+
repoId: input3.repoId,
|
|
856
|
+
kind: "Variable",
|
|
857
|
+
path: input3.relativePath,
|
|
858
|
+
name,
|
|
859
|
+
line
|
|
860
|
+
});
|
|
861
|
+
return {
|
|
862
|
+
node: {
|
|
863
|
+
id,
|
|
864
|
+
kind: "Variable",
|
|
865
|
+
repoId: input3.repoId,
|
|
866
|
+
name,
|
|
867
|
+
path: input3.relativePath,
|
|
868
|
+
lineStart: line,
|
|
869
|
+
lineEnd: endLine(node),
|
|
870
|
+
signature: nodeText(node, source).split("\n")[0]?.slice(0, 200) ?? "",
|
|
871
|
+
isExported: isPythonExported(name)
|
|
872
|
+
}
|
|
873
|
+
};
|
|
874
|
+
}
|
|
875
|
+
function isPythonExported(name) {
|
|
876
|
+
return !name.startsWith("_");
|
|
877
|
+
}
|
|
878
|
+
function isAsyncFunctionDef(node, source) {
|
|
879
|
+
return nodeText(node, source).trimStart().startsWith("async");
|
|
880
|
+
}
|
|
881
|
+
function decoratedLineStart(node) {
|
|
882
|
+
const parent = node.parent;
|
|
883
|
+
if (parent && parent.type === "decorated_definition") {
|
|
884
|
+
return startLine(parent);
|
|
885
|
+
}
|
|
886
|
+
return startLine(node);
|
|
887
|
+
}
|
|
888
|
+
function pythonDefSignature(node, source, keyword) {
|
|
889
|
+
const text = nodeText(node, source);
|
|
890
|
+
const firstLine = text.split("\n")[0]?.trim() ?? "";
|
|
891
|
+
const cleaned = firstLine.replace(/:\s*$/, "");
|
|
892
|
+
if (cleaned.length > 0) return cleaned.slice(0, 200);
|
|
893
|
+
return `${keyword} ?`;
|
|
894
|
+
}
|
|
895
|
+
function pythonDocstring(node, source) {
|
|
896
|
+
const body = node.childForFieldName("body");
|
|
897
|
+
if (!body) return "";
|
|
898
|
+
const firstStmt = body.namedChild(0);
|
|
899
|
+
if (!firstStmt || firstStmt.type !== "expression_statement") return "";
|
|
900
|
+
const expr = firstStmt.namedChild(0);
|
|
901
|
+
if (!expr || expr.type !== "string") return "";
|
|
902
|
+
const raw = nodeText(expr, source);
|
|
903
|
+
return stripPythonStringQuotes(raw).trim();
|
|
904
|
+
}
|
|
905
|
+
function stripPythonStringQuotes(raw) {
|
|
906
|
+
const withoutPrefix = raw.replace(/^[rRbBuUfF]+/, "");
|
|
907
|
+
if (withoutPrefix.startsWith('"""') && withoutPrefix.endsWith('"""')) {
|
|
908
|
+
return withoutPrefix.slice(3, -3);
|
|
909
|
+
}
|
|
910
|
+
if (withoutPrefix.startsWith("'''") && withoutPrefix.endsWith("'''")) {
|
|
911
|
+
return withoutPrefix.slice(3, -3);
|
|
912
|
+
}
|
|
913
|
+
if (withoutPrefix.startsWith('"') && withoutPrefix.endsWith('"') || withoutPrefix.startsWith("'") && withoutPrefix.endsWith("'")) {
|
|
914
|
+
return withoutPrefix.slice(1, -1);
|
|
915
|
+
}
|
|
916
|
+
return withoutPrefix;
|
|
917
|
+
}
|
|
918
|
+
function extractPythonBaseClasses(node, source) {
|
|
919
|
+
const supers = node.childForFieldName("superclasses");
|
|
920
|
+
if (!supers) return [];
|
|
921
|
+
const out = [];
|
|
922
|
+
for (let i = 0; i < supers.namedChildCount; i++) {
|
|
923
|
+
const arg = supers.namedChild(i);
|
|
924
|
+
if (!arg) continue;
|
|
925
|
+
if (arg.type === "keyword_argument") continue;
|
|
926
|
+
const name = extractCalleeName(arg, source);
|
|
927
|
+
if (name) out.push(name);
|
|
928
|
+
}
|
|
929
|
+
return out;
|
|
930
|
+
}
|
|
931
|
+
function moduleNamesInImport(importNode) {
|
|
932
|
+
const out = [];
|
|
933
|
+
for (let i = 0; i < importNode.namedChildCount; i++) {
|
|
934
|
+
const child = importNode.namedChild(i);
|
|
935
|
+
if (!child) continue;
|
|
936
|
+
if (child.type === "dotted_name") {
|
|
937
|
+
out.push(child);
|
|
938
|
+
} else if (child.type === "aliased_import") {
|
|
939
|
+
const inner = child.childForFieldName("name") ?? findChildByType(child, "dotted_name");
|
|
940
|
+
if (inner) out.push(inner);
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
return out;
|
|
944
|
+
}
|
|
945
|
+
function dottedNameToPath(node, source) {
|
|
946
|
+
if (node.type !== "dotted_name") return null;
|
|
947
|
+
const segments = [];
|
|
948
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
949
|
+
const ident = node.namedChild(i);
|
|
950
|
+
if (!ident) continue;
|
|
951
|
+
segments.push(nodeText(ident, source));
|
|
952
|
+
}
|
|
953
|
+
if (segments.length === 0) return null;
|
|
954
|
+
return segments.join("/");
|
|
955
|
+
}
|
|
956
|
+
function moduleSpecFromFromImport(moduleNode, source) {
|
|
957
|
+
if (moduleNode.type === "dotted_name") {
|
|
958
|
+
return dottedNameToPath(moduleNode, source);
|
|
959
|
+
}
|
|
960
|
+
if (moduleNode.type !== "relative_import") return null;
|
|
961
|
+
let dotCount = 0;
|
|
962
|
+
let dottedSpec = null;
|
|
963
|
+
for (let i = 0; i < moduleNode.childCount; i++) {
|
|
964
|
+
const child = moduleNode.child(i);
|
|
965
|
+
if (!child) continue;
|
|
966
|
+
if (child.type === "import_prefix") {
|
|
967
|
+
dotCount = nodeText(child, source).length;
|
|
968
|
+
} else if (child.type === "dotted_name") {
|
|
969
|
+
dottedSpec = dottedNameToPath(child, source);
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
if (dotCount === 0) return dottedSpec;
|
|
973
|
+
const upHops = "../".repeat(Math.max(0, dotCount - 1));
|
|
974
|
+
const base = upHops.length > 0 ? upHops.slice(0, -1) : ".";
|
|
975
|
+
if (dottedSpec) return `${base}/${dottedSpec}`;
|
|
976
|
+
return base;
|
|
977
|
+
}
|
|
978
|
+
function extractCalleeName(node, source) {
|
|
979
|
+
if (node.type === "identifier") {
|
|
980
|
+
return nodeText(node, source);
|
|
981
|
+
}
|
|
982
|
+
if (node.type === "attribute") {
|
|
983
|
+
const attr = node.childForFieldName("attribute");
|
|
984
|
+
if (attr && attr.type === "identifier") return nodeText(attr, source);
|
|
985
|
+
return null;
|
|
986
|
+
}
|
|
987
|
+
return null;
|
|
988
|
+
}
|
|
989
|
+
function findEnclosingPythonSymbolId(node, localSymbols, source) {
|
|
990
|
+
let cursor = node.parent;
|
|
991
|
+
while (cursor) {
|
|
992
|
+
if (cursor.type === "function_definition" || cursor.type === "class_definition") {
|
|
993
|
+
const nameNode = cursor.childForFieldName("name");
|
|
994
|
+
if (nameNode) {
|
|
995
|
+
const id = localSymbols.get(nodeText(nameNode, source));
|
|
996
|
+
if (id) return id;
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
cursor = cursor.parent;
|
|
1000
|
+
}
|
|
1001
|
+
return null;
|
|
1002
|
+
}
|
|
1003
|
+
function sha1(text) {
|
|
1004
|
+
return createHash("sha1").update(text).digest("hex");
|
|
1005
|
+
}
|
|
1006
|
+
|
|
593
1007
|
// ../ingestion/src/extractors/routes.ts
|
|
594
1008
|
var EXPRESS_METHODS = /* @__PURE__ */ new Set([
|
|
595
1009
|
"get",
|
|
@@ -611,44 +1025,44 @@ var HTTP_VERBS = /* @__PURE__ */ new Set([
|
|
|
611
1025
|
"OPTIONS",
|
|
612
1026
|
"HEAD"
|
|
613
1027
|
]);
|
|
614
|
-
function detectRoutes(
|
|
1028
|
+
function detectRoutes(input3) {
|
|
615
1029
|
return [
|
|
616
|
-
...detectExpressRoutes(
|
|
617
|
-
...detectNextAppRouterRoutes(
|
|
618
|
-
...detectNextPagesApiRoutes(
|
|
1030
|
+
...detectExpressRoutes(input3),
|
|
1031
|
+
...detectNextAppRouterRoutes(input3),
|
|
1032
|
+
...detectNextPagesApiRoutes(input3)
|
|
619
1033
|
];
|
|
620
1034
|
}
|
|
621
|
-
function detectExpressRoutes(
|
|
1035
|
+
function detectExpressRoutes(input3) {
|
|
622
1036
|
const out = [];
|
|
623
|
-
for (const node of walk(
|
|
1037
|
+
for (const node of walk(input3.rootNode)) {
|
|
624
1038
|
if (node.type !== "call_expression") continue;
|
|
625
1039
|
const callee = node.childForFieldName("function");
|
|
626
1040
|
if (!callee || callee.type !== "member_expression") continue;
|
|
627
1041
|
const obj = callee.childForFieldName("object");
|
|
628
1042
|
const prop = callee.childForFieldName("property");
|
|
629
1043
|
if (!obj || !prop) continue;
|
|
630
|
-
const method = nodeText(prop,
|
|
1044
|
+
const method = nodeText(prop, input3.source).toLowerCase();
|
|
631
1045
|
if (!EXPRESS_METHODS.has(method)) continue;
|
|
632
1046
|
const args = node.childForFieldName("arguments");
|
|
633
1047
|
if (!args) continue;
|
|
634
1048
|
const firstArg = args.namedChild(0);
|
|
635
1049
|
if (!firstArg || firstArg.type !== "string") continue;
|
|
636
|
-
const routePath = stripQuotes(nodeText(firstArg,
|
|
1050
|
+
const routePath = stripQuotes(nodeText(firstArg, input3.source));
|
|
637
1051
|
if (!routePath.startsWith("/")) continue;
|
|
638
1052
|
const line = startLine(node);
|
|
639
1053
|
const name = `${method.toUpperCase()} ${routePath}`;
|
|
640
1054
|
out.push({
|
|
641
1055
|
id: makeNodeId({
|
|
642
|
-
repoId:
|
|
1056
|
+
repoId: input3.repoId,
|
|
643
1057
|
kind: "Route",
|
|
644
|
-
path:
|
|
1058
|
+
path: input3.relativePath,
|
|
645
1059
|
name,
|
|
646
1060
|
line
|
|
647
1061
|
}),
|
|
648
1062
|
kind: "Route",
|
|
649
|
-
repoId:
|
|
1063
|
+
repoId: input3.repoId,
|
|
650
1064
|
name,
|
|
651
|
-
path:
|
|
1065
|
+
path: input3.relativePath,
|
|
652
1066
|
lineStart: line,
|
|
653
1067
|
lineEnd: endLine(node),
|
|
654
1068
|
method: method.toUpperCase(),
|
|
@@ -658,29 +1072,29 @@ function detectExpressRoutes(input) {
|
|
|
658
1072
|
}
|
|
659
1073
|
return out;
|
|
660
1074
|
}
|
|
661
|
-
function detectNextAppRouterRoutes(
|
|
662
|
-
if (!/(^|\/)app\/.+\/route\.(ts|tsx|js|jsx|mjs|cjs)$/.test(
|
|
663
|
-
const routePath = appRoutePathFor(
|
|
1075
|
+
function detectNextAppRouterRoutes(input3) {
|
|
1076
|
+
if (!/(^|\/)app\/.+\/route\.(ts|tsx|js|jsx|mjs|cjs)$/.test(input3.relativePath)) return [];
|
|
1077
|
+
const routePath = appRoutePathFor(input3.relativePath);
|
|
664
1078
|
const out = [];
|
|
665
|
-
for (const node of walk(
|
|
1079
|
+
for (const node of walk(input3.rootNode)) {
|
|
666
1080
|
if (node.type !== "export_statement") continue;
|
|
667
1081
|
const decl = findChildByType(node, "function_declaration") ?? findChildByType(node, "lexical_declaration");
|
|
668
1082
|
if (!decl) continue;
|
|
669
|
-
const name = extractTopLevelName(decl,
|
|
1083
|
+
const name = extractTopLevelName(decl, input3.source);
|
|
670
1084
|
if (!name || !HTTP_VERBS.has(name)) continue;
|
|
671
1085
|
const line = startLine(node);
|
|
672
1086
|
out.push({
|
|
673
1087
|
id: makeNodeId({
|
|
674
|
-
repoId:
|
|
1088
|
+
repoId: input3.repoId,
|
|
675
1089
|
kind: "Route",
|
|
676
|
-
path:
|
|
1090
|
+
path: input3.relativePath,
|
|
677
1091
|
name: `${name} ${routePath}`,
|
|
678
1092
|
line
|
|
679
1093
|
}),
|
|
680
1094
|
kind: "Route",
|
|
681
|
-
repoId:
|
|
1095
|
+
repoId: input3.repoId,
|
|
682
1096
|
name: `${name} ${routePath}`,
|
|
683
|
-
path:
|
|
1097
|
+
path: input3.relativePath,
|
|
684
1098
|
lineStart: line,
|
|
685
1099
|
lineEnd: endLine(node),
|
|
686
1100
|
method: name,
|
|
@@ -690,27 +1104,27 @@ function detectNextAppRouterRoutes(input) {
|
|
|
690
1104
|
}
|
|
691
1105
|
return out;
|
|
692
1106
|
}
|
|
693
|
-
function detectNextPagesApiRoutes(
|
|
694
|
-
if (!/(^|\/)pages\/api\//.test(
|
|
695
|
-
const routePath = pagesApiPathFor(
|
|
696
|
-
for (const node of walk(
|
|
1107
|
+
function detectNextPagesApiRoutes(input3) {
|
|
1108
|
+
if (!/(^|\/)pages\/api\//.test(input3.relativePath)) return [];
|
|
1109
|
+
const routePath = pagesApiPathFor(input3.relativePath);
|
|
1110
|
+
for (const node of walk(input3.rootNode)) {
|
|
697
1111
|
if (node.type !== "export_statement") continue;
|
|
698
|
-
const text = nodeText(node,
|
|
1112
|
+
const text = nodeText(node, input3.source);
|
|
699
1113
|
if (!/default/.test(text)) continue;
|
|
700
1114
|
const line = startLine(node);
|
|
701
1115
|
return [
|
|
702
1116
|
{
|
|
703
1117
|
id: makeNodeId({
|
|
704
|
-
repoId:
|
|
1118
|
+
repoId: input3.repoId,
|
|
705
1119
|
kind: "Route",
|
|
706
|
-
path:
|
|
1120
|
+
path: input3.relativePath,
|
|
707
1121
|
name: `ANY ${routePath}`,
|
|
708
1122
|
line
|
|
709
1123
|
}),
|
|
710
1124
|
kind: "Route",
|
|
711
|
-
repoId:
|
|
1125
|
+
repoId: input3.repoId,
|
|
712
1126
|
name: `ANY ${routePath}`,
|
|
713
|
-
path:
|
|
1127
|
+
path: input3.relativePath,
|
|
714
1128
|
lineStart: line,
|
|
715
1129
|
lineEnd: endLine(node),
|
|
716
1130
|
method: "ANY",
|
|
@@ -754,26 +1168,32 @@ function pagesApiPathFor(relativePath) {
|
|
|
754
1168
|
}
|
|
755
1169
|
|
|
756
1170
|
// ../ingestion/src/extractors/extract.ts
|
|
757
|
-
async function extractFile(
|
|
758
|
-
|
|
759
|
-
|
|
1171
|
+
async function extractFile(input3) {
|
|
1172
|
+
if (input3.language === "python") {
|
|
1173
|
+
return extractPythonFile(input3);
|
|
1174
|
+
}
|
|
1175
|
+
return extractJsTsFile(input3);
|
|
1176
|
+
}
|
|
1177
|
+
async function extractJsTsFile(input3) {
|
|
1178
|
+
const parsed = await parseSource(input3.source, input3.language);
|
|
1179
|
+
const fileId = makeFileId({ repoId: input3.repoId, path: input3.relativePath });
|
|
760
1180
|
const file = {
|
|
761
1181
|
id: fileId,
|
|
762
1182
|
kind: "File",
|
|
763
|
-
repoId:
|
|
764
|
-
name:
|
|
765
|
-
path:
|
|
1183
|
+
repoId: input3.repoId,
|
|
1184
|
+
name: basename2(input3.relativePath),
|
|
1185
|
+
path: input3.relativePath,
|
|
766
1186
|
lineStart: 1,
|
|
767
1187
|
lineEnd: Math.max(1, parsed.rootNode.endPosition.row + 1),
|
|
768
|
-
language:
|
|
769
|
-
sizeBytes: Buffer.byteLength(
|
|
770
|
-
contentHash:
|
|
1188
|
+
language: input3.language,
|
|
1189
|
+
sizeBytes: Buffer.byteLength(input3.source, "utf8"),
|
|
1190
|
+
contentHash: sha12(input3.source)
|
|
771
1191
|
};
|
|
772
1192
|
const nodes = [];
|
|
773
1193
|
const edges = [];
|
|
774
1194
|
const localSymbols = /* @__PURE__ */ new Map();
|
|
775
1195
|
for (const node of walk(parsed.rootNode)) {
|
|
776
|
-
const symbol = extractSymbol(node,
|
|
1196
|
+
const symbol = extractSymbol(node, input3, parsed.source);
|
|
777
1197
|
if (!symbol) continue;
|
|
778
1198
|
nodes.push(symbol.node);
|
|
779
1199
|
localSymbols.set(symbol.node.name, symbol.node.id);
|
|
@@ -801,7 +1221,7 @@ async function extractFile(input) {
|
|
|
801
1221
|
fromId: fileId,
|
|
802
1222
|
toId: "",
|
|
803
1223
|
line: startLine(node),
|
|
804
|
-
fromPath:
|
|
1224
|
+
fromPath: input3.relativePath,
|
|
805
1225
|
toPath: target,
|
|
806
1226
|
unresolvedTargetName: target
|
|
807
1227
|
});
|
|
@@ -811,9 +1231,9 @@ async function extractFile(input) {
|
|
|
811
1231
|
if (node.type !== "call_expression") continue;
|
|
812
1232
|
const callee = node.childForFieldName("function");
|
|
813
1233
|
if (!callee) continue;
|
|
814
|
-
const calleeName =
|
|
1234
|
+
const calleeName = extractCalleeName2(callee, parsed.source);
|
|
815
1235
|
if (!calleeName) continue;
|
|
816
|
-
const enclosing = findEnclosingSymbolId(node,
|
|
1236
|
+
const enclosing = findEnclosingSymbolId(node, input3, parsed.source, localSymbols);
|
|
817
1237
|
if (!enclosing) continue;
|
|
818
1238
|
edges.push({
|
|
819
1239
|
kind: "CALLS",
|
|
@@ -823,14 +1243,14 @@ async function extractFile(input) {
|
|
|
823
1243
|
unresolvedTargetName: calleeName
|
|
824
1244
|
});
|
|
825
1245
|
}
|
|
826
|
-
if (
|
|
1246
|
+
if (input3.language === "tsx" || input3.language === "jsx") {
|
|
827
1247
|
for (const node of walk(parsed.rootNode)) {
|
|
828
1248
|
if (node.type !== "jsx_opening_element" && node.type !== "jsx_self_closing_element") continue;
|
|
829
1249
|
const ident = node.childForFieldName("name") ?? findChildByType(node, "identifier");
|
|
830
1250
|
if (!ident) continue;
|
|
831
1251
|
const tag = nodeText(ident, parsed.source);
|
|
832
1252
|
if (!isPascalCase(tag)) continue;
|
|
833
|
-
const enclosing = findEnclosingSymbolId(node,
|
|
1253
|
+
const enclosing = findEnclosingSymbolId(node, input3, parsed.source, localSymbols);
|
|
834
1254
|
if (!enclosing) continue;
|
|
835
1255
|
edges.push({
|
|
836
1256
|
kind: "RENDERS",
|
|
@@ -842,13 +1262,13 @@ async function extractFile(input) {
|
|
|
842
1262
|
}
|
|
843
1263
|
}
|
|
844
1264
|
const routes = detectRoutes({
|
|
845
|
-
repoId:
|
|
846
|
-
relativePath:
|
|
847
|
-
absolutePath:
|
|
1265
|
+
repoId: input3.repoId,
|
|
1266
|
+
relativePath: input3.relativePath,
|
|
1267
|
+
absolutePath: input3.absolutePath,
|
|
848
1268
|
fileId,
|
|
849
1269
|
rootNode: parsed.rootNode,
|
|
850
1270
|
source: parsed.source,
|
|
851
|
-
language:
|
|
1271
|
+
language: input3.language
|
|
852
1272
|
});
|
|
853
1273
|
for (const route of routes) {
|
|
854
1274
|
nodes.push(route);
|
|
@@ -856,53 +1276,55 @@ async function extractFile(input) {
|
|
|
856
1276
|
}
|
|
857
1277
|
return { file, nodes, edges };
|
|
858
1278
|
}
|
|
859
|
-
function extractSymbol(node,
|
|
1279
|
+
function extractSymbol(node, input3, source) {
|
|
860
1280
|
switch (node.type) {
|
|
861
1281
|
case "function_declaration":
|
|
862
1282
|
case "generator_function_declaration":
|
|
863
|
-
return functionFromDeclaration(node,
|
|
1283
|
+
return functionFromDeclaration(node, input3, source);
|
|
864
1284
|
case "class_declaration":
|
|
865
|
-
return classFromDeclaration(node,
|
|
1285
|
+
return classFromDeclaration(node, input3, source);
|
|
866
1286
|
case "interface_declaration":
|
|
867
|
-
return interfaceFromDeclaration(node,
|
|
1287
|
+
return interfaceFromDeclaration(node, input3, source, "Interface");
|
|
868
1288
|
case "type_alias_declaration":
|
|
869
|
-
return interfaceFromDeclaration(node,
|
|
1289
|
+
return interfaceFromDeclaration(node, input3, source, "Interface");
|
|
870
1290
|
case "lexical_declaration":
|
|
871
1291
|
case "variable_declaration":
|
|
872
|
-
return variableOrArrowFromDeclaration(node,
|
|
1292
|
+
return variableOrArrowFromDeclaration(node, input3, source);
|
|
873
1293
|
default:
|
|
874
1294
|
return null;
|
|
875
1295
|
}
|
|
876
1296
|
}
|
|
877
|
-
function functionFromDeclaration(node,
|
|
1297
|
+
function functionFromDeclaration(node, input3, source) {
|
|
878
1298
|
const nameNode = node.childForFieldName("name");
|
|
879
1299
|
if (!nameNode) return null;
|
|
880
1300
|
const name = nodeText(nameNode, source);
|
|
881
1301
|
const line = startLine(node);
|
|
1302
|
+
const isComponent = (input3.language === "tsx" || input3.language === "jsx") && isPascalCase(name) && containsJsx(node.childForFieldName("body"));
|
|
1303
|
+
const kind = isComponent ? "Component" : "Function";
|
|
882
1304
|
const id = makeNodeId({
|
|
883
|
-
repoId:
|
|
884
|
-
kind
|
|
885
|
-
path:
|
|
1305
|
+
repoId: input3.repoId,
|
|
1306
|
+
kind,
|
|
1307
|
+
path: input3.relativePath,
|
|
886
1308
|
name,
|
|
887
1309
|
line
|
|
888
1310
|
});
|
|
889
1311
|
return {
|
|
890
1312
|
node: {
|
|
891
1313
|
id,
|
|
892
|
-
kind
|
|
893
|
-
repoId:
|
|
1314
|
+
kind,
|
|
1315
|
+
repoId: input3.repoId,
|
|
894
1316
|
name,
|
|
895
|
-
path:
|
|
1317
|
+
path: input3.relativePath,
|
|
896
1318
|
lineStart: line,
|
|
897
1319
|
lineEnd: endLine(node),
|
|
898
1320
|
signature: extractSignature(node, source),
|
|
899
1321
|
leadingComment: leadingCommentFor(parentForLeadingComment(node), source),
|
|
900
1322
|
isExported: isExported(node),
|
|
901
|
-
isAsync: hasAsyncModifier(node, source)
|
|
1323
|
+
...kind === "Function" ? { isAsync: hasAsyncModifier(node, source) } : {}
|
|
902
1324
|
}
|
|
903
1325
|
};
|
|
904
1326
|
}
|
|
905
|
-
function classFromDeclaration(node,
|
|
1327
|
+
function classFromDeclaration(node, input3, source) {
|
|
906
1328
|
const nameNode = node.childForFieldName("name");
|
|
907
1329
|
if (!nameNode) return null;
|
|
908
1330
|
const name = nodeText(nameNode, source);
|
|
@@ -910,9 +1332,9 @@ function classFromDeclaration(node, input, source) {
|
|
|
910
1332
|
const isComponent = looksLikeComponentClass(node, name, source);
|
|
911
1333
|
const kind = isComponent ? "Component" : "Class";
|
|
912
1334
|
const id = makeNodeId({
|
|
913
|
-
repoId:
|
|
1335
|
+
repoId: input3.repoId,
|
|
914
1336
|
kind,
|
|
915
|
-
path:
|
|
1337
|
+
path: input3.relativePath,
|
|
916
1338
|
name,
|
|
917
1339
|
line
|
|
918
1340
|
});
|
|
@@ -921,9 +1343,9 @@ function classFromDeclaration(node, input, source) {
|
|
|
921
1343
|
node: {
|
|
922
1344
|
id,
|
|
923
1345
|
kind,
|
|
924
|
-
repoId:
|
|
1346
|
+
repoId: input3.repoId,
|
|
925
1347
|
name,
|
|
926
|
-
path:
|
|
1348
|
+
path: input3.relativePath,
|
|
927
1349
|
lineStart: line,
|
|
928
1350
|
lineEnd: endLine(node),
|
|
929
1351
|
signature: `class ${name}${parentClass ? ` extends ${parentClass}` : ""}`,
|
|
@@ -933,15 +1355,15 @@ function classFromDeclaration(node, input, source) {
|
|
|
933
1355
|
...parentClass !== void 0 ? { parentClass } : {}
|
|
934
1356
|
};
|
|
935
1357
|
}
|
|
936
|
-
function interfaceFromDeclaration(node,
|
|
1358
|
+
function interfaceFromDeclaration(node, input3, source, kind) {
|
|
937
1359
|
const nameNode = node.childForFieldName("name");
|
|
938
1360
|
if (!nameNode) return null;
|
|
939
1361
|
const name = nodeText(nameNode, source);
|
|
940
1362
|
const line = startLine(node);
|
|
941
1363
|
const id = makeNodeId({
|
|
942
|
-
repoId:
|
|
1364
|
+
repoId: input3.repoId,
|
|
943
1365
|
kind,
|
|
944
|
-
path:
|
|
1366
|
+
path: input3.relativePath,
|
|
945
1367
|
name,
|
|
946
1368
|
line
|
|
947
1369
|
});
|
|
@@ -949,9 +1371,9 @@ function interfaceFromDeclaration(node, input, source, kind) {
|
|
|
949
1371
|
node: {
|
|
950
1372
|
id,
|
|
951
1373
|
kind,
|
|
952
|
-
repoId:
|
|
1374
|
+
repoId: input3.repoId,
|
|
953
1375
|
name,
|
|
954
|
-
path:
|
|
1376
|
+
path: input3.relativePath,
|
|
955
1377
|
lineStart: line,
|
|
956
1378
|
lineEnd: endLine(node),
|
|
957
1379
|
signature: nodeText(node, source).split("\n")[0]?.slice(0, 200) ?? "",
|
|
@@ -960,7 +1382,7 @@ function interfaceFromDeclaration(node, input, source, kind) {
|
|
|
960
1382
|
}
|
|
961
1383
|
};
|
|
962
1384
|
}
|
|
963
|
-
function variableOrArrowFromDeclaration(node,
|
|
1385
|
+
function variableOrArrowFromDeclaration(node, input3, source) {
|
|
964
1386
|
const declarators = findChildrenByType(node, "variable_declarator");
|
|
965
1387
|
if (declarators.length === 0) return null;
|
|
966
1388
|
const decl = declarators[0];
|
|
@@ -968,6 +1390,9 @@ function variableOrArrowFromDeclaration(node, input, source) {
|
|
|
968
1390
|
const nameNode = decl.childForFieldName("name");
|
|
969
1391
|
const value = decl.childForFieldName("value");
|
|
970
1392
|
if (!nameNode) return null;
|
|
1393
|
+
if (nameNode.type !== "identifier" && nameNode.type !== "property_identifier") {
|
|
1394
|
+
return null;
|
|
1395
|
+
}
|
|
971
1396
|
const name = nodeText(nameNode, source);
|
|
972
1397
|
const line = startLine(decl);
|
|
973
1398
|
const isArrow = value?.type === "arrow_function" || value?.type === "function_expression";
|
|
@@ -975,9 +1400,9 @@ function variableOrArrowFromDeclaration(node, input, source) {
|
|
|
975
1400
|
const isComponent = isPascalCase(name) && containsJsx(value);
|
|
976
1401
|
const kind = isComponent ? "Component" : "Function";
|
|
977
1402
|
const id2 = makeNodeId({
|
|
978
|
-
repoId:
|
|
1403
|
+
repoId: input3.repoId,
|
|
979
1404
|
kind,
|
|
980
|
-
path:
|
|
1405
|
+
path: input3.relativePath,
|
|
981
1406
|
name,
|
|
982
1407
|
line
|
|
983
1408
|
});
|
|
@@ -985,9 +1410,9 @@ function variableOrArrowFromDeclaration(node, input, source) {
|
|
|
985
1410
|
node: {
|
|
986
1411
|
id: id2,
|
|
987
1412
|
kind,
|
|
988
|
-
repoId:
|
|
1413
|
+
repoId: input3.repoId,
|
|
989
1414
|
name,
|
|
990
|
-
path:
|
|
1415
|
+
path: input3.relativePath,
|
|
991
1416
|
lineStart: line,
|
|
992
1417
|
lineEnd: endLine(decl),
|
|
993
1418
|
signature: extractSignature(value, source),
|
|
@@ -998,9 +1423,9 @@ function variableOrArrowFromDeclaration(node, input, source) {
|
|
|
998
1423
|
};
|
|
999
1424
|
}
|
|
1000
1425
|
const id = makeNodeId({
|
|
1001
|
-
repoId:
|
|
1426
|
+
repoId: input3.repoId,
|
|
1002
1427
|
kind: "Variable",
|
|
1003
|
-
path:
|
|
1428
|
+
path: input3.relativePath,
|
|
1004
1429
|
name,
|
|
1005
1430
|
line
|
|
1006
1431
|
});
|
|
@@ -1008,9 +1433,9 @@ function variableOrArrowFromDeclaration(node, input, source) {
|
|
|
1008
1433
|
node: {
|
|
1009
1434
|
id,
|
|
1010
1435
|
kind: "Variable",
|
|
1011
|
-
repoId:
|
|
1436
|
+
repoId: input3.repoId,
|
|
1012
1437
|
name,
|
|
1013
|
-
path:
|
|
1438
|
+
path: input3.relativePath,
|
|
1014
1439
|
lineStart: line,
|
|
1015
1440
|
lineEnd: endLine(decl),
|
|
1016
1441
|
signature: nodeText(decl, source).split("\n")[0]?.slice(0, 200) ?? "",
|
|
@@ -1029,9 +1454,13 @@ function extractSignature(node, source) {
|
|
|
1029
1454
|
const rs = ret ? ` ${nodeText(ret, source)}` : "";
|
|
1030
1455
|
return `${head}${ps}${rs}`.trim().slice(0, 200);
|
|
1031
1456
|
}
|
|
1032
|
-
function
|
|
1457
|
+
function extractCalleeName2(callee, source) {
|
|
1033
1458
|
if (callee.type === "identifier") return nodeText(callee, source);
|
|
1034
1459
|
if (callee.type === "member_expression") {
|
|
1460
|
+
const object = callee.childForFieldName("object");
|
|
1461
|
+
if (object && object.type === "identifier") {
|
|
1462
|
+
return nodeText(object, source);
|
|
1463
|
+
}
|
|
1035
1464
|
const property = callee.childForFieldName("property");
|
|
1036
1465
|
if (property) return nodeText(property, source);
|
|
1037
1466
|
}
|
|
@@ -1088,7 +1517,7 @@ function parentForLeadingComment(node) {
|
|
|
1088
1517
|
}
|
|
1089
1518
|
return cursor;
|
|
1090
1519
|
}
|
|
1091
|
-
function findEnclosingSymbolId(node,
|
|
1520
|
+
function findEnclosingSymbolId(node, input3, source, localSymbols) {
|
|
1092
1521
|
let cursor = node.parent;
|
|
1093
1522
|
while (cursor) {
|
|
1094
1523
|
if (cursor.type === "function_declaration" || cursor.type === "method_definition" || cursor.type === "class_declaration" || cursor.type === "arrow_function" || cursor.type === "function_expression") {
|
|
@@ -1100,7 +1529,7 @@ function findEnclosingSymbolId(node, input, source, localSymbols) {
|
|
|
1100
1529
|
}
|
|
1101
1530
|
cursor = cursor.parent;
|
|
1102
1531
|
}
|
|
1103
|
-
return makeFileId({ repoId:
|
|
1532
|
+
return makeFileId({ repoId: input3.repoId, path: input3.relativePath });
|
|
1104
1533
|
}
|
|
1105
1534
|
function enclosingDeclarationName(node, source) {
|
|
1106
1535
|
const nameField = node.childForFieldName("name");
|
|
@@ -1117,22 +1546,31 @@ function enclosingDeclarationName(node, source) {
|
|
|
1117
1546
|
}
|
|
1118
1547
|
return null;
|
|
1119
1548
|
}
|
|
1120
|
-
function
|
|
1121
|
-
return
|
|
1549
|
+
function sha12(s) {
|
|
1550
|
+
return createHash2("sha1").update(s).digest("hex");
|
|
1122
1551
|
}
|
|
1123
1552
|
|
|
1124
1553
|
// ../ingestion/src/extractors/resolve.ts
|
|
1125
1554
|
import path2 from "path";
|
|
1126
|
-
var EXTENSIONS = [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"];
|
|
1127
|
-
|
|
1555
|
+
var EXTENSIONS = [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".py"];
|
|
1556
|
+
var DIRECTORY_INDEX_FILES = [
|
|
1557
|
+
"index.ts",
|
|
1558
|
+
"index.tsx",
|
|
1559
|
+
"index.js",
|
|
1560
|
+
"index.jsx",
|
|
1561
|
+
"index.mjs",
|
|
1562
|
+
"index.cjs",
|
|
1563
|
+
"__init__.py"
|
|
1564
|
+
];
|
|
1565
|
+
function resolveEdges(input3) {
|
|
1128
1566
|
const byName = /* @__PURE__ */ new Map();
|
|
1129
|
-
for (const n of
|
|
1567
|
+
for (const n of input3.nodes) {
|
|
1130
1568
|
if (n.kind === "File") continue;
|
|
1131
1569
|
if (!byName.has(n.name)) byName.set(n.name, n.id);
|
|
1132
1570
|
}
|
|
1133
1571
|
const out = [];
|
|
1134
1572
|
let dropped = 0;
|
|
1135
|
-
for (const edge of
|
|
1573
|
+
for (const edge of input3.edges) {
|
|
1136
1574
|
if (edge.toId) {
|
|
1137
1575
|
out.push(edge);
|
|
1138
1576
|
continue;
|
|
@@ -1143,16 +1581,17 @@ function resolveEdges(input) {
|
|
|
1143
1581
|
}
|
|
1144
1582
|
if (edge.kind === "IMPORTS") {
|
|
1145
1583
|
const resolved = resolveImportPath({
|
|
1146
|
-
repoId:
|
|
1584
|
+
repoId: input3.repoId,
|
|
1147
1585
|
fromPath: edge.fromPath ?? "",
|
|
1148
1586
|
spec: edge.unresolvedTargetName,
|
|
1149
|
-
known:
|
|
1587
|
+
known: input3.knownFilePaths,
|
|
1588
|
+
...input3.tsconfigPaths ? { tsconfigPaths: input3.tsconfigPaths } : {}
|
|
1150
1589
|
});
|
|
1151
1590
|
if (!resolved) {
|
|
1152
1591
|
dropped++;
|
|
1153
1592
|
continue;
|
|
1154
1593
|
}
|
|
1155
|
-
const targetId2 = makeFileId({ repoId:
|
|
1594
|
+
const targetId2 = makeFileId({ repoId: input3.repoId, path: resolved });
|
|
1156
1595
|
const { unresolvedTargetName: _unused2, ...rest2 } = edge;
|
|
1157
1596
|
out.push({ ...rest2, toId: targetId2, toPath: resolved });
|
|
1158
1597
|
continue;
|
|
@@ -1167,30 +1606,69 @@ function resolveEdges(input) {
|
|
|
1167
1606
|
}
|
|
1168
1607
|
return { resolved: out, dropped };
|
|
1169
1608
|
}
|
|
1170
|
-
function resolveImportPath(
|
|
1171
|
-
const { fromPath, spec, known } =
|
|
1172
|
-
if (
|
|
1173
|
-
|
|
1174
|
-
|
|
1609
|
+
function resolveImportPath(input3) {
|
|
1610
|
+
const { fromPath, spec, known, tsconfigPaths } = input3;
|
|
1611
|
+
if (spec.startsWith(".") || spec.startsWith("/")) {
|
|
1612
|
+
const baseDir = path2.posix.dirname(toPosix(fromPath));
|
|
1613
|
+
const joined = path2.posix.normalize(path2.posix.join(baseDir, toPosix(spec)));
|
|
1614
|
+
return firstMatchingCandidate(joined, known);
|
|
1615
|
+
}
|
|
1616
|
+
if (tsconfigPaths) {
|
|
1617
|
+
const candidates = expandAliasCandidates(spec, tsconfigPaths);
|
|
1618
|
+
for (const c of candidates) {
|
|
1619
|
+
const resolved = firstMatchingCandidate(c, known);
|
|
1620
|
+
if (resolved) return resolved;
|
|
1621
|
+
}
|
|
1622
|
+
}
|
|
1623
|
+
const fromRoot = firstMatchingCandidate(toPosix(spec), known);
|
|
1624
|
+
if (fromRoot) return fromRoot;
|
|
1625
|
+
return null;
|
|
1626
|
+
}
|
|
1627
|
+
function firstMatchingCandidate(joined, known) {
|
|
1175
1628
|
const candidates = [joined];
|
|
1176
1629
|
const ext = path2.posix.extname(joined);
|
|
1177
1630
|
const stem = ext ? joined.slice(0, -ext.length) : joined;
|
|
1178
1631
|
for (const e of EXTENSIONS) candidates.push(`${stem}${e}`);
|
|
1179
|
-
for (const
|
|
1632
|
+
for (const indexFile of DIRECTORY_INDEX_FILES) candidates.push(`${stem}/${indexFile}`);
|
|
1180
1633
|
for (const c of candidates) {
|
|
1181
1634
|
if (known.has(c)) return c;
|
|
1182
1635
|
}
|
|
1183
1636
|
return null;
|
|
1184
1637
|
}
|
|
1638
|
+
function expandAliasCandidates(spec, cfg) {
|
|
1639
|
+
const sortedPatterns = Object.keys(cfg.paths).sort((a, b) => b.length - a.length);
|
|
1640
|
+
const out = [];
|
|
1641
|
+
for (const pattern of sortedPatterns) {
|
|
1642
|
+
const replacements = cfg.paths[pattern] ?? [];
|
|
1643
|
+
if (pattern.endsWith("/*")) {
|
|
1644
|
+
const prefix = pattern.slice(0, -1);
|
|
1645
|
+
if (!spec.startsWith(prefix)) continue;
|
|
1646
|
+
const tail = spec.slice(prefix.length);
|
|
1647
|
+
for (const r of replacements) {
|
|
1648
|
+
const rTail = r.endsWith("/*") ? r.slice(0, -1) : r.endsWith("*") ? r.slice(0, -1) : r;
|
|
1649
|
+
const joined = path2.posix.normalize(
|
|
1650
|
+
path2.posix.join(toPosix(cfg.baseUrl), `${rTail}${tail}`)
|
|
1651
|
+
);
|
|
1652
|
+
out.push(joined);
|
|
1653
|
+
}
|
|
1654
|
+
} else if (spec === pattern) {
|
|
1655
|
+
for (const r of replacements) {
|
|
1656
|
+
const joined = path2.posix.normalize(path2.posix.join(toPosix(cfg.baseUrl), r));
|
|
1657
|
+
out.push(joined);
|
|
1658
|
+
}
|
|
1659
|
+
}
|
|
1660
|
+
}
|
|
1661
|
+
return out;
|
|
1662
|
+
}
|
|
1185
1663
|
function toPosix(p) {
|
|
1186
1664
|
return p.split(path2.sep).join("/");
|
|
1187
1665
|
}
|
|
1188
1666
|
|
|
1189
1667
|
// ../ingestion/src/orchestrator.ts
|
|
1190
|
-
import { readFile as
|
|
1668
|
+
import { readFile as readFile5, stat as stat2 } from "fs/promises";
|
|
1191
1669
|
import { cpus } from "os";
|
|
1192
1670
|
import { join as join2 } from "path";
|
|
1193
|
-
import { createHash as
|
|
1671
|
+
import { createHash as createHash3 } from "crypto";
|
|
1194
1672
|
import ignore2 from "ignore";
|
|
1195
1673
|
|
|
1196
1674
|
// ../ingestion/src/embedder.ts
|
|
@@ -1204,8 +1682,8 @@ ${comment}`.trim();
|
|
|
1204
1682
|
async function embedNodes(nodes, opts) {
|
|
1205
1683
|
const batchSize = opts.batchSize ?? 100;
|
|
1206
1684
|
const candidates = nodes.filter((n) => n.kind !== "File");
|
|
1685
|
+
const totalSymbols = candidates.length;
|
|
1207
1686
|
const result = [];
|
|
1208
|
-
const total = Math.ceil(candidates.length / batchSize);
|
|
1209
1687
|
const namespace = `${opts.router.config.embeddingNamespace.provider}:${opts.router.config.embeddingNamespace.model}:${opts.router.config.embeddingNamespace.dimension}`;
|
|
1210
1688
|
for (let i = 0; i < candidates.length; i += batchSize) {
|
|
1211
1689
|
const slice = candidates.slice(i, i + batchSize);
|
|
@@ -1219,32 +1697,134 @@ async function embedNodes(nodes, opts) {
|
|
|
1219
1697
|
}
|
|
1220
1698
|
opts.onBatch?.({
|
|
1221
1699
|
batchIndex: i / batchSize,
|
|
1222
|
-
total,
|
|
1700
|
+
total: totalSymbols,
|
|
1223
1701
|
embedded: result.length
|
|
1224
1702
|
});
|
|
1225
1703
|
}
|
|
1226
1704
|
return result;
|
|
1227
1705
|
}
|
|
1228
1706
|
|
|
1229
|
-
// ../ingestion/src/
|
|
1230
|
-
import { readFile as readFile3
|
|
1707
|
+
// ../ingestion/src/tsconfig.ts
|
|
1708
|
+
import { readFile as readFile3 } from "fs/promises";
|
|
1231
1709
|
import path3 from "path";
|
|
1710
|
+
async function loadTsconfigPaths(repoRoot) {
|
|
1711
|
+
for (const name of ["tsconfig.json", "jsconfig.json"]) {
|
|
1712
|
+
const raw = await readIfExists(path3.join(repoRoot, name));
|
|
1713
|
+
if (!raw) continue;
|
|
1714
|
+
let parsed;
|
|
1715
|
+
try {
|
|
1716
|
+
parsed = JSON.parse(stripJsonComments(raw));
|
|
1717
|
+
} catch {
|
|
1718
|
+
continue;
|
|
1719
|
+
}
|
|
1720
|
+
const co = parsed.compilerOptions ?? {};
|
|
1721
|
+
const paths = co.paths ?? {};
|
|
1722
|
+
if (Object.keys(paths).length === 0) continue;
|
|
1723
|
+
const baseUrl = (co.baseUrl ?? ".").trim();
|
|
1724
|
+
return {
|
|
1725
|
+
baseUrl: toPosix2(baseUrl),
|
|
1726
|
+
paths
|
|
1727
|
+
};
|
|
1728
|
+
}
|
|
1729
|
+
return null;
|
|
1730
|
+
}
|
|
1731
|
+
async function readIfExists(filePath) {
|
|
1732
|
+
try {
|
|
1733
|
+
return await readFile3(filePath, "utf8");
|
|
1734
|
+
} catch {
|
|
1735
|
+
return null;
|
|
1736
|
+
}
|
|
1737
|
+
}
|
|
1738
|
+
function stripJsonComments(input3) {
|
|
1739
|
+
let out = "";
|
|
1740
|
+
let i = 0;
|
|
1741
|
+
let inString = false;
|
|
1742
|
+
let stringChar = "";
|
|
1743
|
+
while (i < input3.length) {
|
|
1744
|
+
const c = input3[i];
|
|
1745
|
+
const next = input3[i + 1];
|
|
1746
|
+
if (inString) {
|
|
1747
|
+
out += c;
|
|
1748
|
+
if (c === "\\" && next !== void 0) {
|
|
1749
|
+
out += next;
|
|
1750
|
+
i += 2;
|
|
1751
|
+
continue;
|
|
1752
|
+
}
|
|
1753
|
+
if (c === stringChar) inString = false;
|
|
1754
|
+
i++;
|
|
1755
|
+
continue;
|
|
1756
|
+
}
|
|
1757
|
+
if (c === '"' || c === "'") {
|
|
1758
|
+
inString = true;
|
|
1759
|
+
stringChar = c;
|
|
1760
|
+
out += c;
|
|
1761
|
+
i++;
|
|
1762
|
+
continue;
|
|
1763
|
+
}
|
|
1764
|
+
if (c === "/" && next === "/") {
|
|
1765
|
+
while (i < input3.length && input3[i] !== "\n") i++;
|
|
1766
|
+
continue;
|
|
1767
|
+
}
|
|
1768
|
+
if (c === "/" && next === "*") {
|
|
1769
|
+
i += 2;
|
|
1770
|
+
while (i < input3.length && !(input3[i] === "*" && input3[i + 1] === "/")) i++;
|
|
1771
|
+
i += 2;
|
|
1772
|
+
continue;
|
|
1773
|
+
}
|
|
1774
|
+
out += c;
|
|
1775
|
+
i++;
|
|
1776
|
+
}
|
|
1777
|
+
return removeTrailingCommas(out);
|
|
1778
|
+
}
|
|
1779
|
+
function removeTrailingCommas(s) {
|
|
1780
|
+
return s.replace(/,\s*([}\]])/g, "$1");
|
|
1781
|
+
}
|
|
1782
|
+
function toPosix2(p) {
|
|
1783
|
+
return p.split(path3.sep).join("/");
|
|
1784
|
+
}
|
|
1785
|
+
|
|
1786
|
+
// ../ingestion/src/repo-walker.ts
|
|
1787
|
+
import { readFile as readFile4, readdir, stat } from "fs/promises";
|
|
1788
|
+
import path4 from "path";
|
|
1232
1789
|
import ignore from "ignore";
|
|
1233
1790
|
var ALWAYS_SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
1234
1791
|
".git",
|
|
1792
|
+
".hg",
|
|
1793
|
+
".svn",
|
|
1235
1794
|
"node_modules",
|
|
1236
1795
|
".next",
|
|
1796
|
+
".nuxt",
|
|
1797
|
+
".svelte-kit",
|
|
1237
1798
|
"dist",
|
|
1238
1799
|
"build",
|
|
1239
1800
|
"out",
|
|
1240
1801
|
".turbo",
|
|
1241
1802
|
".cache",
|
|
1803
|
+
".parcel-cache",
|
|
1242
1804
|
"coverage",
|
|
1805
|
+
".nyc_output",
|
|
1243
1806
|
".idea",
|
|
1244
|
-
".vscode"
|
|
1807
|
+
".vscode",
|
|
1808
|
+
".vs",
|
|
1809
|
+
"venv",
|
|
1810
|
+
".venv",
|
|
1811
|
+
"env",
|
|
1812
|
+
"__pycache__",
|
|
1813
|
+
"site-packages",
|
|
1814
|
+
".pytest_cache",
|
|
1815
|
+
".mypy_cache",
|
|
1816
|
+
".ruff_cache",
|
|
1817
|
+
".tox",
|
|
1818
|
+
".eggs",
|
|
1819
|
+
"target",
|
|
1820
|
+
"vendor",
|
|
1821
|
+
"Pods",
|
|
1822
|
+
"DerivedData",
|
|
1823
|
+
".gradle",
|
|
1824
|
+
".terraform"
|
|
1245
1825
|
]);
|
|
1246
1826
|
async function walkRepo(root) {
|
|
1247
|
-
const base =
|
|
1827
|
+
const base = path4.resolve(root);
|
|
1248
1828
|
const matcher = ignore();
|
|
1249
1829
|
await loadGitignoreInto(matcher, base);
|
|
1250
1830
|
const out = [];
|
|
@@ -1261,7 +1841,7 @@ async function walkDir(absDir, relDir, ig, out) {
|
|
|
1261
1841
|
for (const entry of entries) {
|
|
1262
1842
|
const name = entry.name;
|
|
1263
1843
|
const rel = relDir ? `${relDir}/${name}` : name;
|
|
1264
|
-
const abs =
|
|
1844
|
+
const abs = path4.join(absDir, name);
|
|
1265
1845
|
if (entry.isDirectory()) {
|
|
1266
1846
|
if (ALWAYS_SKIP_DIRS.has(name)) continue;
|
|
1267
1847
|
if (ig.ignores(`${rel}/`)) continue;
|
|
@@ -1275,9 +1855,9 @@ async function walkDir(absDir, relDir, ig, out) {
|
|
|
1275
1855
|
}
|
|
1276
1856
|
}
|
|
1277
1857
|
async function loadGitignoreInto(ig, absDir) {
|
|
1278
|
-
const file =
|
|
1858
|
+
const file = path4.join(absDir, ".gitignore");
|
|
1279
1859
|
try {
|
|
1280
|
-
const text = await
|
|
1860
|
+
const text = await readFile4(file, "utf8");
|
|
1281
1861
|
ig.add(text);
|
|
1282
1862
|
} catch {
|
|
1283
1863
|
}
|
|
@@ -1307,7 +1887,7 @@ async function indexRepo(opts) {
|
|
|
1307
1887
|
await runWithConcurrency(parsable, parallelism, async (entry) => {
|
|
1308
1888
|
const abs = join2(opts.repoPath, entry.rel);
|
|
1309
1889
|
try {
|
|
1310
|
-
const source = await
|
|
1890
|
+
const source = await readFile5(abs, "utf8");
|
|
1311
1891
|
const result = await extractFile({
|
|
1312
1892
|
repoId: opts.repoId,
|
|
1313
1893
|
relativePath: entry.rel,
|
|
@@ -1324,11 +1904,13 @@ async function indexRepo(opts) {
|
|
|
1324
1904
|
opts.onProgress?.({ type: "parse", parsed: parsedCount, total: parsable.length });
|
|
1325
1905
|
}
|
|
1326
1906
|
});
|
|
1907
|
+
const tsconfigPaths = await loadTsconfigPaths(opts.repoPath);
|
|
1327
1908
|
const { resolved, dropped } = resolveEdges({
|
|
1328
1909
|
repoId: opts.repoId,
|
|
1329
1910
|
nodes: allNodes,
|
|
1330
1911
|
edges: allEdges,
|
|
1331
|
-
knownFilePaths
|
|
1912
|
+
knownFilePaths,
|
|
1913
|
+
...tsconfigPaths ? { tsconfigPaths } : {}
|
|
1332
1914
|
});
|
|
1333
1915
|
let embeddedById = /* @__PURE__ */ new Map();
|
|
1334
1916
|
if (!opts.skipEmbeddings && opts.router) {
|
|
@@ -1376,11 +1958,11 @@ async function runWithConcurrency(items, concurrency, fn) {
|
|
|
1376
1958
|
import kleur4 from "kleur";
|
|
1377
1959
|
|
|
1378
1960
|
// src/repo-id.ts
|
|
1379
|
-
import { createHash as
|
|
1380
|
-
import
|
|
1961
|
+
import { createHash as createHash4 } from "crypto";
|
|
1962
|
+
import path5 from "path";
|
|
1381
1963
|
function repoIdFromPath(absPath) {
|
|
1382
|
-
const base =
|
|
1383
|
-
const sha =
|
|
1964
|
+
const base = path5.basename(path5.resolve(absPath));
|
|
1965
|
+
const sha = createHash4("sha1").update(path5.resolve(absPath)).digest("hex").slice(0, 8);
|
|
1384
1966
|
const safe = base.replace(/[^A-Za-z0-9_-]/g, "_");
|
|
1385
1967
|
return `${safe}-${sha}`;
|
|
1386
1968
|
}
|
|
@@ -1388,7 +1970,7 @@ function repoIdFromPath(absPath) {
|
|
|
1388
1970
|
// src/commands/index.ts
|
|
1389
1971
|
async function runIndexCommand(opts) {
|
|
1390
1972
|
const config = await loadConfig();
|
|
1391
|
-
const absolutePath =
|
|
1973
|
+
const absolutePath = path6.resolve(opts.repoPath);
|
|
1392
1974
|
const repoId = repoIdFromPath(absolutePath);
|
|
1393
1975
|
const dbPath = config.data.dbPath ?? defaultDbPath();
|
|
1394
1976
|
console.log(kleur4.dim(`repo: ${absolutePath}`));
|
|
@@ -1426,8 +2008,10 @@ async function runIndexCommand(opts) {
|
|
|
1426
2008
|
router = void 0;
|
|
1427
2009
|
}
|
|
1428
2010
|
}
|
|
1429
|
-
|
|
1430
|
-
|
|
2011
|
+
const bars = {
|
|
2012
|
+
parse: null,
|
|
2013
|
+
embed: null
|
|
2014
|
+
};
|
|
1431
2015
|
const result = await indexRepo({
|
|
1432
2016
|
repoId,
|
|
1433
2017
|
repoPath: absolutePath,
|
|
@@ -1439,8 +2023,8 @@ async function runIndexCommand(opts) {
|
|
|
1439
2023
|
console.log(kleur4.dim(`walked ${event.files} files`));
|
|
1440
2024
|
}
|
|
1441
2025
|
if (event.type === "parse") {
|
|
1442
|
-
if (!
|
|
1443
|
-
|
|
2026
|
+
if (!bars.parse) bars.parse = new ProgressBar("parsing", event.total);
|
|
2027
|
+
bars.parse.update(event.parsed);
|
|
1444
2028
|
}
|
|
1445
2029
|
if (event.type === "upsert") {
|
|
1446
2030
|
console.log(
|
|
@@ -1448,13 +2032,13 @@ async function runIndexCommand(opts) {
|
|
|
1448
2032
|
);
|
|
1449
2033
|
}
|
|
1450
2034
|
if (event.type === "embed") {
|
|
1451
|
-
if (!
|
|
1452
|
-
|
|
2035
|
+
if (!bars.embed) bars.embed = new ProgressBar("embedding", event.total);
|
|
2036
|
+
bars.embed.update(event.embedded);
|
|
1453
2037
|
}
|
|
1454
2038
|
}
|
|
1455
2039
|
});
|
|
1456
|
-
|
|
1457
|
-
|
|
2040
|
+
bars.parse?.done();
|
|
2041
|
+
bars.embed?.done();
|
|
1458
2042
|
await graphDb.close();
|
|
1459
2043
|
console.log();
|
|
1460
2044
|
console.log(
|
|
@@ -1470,7 +2054,7 @@ async function runIndexCommand(opts) {
|
|
|
1470
2054
|
}
|
|
1471
2055
|
async function runStatusCommand(opts) {
|
|
1472
2056
|
const config = await loadConfig();
|
|
1473
|
-
const absolutePath =
|
|
2057
|
+
const absolutePath = path6.resolve(opts.repoPath);
|
|
1474
2058
|
const repoId = repoIdFromPath(absolutePath);
|
|
1475
2059
|
const graphDb = new GraphDb({
|
|
1476
2060
|
dbPath: config.data.dbPath ?? defaultDbPath(),
|
|
@@ -1488,16 +2072,71 @@ async function runStatusCommand(opts) {
|
|
|
1488
2072
|
throw err;
|
|
1489
2073
|
}
|
|
1490
2074
|
}
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
2075
|
+
async function runWipeCommand(opts) {
|
|
2076
|
+
const config = await loadConfig();
|
|
2077
|
+
const dbPath = config.data.dbPath ?? defaultDbPath();
|
|
2078
|
+
if (opts.repoPath) {
|
|
2079
|
+
const absolutePath = path6.resolve(opts.repoPath);
|
|
2080
|
+
const repoId = repoIdFromPath(absolutePath);
|
|
2081
|
+
if (!opts.yes) {
|
|
2082
|
+
console.log(
|
|
2083
|
+
kleur4.yellow(
|
|
2084
|
+
`Will delete all graph rows for ${kleur4.bold(repoId)} (${absolutePath}). Pass --yes to confirm.`
|
|
2085
|
+
)
|
|
2086
|
+
);
|
|
2087
|
+
return;
|
|
2088
|
+
}
|
|
2089
|
+
const spinner2 = makeSpinner(`Wiping ${repoId}`).start();
|
|
2090
|
+
const graphDb = new GraphDb({
|
|
2091
|
+
dbPath,
|
|
2092
|
+
embeddingDimension: config.llm.embeddings.dimension
|
|
2093
|
+
});
|
|
2094
|
+
try {
|
|
2095
|
+
await graphDb.connect();
|
|
2096
|
+
await graphDb.migrate();
|
|
2097
|
+
await graphDb.deleteByRepo(repoId);
|
|
2098
|
+
await graphDb.close();
|
|
2099
|
+
spinner2.succeed(`Removed ${repoId} from ${dbPath}`);
|
|
2100
|
+
} catch (err) {
|
|
2101
|
+
spinner2.fail("Wipe failed");
|
|
2102
|
+
throw err;
|
|
2103
|
+
}
|
|
2104
|
+
return;
|
|
2105
|
+
}
|
|
2106
|
+
if (!opts.yes) {
|
|
2107
|
+
console.log(
|
|
2108
|
+
kleur4.yellow(
|
|
2109
|
+
`Will delete the entire graph directory at ${kleur4.bold(dbPath)}. Pass --yes to confirm.`
|
|
2110
|
+
)
|
|
2111
|
+
);
|
|
2112
|
+
return;
|
|
2113
|
+
}
|
|
2114
|
+
const spinner = makeSpinner(`Removing ${dbPath}`).start();
|
|
2115
|
+
try {
|
|
2116
|
+
await rm(dbPath, { recursive: true, force: true });
|
|
2117
|
+
spinner.succeed(`Graph directory removed. Re-run \`codegraph index <repo>\` to rebuild.`);
|
|
2118
|
+
} catch (err) {
|
|
2119
|
+
spinner.fail("Wipe failed");
|
|
2120
|
+
throw err;
|
|
2121
|
+
}
|
|
2122
|
+
}
|
|
2123
|
+
|
|
2124
|
+
// src/commands/init.ts
|
|
2125
|
+
import { spawn } from "child_process";
|
|
2126
|
+
import path7 from "path";
|
|
2127
|
+
import { fileURLToPath } from "url";
|
|
2128
|
+
import { confirm, input as input2, password as password2, select as select2 } from "@inquirer/prompts";
|
|
2129
|
+
|
|
2130
|
+
// ../mcp-server/dist/index.js
|
|
2131
|
+
import { createHash as createHash5 } from "crypto";
|
|
2132
|
+
import { mkdir as mkdir3, readFile as readFile6, writeFile as writeFile2 } from "fs/promises";
|
|
2133
|
+
import { homedir } from "os";
|
|
2134
|
+
import { dirname as dirname3, resolve } from "path";
|
|
2135
|
+
import { randomUUID } from "crypto";
|
|
2136
|
+
import { join as join3 } from "path";
|
|
2137
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2138
|
+
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
|
|
2139
|
+
import express, { Router } from "express";
|
|
1501
2140
|
import { z } from "zod";
|
|
1502
2141
|
import { z as z2 } from "zod";
|
|
1503
2142
|
import { z as z3 } from "zod";
|
|
@@ -1510,7 +2149,7 @@ import { z as z9 } from "zod";
|
|
|
1510
2149
|
import { z as z10 } from "zod";
|
|
1511
2150
|
function buildCacheKey(toolName, args, namespace = "v1") {
|
|
1512
2151
|
const serialized = stableStringify(args);
|
|
1513
|
-
const hash =
|
|
2152
|
+
const hash = createHash5("sha1").update(serialized).digest("hex");
|
|
1514
2153
|
return `codegraph:${namespace}:tool:${toolName}:${hash}`;
|
|
1515
2154
|
}
|
|
1516
2155
|
function stableStringify(value) {
|
|
@@ -1587,8 +2226,8 @@ var affectedByTool = {
|
|
|
1587
2226
|
inputSchema,
|
|
1588
2227
|
annotations: { readOnlyHint: true, idempotentHint: true }
|
|
1589
2228
|
},
|
|
1590
|
-
handler: async ({ symbol, path:
|
|
1591
|
-
const where =
|
|
2229
|
+
handler: async ({ symbol, path: path8, depth, limit }, deps) => cachedJsonResult("affected_by", { symbol, path: path8, depth, limit }, deps, async () => {
|
|
2230
|
+
const where = path8 ? "WHERE target.name = $symbol AND target.path = $path" : "WHERE target.name = $symbol";
|
|
1592
2231
|
const rows = await deps.graph.query(
|
|
1593
2232
|
`MATCH (target:Symbol) ${where}
|
|
1594
2233
|
WITH target
|
|
@@ -1597,9 +2236,9 @@ var affectedByTool = {
|
|
|
1597
2236
|
RETURN affected.id AS id, affected.name AS name, affected.kind AS kind,
|
|
1598
2237
|
affected.path AS path, distance
|
|
1599
2238
|
ORDER BY distance ASC, affected.path ASC LIMIT $limit`,
|
|
1600
|
-
{ symbol, path:
|
|
2239
|
+
{ symbol, path: path8 ?? null, limit }
|
|
1601
2240
|
);
|
|
1602
|
-
return { symbol, path:
|
|
2241
|
+
return { symbol, path: path8 ?? null, depth, count: rows.length, affected: rows };
|
|
1603
2242
|
})
|
|
1604
2243
|
};
|
|
1605
2244
|
var inputSchema2 = {
|
|
@@ -1616,14 +2255,14 @@ var blastRadiusTool = {
|
|
|
1616
2255
|
inputSchema: inputSchema2,
|
|
1617
2256
|
annotations: { readOnlyHint: true, idempotentHint: true }
|
|
1618
2257
|
},
|
|
1619
|
-
handler: async ({ symbol, path:
|
|
1620
|
-
const where =
|
|
2258
|
+
handler: async ({ symbol, path: path8, depth, sampleSize }, deps) => cachedJsonResult("blast_radius", { symbol, path: path8, depth, sampleSize }, deps, async () => {
|
|
2259
|
+
const where = path8 ? "WHERE target.name = $symbol AND target.path = $path" : "WHERE target.name = $symbol";
|
|
1621
2260
|
const [stats] = await deps.graph.query(
|
|
1622
2261
|
`MATCH (target:Symbol) ${where}
|
|
1623
2262
|
WITH target
|
|
1624
2263
|
MATCH (dependent:Symbol)-[:CALLS|IMPORTS|RENDERS*1..${depth}]->(target)
|
|
1625
2264
|
RETURN count(DISTINCT dependent) AS total`,
|
|
1626
|
-
{ symbol, path:
|
|
2265
|
+
{ symbol, path: path8 ?? null }
|
|
1627
2266
|
);
|
|
1628
2267
|
const sample = await deps.graph.query(
|
|
1629
2268
|
`MATCH (target:Symbol) ${where}
|
|
@@ -1633,11 +2272,11 @@ var blastRadiusTool = {
|
|
|
1633
2272
|
RETURN dependent.id AS id, dependent.name AS name, dependent.kind AS kind,
|
|
1634
2273
|
dependent.path AS path, distance
|
|
1635
2274
|
ORDER BY distance ASC, dependent.path ASC LIMIT $sampleSize`,
|
|
1636
|
-
{ symbol, path:
|
|
2275
|
+
{ symbol, path: path8 ?? null, sampleSize }
|
|
1637
2276
|
);
|
|
1638
2277
|
return {
|
|
1639
2278
|
symbol,
|
|
1640
|
-
path:
|
|
2279
|
+
path: path8 ?? null,
|
|
1641
2280
|
depth,
|
|
1642
2281
|
totalDependents: stats?.total ?? 0,
|
|
1643
2282
|
sample
|
|
@@ -1704,15 +2343,46 @@ var getComponentTreeTool = {
|
|
|
1704
2343
|
annotations: { readOnlyHint: true, idempotentHint: true }
|
|
1705
2344
|
},
|
|
1706
2345
|
handler: async ({ name, depth }, deps) => cachedJsonResult("get_component_tree", { name, depth }, deps, async () => {
|
|
1707
|
-
const
|
|
1708
|
-
`MATCH (root:Symbol
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
2346
|
+
const rootRows = await deps.graph.query(
|
|
2347
|
+
`MATCH (root:Symbol)
|
|
2348
|
+
WHERE root.name = $name AND (root.kind = 'Component' OR root.kind = 'Function' OR root.kind = 'Class')
|
|
2349
|
+
RETURN root.id AS rootId, root.name AS rootName, root.path AS rootPath, root.kind AS rootKind
|
|
2350
|
+
ORDER BY CASE root.kind WHEN 'Component' THEN 0 WHEN 'Function' THEN 1 ELSE 2 END
|
|
2351
|
+
LIMIT 1`,
|
|
1713
2352
|
{ name }
|
|
1714
2353
|
);
|
|
1715
|
-
|
|
2354
|
+
const root = rootRows[0];
|
|
2355
|
+
if (!root) {
|
|
2356
|
+
return {
|
|
2357
|
+
name,
|
|
2358
|
+
depth,
|
|
2359
|
+
found: false,
|
|
2360
|
+
root: null,
|
|
2361
|
+
children: []
|
|
2362
|
+
};
|
|
2363
|
+
}
|
|
2364
|
+
const children = await deps.graph.query(
|
|
2365
|
+
`MATCH (root:Symbol {id: $rootId})
|
|
2366
|
+
OPTIONAL MATCH p = (root)-[:RENDERS*1..${depth}]->(child:Symbol)
|
|
2367
|
+
WITH child, min(length(p)) AS distance
|
|
2368
|
+
WHERE child IS NOT NULL
|
|
2369
|
+
RETURN child.id AS id, child.name AS name, child.kind AS kind,
|
|
2370
|
+
child.path AS path, distance
|
|
2371
|
+
ORDER BY distance ASC, child.name ASC`,
|
|
2372
|
+
{ rootId: root.rootId }
|
|
2373
|
+
);
|
|
2374
|
+
return {
|
|
2375
|
+
name,
|
|
2376
|
+
depth,
|
|
2377
|
+
found: true,
|
|
2378
|
+
root: {
|
|
2379
|
+
id: root.rootId,
|
|
2380
|
+
name: root.rootName,
|
|
2381
|
+
path: root.rootPath,
|
|
2382
|
+
kind: root.rootKind
|
|
2383
|
+
},
|
|
2384
|
+
children
|
|
2385
|
+
};
|
|
1716
2386
|
})
|
|
1717
2387
|
};
|
|
1718
2388
|
var inputSchema6 = {
|
|
@@ -1728,7 +2398,7 @@ var getDependenciesTool = {
|
|
|
1728
2398
|
inputSchema: inputSchema6,
|
|
1729
2399
|
annotations: { readOnlyHint: true, idempotentHint: true }
|
|
1730
2400
|
},
|
|
1731
|
-
handler: async ({ path:
|
|
2401
|
+
handler: async ({ path: path8, depth, limit }, deps) => cachedJsonResult("get_dependencies", { path: path8, depth, limit }, deps, async () => {
|
|
1732
2402
|
const rows = await deps.graph.query(
|
|
1733
2403
|
`MATCH (root:Symbol)
|
|
1734
2404
|
WHERE root.kind = 'File' AND root.path = $path
|
|
@@ -1738,9 +2408,9 @@ var getDependenciesTool = {
|
|
|
1738
2408
|
WHERE dep IS NOT NULL
|
|
1739
2409
|
RETURN dep.id AS id, dep.path AS path, distance
|
|
1740
2410
|
ORDER BY distance ASC, dep.path ASC LIMIT $limit`,
|
|
1741
|
-
{ path:
|
|
2411
|
+
{ path: path8, limit }
|
|
1742
2412
|
);
|
|
1743
|
-
return { path:
|
|
2413
|
+
return { path: path8, depth, count: rows.length, dependencies: rows };
|
|
1744
2414
|
})
|
|
1745
2415
|
};
|
|
1746
2416
|
var inputSchema7 = {
|
|
@@ -1755,15 +2425,15 @@ var getFileContextTool = {
|
|
|
1755
2425
|
inputSchema: inputSchema7,
|
|
1756
2426
|
annotations: { readOnlyHint: true, idempotentHint: true }
|
|
1757
2427
|
},
|
|
1758
|
-
handler: async ({ path:
|
|
2428
|
+
handler: async ({ path: path8, symbolLimit }, deps) => cachedJsonResult("get_file_context", { path: path8, symbolLimit }, deps, async () => {
|
|
1759
2429
|
const fileRows = await deps.graph.query(
|
|
1760
2430
|
`MATCH (f:Symbol)
|
|
1761
2431
|
WHERE f.kind = 'File' AND f.path = $path
|
|
1762
2432
|
RETURN f.id AS id, f.path AS path, f.name AS name LIMIT 1`,
|
|
1763
|
-
{ path:
|
|
2433
|
+
{ path: path8 }
|
|
1764
2434
|
);
|
|
1765
2435
|
if (fileRows.length === 0) {
|
|
1766
|
-
return { path:
|
|
2436
|
+
return { path: path8, found: false };
|
|
1767
2437
|
}
|
|
1768
2438
|
const [defined, imports, exports, importers] = await Promise.all([
|
|
1769
2439
|
deps.graph.query(
|
|
@@ -1771,30 +2441,33 @@ var getFileContextTool = {
|
|
|
1771
2441
|
WHERE f.kind = 'File' AND f.path = $path
|
|
1772
2442
|
RETURN s.id AS id, s.name AS name, s.kind AS kind, s.lineStart AS line, s.signature AS signature
|
|
1773
2443
|
ORDER BY s.lineStart LIMIT $limit`,
|
|
1774
|
-
{ path:
|
|
2444
|
+
{ path: path8, limit: symbolLimit }
|
|
1775
2445
|
),
|
|
1776
2446
|
deps.graph.query(
|
|
2447
|
+
// Kuzu drops the node variable `t` from scope after `RETURN DISTINCT`, so the
|
|
2448
|
+
// ORDER BY references the projection alias instead - using `t.path` here
|
|
2449
|
+
// raises a "Variable t is not in scope" binder exception.
|
|
1777
2450
|
`MATCH (f:Symbol)-[:IMPORTS]->(t:Symbol)
|
|
1778
2451
|
WHERE f.kind = 'File' AND t.kind = 'File' AND f.path = $path
|
|
1779
|
-
RETURN DISTINCT t.path AS path ORDER BY
|
|
1780
|
-
{ path:
|
|
2452
|
+
RETURN DISTINCT t.path AS path ORDER BY path`,
|
|
2453
|
+
{ path: path8 }
|
|
1781
2454
|
),
|
|
1782
2455
|
deps.graph.query(
|
|
1783
2456
|
`MATCH (f:Symbol)-[:EXPORTS]->(s:Symbol)
|
|
1784
2457
|
WHERE f.kind = 'File' AND f.path = $path
|
|
1785
2458
|
RETURN s.id AS id, s.name AS name, s.kind AS kind, s.lineStart AS line
|
|
1786
2459
|
ORDER BY s.name`,
|
|
1787
|
-
{ path:
|
|
2460
|
+
{ path: path8 }
|
|
1788
2461
|
),
|
|
1789
2462
|
deps.graph.query(
|
|
1790
2463
|
`MATCH (other:Symbol)-[:IMPORTS]->(f:Symbol)
|
|
1791
2464
|
WHERE other.kind = 'File' AND f.kind = 'File' AND f.path = $path
|
|
1792
|
-
RETURN DISTINCT other.path AS path ORDER BY
|
|
1793
|
-
{ path:
|
|
2465
|
+
RETURN DISTINCT other.path AS path ORDER BY path`,
|
|
2466
|
+
{ path: path8 }
|
|
1794
2467
|
)
|
|
1795
2468
|
]);
|
|
1796
2469
|
return {
|
|
1797
|
-
path:
|
|
2470
|
+
path: path8,
|
|
1798
2471
|
found: true,
|
|
1799
2472
|
file: fileRows[0],
|
|
1800
2473
|
definedSymbols: defined,
|
|
@@ -1839,11 +2512,11 @@ function findForbiddenKeyword(upper) {
|
|
|
1839
2512
|
}
|
|
1840
2513
|
return null;
|
|
1841
2514
|
}
|
|
1842
|
-
function validateReadOnlyCypher(
|
|
1843
|
-
if (typeof
|
|
2515
|
+
function validateReadOnlyCypher(input3) {
|
|
2516
|
+
if (typeof input3 !== "string" || input3.trim().length === 0) {
|
|
1844
2517
|
return { ok: false, reason: "Empty Cypher statement." };
|
|
1845
2518
|
}
|
|
1846
|
-
const withoutComments = stripComments(
|
|
2519
|
+
const withoutComments = stripComments(input3);
|
|
1847
2520
|
const sanitized = stripStringLiterals(withoutComments).trim();
|
|
1848
2521
|
const withoutTrailingSemicolon = sanitized.replace(/;\s*$/, "");
|
|
1849
2522
|
if (withoutTrailingSemicolon.includes(";")) {
|
|
@@ -2073,6 +2746,63 @@ function registerAllTools(server, deps) {
|
|
|
2073
2746
|
});
|
|
2074
2747
|
}
|
|
2075
2748
|
}
|
|
2749
|
+
function registerViewerApiRoutes(router, graph) {
|
|
2750
|
+
router.get("/api/repos", async (_req, res) => {
|
|
2751
|
+
try {
|
|
2752
|
+
const rows = await graph.query(
|
|
2753
|
+
"MATCH (n:Symbol) RETURN DISTINCT n.repoId AS repoId ORDER BY repoId"
|
|
2754
|
+
);
|
|
2755
|
+
const repos = rows.map((r) => r.repoId).filter((id) => typeof id === "string" && id.length > 0);
|
|
2756
|
+
res.json({ repos });
|
|
2757
|
+
} catch (err) {
|
|
2758
|
+
res.status(500).json({ error: String(err) });
|
|
2759
|
+
}
|
|
2760
|
+
});
|
|
2761
|
+
router.get("/api/graph", async (req, res) => {
|
|
2762
|
+
const repoId = req.query.repoId;
|
|
2763
|
+
if (!repoId) {
|
|
2764
|
+
res.status(400).json({ error: "Missing repoId query parameter" });
|
|
2765
|
+
return;
|
|
2766
|
+
}
|
|
2767
|
+
const limit = Math.min(Number(req.query.limit) || 600, 2e3);
|
|
2768
|
+
try {
|
|
2769
|
+
const nodeRows = await graph.query(
|
|
2770
|
+
`MATCH (n:Symbol)
|
|
2771
|
+
WHERE n.repoId = $repoId
|
|
2772
|
+
RETURN n.id AS id, n.kind AS kind, n.name AS name,
|
|
2773
|
+
n.path AS path, n.lineStart AS lineStart
|
|
2774
|
+
ORDER BY n.path ASC, n.lineStart ASC
|
|
2775
|
+
LIMIT $limit`,
|
|
2776
|
+
{ repoId, limit }
|
|
2777
|
+
);
|
|
2778
|
+
const nodeIds = new Set(
|
|
2779
|
+
nodeRows.map((n) => n.id).filter((v) => typeof v === "string")
|
|
2780
|
+
);
|
|
2781
|
+
const edgeRows = [];
|
|
2782
|
+
const edgeLimit = limit * 3;
|
|
2783
|
+
for (const kind of EDGE_KINDS) {
|
|
2784
|
+
const rows = await graph.query(
|
|
2785
|
+
`MATCH (a:Symbol)-[:${kind}]->(b:Symbol)
|
|
2786
|
+
WHERE a.repoId = $repoId AND b.repoId = $repoId
|
|
2787
|
+
RETURN a.id AS fromId, b.id AS toId
|
|
2788
|
+
LIMIT $edgeLimit`,
|
|
2789
|
+
{ repoId, edgeLimit }
|
|
2790
|
+
);
|
|
2791
|
+
for (const r of rows) {
|
|
2792
|
+
const from = r.fromId;
|
|
2793
|
+
const to = r.toId;
|
|
2794
|
+
if (typeof from !== "string" || typeof to !== "string") continue;
|
|
2795
|
+
if (!nodeIds.has(from) || !nodeIds.has(to)) continue;
|
|
2796
|
+
edgeRows.push({ from, to, kind });
|
|
2797
|
+
}
|
|
2798
|
+
}
|
|
2799
|
+
res.json({ repoId, nodes: nodeRows, edges: edgeRows });
|
|
2800
|
+
} catch (err) {
|
|
2801
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2802
|
+
res.status(500).json({ error: message });
|
|
2803
|
+
}
|
|
2804
|
+
});
|
|
2805
|
+
}
|
|
2076
2806
|
function createMcpServer(opts) {
|
|
2077
2807
|
const info = opts.serverInfo ?? { name: "codegraph", version: "0.0.0" };
|
|
2078
2808
|
const server = new McpServer(info, {
|
|
@@ -2089,6 +2819,23 @@ async function startSseServer(opts) {
|
|
|
2089
2819
|
app.get("/healthz", (_req, res) => {
|
|
2090
2820
|
res.status(200).json({ ok: true });
|
|
2091
2821
|
});
|
|
2822
|
+
if (opts.viewerDir) {
|
|
2823
|
+
const viewerDir = opts.viewerDir;
|
|
2824
|
+
const sendIndex = (_req, res, next) => {
|
|
2825
|
+
res.sendFile(join3(viewerDir, "index.html"), (err) => {
|
|
2826
|
+
if (err) next(err);
|
|
2827
|
+
});
|
|
2828
|
+
};
|
|
2829
|
+
app.get("/viewer", sendIndex);
|
|
2830
|
+
app.get("/viewer/", sendIndex);
|
|
2831
|
+
app.use(
|
|
2832
|
+
"/viewer",
|
|
2833
|
+
express.static(viewerDir, { index: false, fallthrough: false })
|
|
2834
|
+
);
|
|
2835
|
+
const apiRouter = Router();
|
|
2836
|
+
registerViewerApiRoutes(apiRouter, deps.graph);
|
|
2837
|
+
app.use(apiRouter);
|
|
2838
|
+
}
|
|
2092
2839
|
app.use(bearerAuthMiddleware(config.bearerToken, logger));
|
|
2093
2840
|
const transports = /* @__PURE__ */ new Map();
|
|
2094
2841
|
app.get("/mcp", async (_req, res) => {
|
|
@@ -2208,23 +2955,23 @@ var DEFAULT_TTL = 30;
|
|
|
2208
2955
|
function defaultConfigPath() {
|
|
2209
2956
|
return resolve(homedir(), ".codegraph", "config.json");
|
|
2210
2957
|
}
|
|
2211
|
-
async function readCodegraphConfig(
|
|
2958
|
+
async function readCodegraphConfig(path8 = defaultConfigPath()) {
|
|
2212
2959
|
try {
|
|
2213
|
-
const raw = await
|
|
2960
|
+
const raw = await readFile6(path8, "utf8");
|
|
2214
2961
|
return JSON.parse(raw);
|
|
2215
2962
|
} catch (err) {
|
|
2216
2963
|
if (err.code === "ENOENT") return {};
|
|
2217
2964
|
throw err;
|
|
2218
2965
|
}
|
|
2219
2966
|
}
|
|
2220
|
-
async function writeCodegraphConfig(config,
|
|
2221
|
-
await mkdir3(dirname3(
|
|
2222
|
-
await writeFile2(
|
|
2967
|
+
async function writeCodegraphConfig(config, path8 = defaultConfigPath()) {
|
|
2968
|
+
await mkdir3(dirname3(path8), { recursive: true });
|
|
2969
|
+
await writeFile2(path8, `${JSON.stringify(config, null, 2)}
|
|
2223
2970
|
`, "utf8");
|
|
2224
2971
|
}
|
|
2225
2972
|
async function resolveServerConfig(opts) {
|
|
2226
|
-
const
|
|
2227
|
-
const fileConfig = await readCodegraphConfig(
|
|
2973
|
+
const path8 = opts.configPath ?? defaultConfigPath();
|
|
2974
|
+
const fileConfig = await readCodegraphConfig(path8);
|
|
2228
2975
|
const env = process.env;
|
|
2229
2976
|
let bearerToken = opts.overrides?.bearerToken ?? fileConfig.mcpToken ?? fileConfig.server?.bearerToken ?? env.CODEGRAPH_BEARER_TOKEN ?? "";
|
|
2230
2977
|
let created = false;
|
|
@@ -2237,7 +2984,7 @@ async function resolveServerConfig(opts) {
|
|
|
2237
2984
|
mcpToken: bearerToken,
|
|
2238
2985
|
server: { ...fileConfig.server ?? {}, bearerToken }
|
|
2239
2986
|
},
|
|
2240
|
-
|
|
2987
|
+
path8
|
|
2241
2988
|
);
|
|
2242
2989
|
}
|
|
2243
2990
|
const config = {
|
|
@@ -2246,7 +2993,7 @@ async function resolveServerConfig(opts) {
|
|
|
2246
2993
|
bearerToken,
|
|
2247
2994
|
cacheTtlSeconds: opts.overrides?.cacheTtlSeconds ?? fileConfig.server?.cacheTtlSeconds ?? (env.CODEGRAPH_CACHE_TTL ? Number(env.CODEGRAPH_CACHE_TTL) : DEFAULT_TTL)
|
|
2248
2995
|
};
|
|
2249
|
-
return { config, configPath:
|
|
2996
|
+
return { config, configPath: path8, created };
|
|
2250
2997
|
}
|
|
2251
2998
|
function resolveDbPath(fileConfig) {
|
|
2252
2999
|
return fileConfig.data?.dbPath ?? process.env.CODEGRAPH_DB_PATH ?? void 0;
|
|
@@ -2289,12 +3036,12 @@ async function startMcpServer(portOrOptions) {
|
|
|
2289
3036
|
cacheTtlSeconds: config.cacheTtlSeconds,
|
|
2290
3037
|
logger
|
|
2291
3038
|
};
|
|
2292
|
-
return startSseServer({ deps, config });
|
|
3039
|
+
return startSseServer({ deps, config, viewerDir: options.viewerDir });
|
|
2293
3040
|
}
|
|
2294
3041
|
async function loadGraphClient(dbPath) {
|
|
2295
3042
|
let mod;
|
|
2296
3043
|
try {
|
|
2297
|
-
mod = await import("./src-
|
|
3044
|
+
mod = await import("./src-BTVJBGZ5.js");
|
|
2298
3045
|
} catch (err) {
|
|
2299
3046
|
throw new Error(
|
|
2300
3047
|
`Failed to import @codegraph/graph-db. Run \`pnpm -r build\` first. Underlying error: ${err instanceof Error ? err.message : String(err)}`
|
|
@@ -2320,7 +3067,7 @@ async function loadGraphClient(dbPath) {
|
|
|
2320
3067
|
async function loadLlmRouter(configPath2) {
|
|
2321
3068
|
let mod;
|
|
2322
3069
|
try {
|
|
2323
|
-
mod = await import("./src-
|
|
3070
|
+
mod = await import("./src-DBJQ22XR.js");
|
|
2324
3071
|
} catch (err) {
|
|
2325
3072
|
throw new Error(
|
|
2326
3073
|
`Failed to import @codegraph/llm-router. Run \`pnpm -r build\` first. Underlying error: ${err instanceof Error ? err.message : String(err)}`
|
|
@@ -2352,24 +3099,682 @@ function adaptLlmRouter(router) {
|
|
|
2352
3099
|
};
|
|
2353
3100
|
}
|
|
2354
3101
|
|
|
3102
|
+
// src/commands/init.ts
|
|
3103
|
+
import boxen2 from "boxen";
|
|
3104
|
+
import kleur5 from "kleur";
|
|
3105
|
+
var TOTAL_STEPS = 6;
|
|
3106
|
+
async function runInitCommand() {
|
|
3107
|
+
printWelcome();
|
|
3108
|
+
const start = await confirm({ message: "Ready to get started?", default: true });
|
|
3109
|
+
if (!start) {
|
|
3110
|
+
console.log(kleur5.dim("Aborted. Run `codegraph init` again any time."));
|
|
3111
|
+
return;
|
|
3112
|
+
}
|
|
3113
|
+
runEnvCheck();
|
|
3114
|
+
await runLlmSetup();
|
|
3115
|
+
await runLiveTest();
|
|
3116
|
+
await runIndexStep();
|
|
3117
|
+
const bearerToken = await runServeStep();
|
|
3118
|
+
await runClientStep(bearerToken);
|
|
3119
|
+
await runCelebration();
|
|
3120
|
+
}
|
|
3121
|
+
function printWelcome() {
|
|
3122
|
+
printBanner();
|
|
3123
|
+
const description = [
|
|
3124
|
+
kleur5.bold("CodeGraph") + " parses your JS/TS repo with tree-sitter, stores nodes (Function, Class,",
|
|
3125
|
+
"Route\u2026) and edges (CALLS, IMPORTS, RENDERS\u2026) in an embedded Kuzu graph with",
|
|
3126
|
+
"vector embeddings, then runs a local MCP server your AI assistant can call.",
|
|
3127
|
+
"",
|
|
3128
|
+
kleur5.dim("This wizard will:"),
|
|
3129
|
+
` ${kleur5.cyan("1.")} Configure an LLM provider (cloud or local)`,
|
|
3130
|
+
` ${kleur5.cyan("2.")} Verify your credentials with a live round-trip`,
|
|
3131
|
+
` ${kleur5.cyan("3.")} Index this repo into the graph`,
|
|
3132
|
+
` ${kleur5.cyan("4.")} Show you how to boot the MCP server`,
|
|
3133
|
+
` ${kleur5.cyan("5.")} Print a copy-paste config for your AI client`
|
|
3134
|
+
].join("\n");
|
|
3135
|
+
process.stdout.write(
|
|
3136
|
+
`${boxen2(description, {
|
|
3137
|
+
padding: 1,
|
|
3138
|
+
margin: { top: 0, bottom: 1, left: 0, right: 0 },
|
|
3139
|
+
borderStyle: "round",
|
|
3140
|
+
borderColor: "cyan",
|
|
3141
|
+
title: kleur5.bold("Welcome"),
|
|
3142
|
+
titleAlignment: "left"
|
|
3143
|
+
})}
|
|
3144
|
+
`
|
|
3145
|
+
);
|
|
3146
|
+
}
|
|
3147
|
+
function stepHeader(n, label) {
|
|
3148
|
+
console.log();
|
|
3149
|
+
console.log(
|
|
3150
|
+
kleur5.cyan(`\u2500\u2500\u2500\u2500 Step ${n}/${TOTAL_STEPS}: ${kleur5.bold(label)} ${"\u2500".repeat(Math.max(4, 40 - label.length))}`)
|
|
3151
|
+
);
|
|
3152
|
+
}
|
|
3153
|
+
function runEnvCheck() {
|
|
3154
|
+
stepHeader(1, "Environment check");
|
|
3155
|
+
const version = process.versions.node;
|
|
3156
|
+
const major = Number(version.split(".")[0] ?? 0);
|
|
3157
|
+
if (major < 20) {
|
|
3158
|
+
console.log(`${kleur5.red("\u2717")} node version v${version} ${kleur5.red("(codegraph requires >= 20)")}`);
|
|
3159
|
+
throw new Error(`Node ${version} is too old; install Node 20+ and re-run \`codegraph init\`.`);
|
|
3160
|
+
}
|
|
3161
|
+
console.log(`${kleur5.green("\u2713")} node version ${kleur5.dim(`v${version}`)}`);
|
|
3162
|
+
}
|
|
3163
|
+
async function runLlmSetup() {
|
|
3164
|
+
stepHeader(2, "LLM setup");
|
|
3165
|
+
const mode = await select2({
|
|
3166
|
+
message: "How would you like to power CodeGraph?",
|
|
3167
|
+
choices: [
|
|
3168
|
+
{
|
|
3169
|
+
name: "Local, zero cost",
|
|
3170
|
+
value: "local",
|
|
3171
|
+
description: "Runs entirely on your laptop. No API keys. Uses Ollama (guided install)."
|
|
3172
|
+
},
|
|
3173
|
+
{
|
|
3174
|
+
name: "Cloud provider",
|
|
3175
|
+
value: "cloud",
|
|
3176
|
+
description: "OpenAI, Anthropic, or Google. Better quality, requires an API key."
|
|
3177
|
+
}
|
|
3178
|
+
]
|
|
3179
|
+
});
|
|
3180
|
+
if (mode === "local") {
|
|
3181
|
+
await setupLocalOllama();
|
|
3182
|
+
} else {
|
|
3183
|
+
await setupCloudProvider();
|
|
3184
|
+
}
|
|
3185
|
+
}
|
|
3186
|
+
async function setupLocalOllama() {
|
|
3187
|
+
const spinner = makeSpinner("Detecting Ollama").start();
|
|
3188
|
+
let detection = await detectOllama();
|
|
3189
|
+
spinner.stop();
|
|
3190
|
+
if (detection.status === "not-installed") {
|
|
3191
|
+
console.log();
|
|
3192
|
+
console.log(kleur5.yellow(" Ollama is not installed."));
|
|
3193
|
+
console.log(` ${kleur5.cyan("\u2192")} Install from: ${kleur5.underline("https://ollama.com")}`);
|
|
3194
|
+
console.log(` ${kleur5.dim(" Then run: ollama serve")}`);
|
|
3195
|
+
openUrl("https://ollama.com");
|
|
3196
|
+
const next = await select2({
|
|
3197
|
+
message: "What would you like to do?",
|
|
3198
|
+
choices: [
|
|
3199
|
+
{ name: "I just installed it \u2014 detect again", value: "retry" },
|
|
3200
|
+
{ name: "Use a custom local server (llama.cpp, LM Studio, vLLM\u2026)", value: "custom" },
|
|
3201
|
+
{ name: "Switch to a cloud provider instead", value: "cloud" },
|
|
3202
|
+
{ name: "Skip for now (configure LLM later)", value: "skip" }
|
|
3203
|
+
]
|
|
3204
|
+
});
|
|
3205
|
+
if (next === "retry") {
|
|
3206
|
+
await setupLocalOllama();
|
|
3207
|
+
return;
|
|
3208
|
+
}
|
|
3209
|
+
if (next === "custom") {
|
|
3210
|
+
await setupCustomUrl();
|
|
3211
|
+
return;
|
|
3212
|
+
}
|
|
3213
|
+
if (next === "cloud") {
|
|
3214
|
+
await setupCloudProvider();
|
|
3215
|
+
return;
|
|
3216
|
+
}
|
|
3217
|
+
console.log(kleur5.yellow("! Skipped. Run `codegraph config llm set` to configure later."));
|
|
3218
|
+
return;
|
|
3219
|
+
}
|
|
3220
|
+
if (detection.status === "installed-not-running") {
|
|
3221
|
+
console.log();
|
|
3222
|
+
console.log(kleur5.yellow(" Ollama is installed but not running."));
|
|
3223
|
+
console.log(` Start it in another terminal: ${kleur5.cyan("ollama serve")}`);
|
|
3224
|
+
while (true) {
|
|
3225
|
+
const next = await select2({
|
|
3226
|
+
message: "What would you like to do?",
|
|
3227
|
+
choices: [
|
|
3228
|
+
{ name: "Retry (I started ollama serve)", value: "retry" },
|
|
3229
|
+
{ name: "Use a custom local server (llama.cpp, LM Studio, vLLM\u2026)", value: "custom" },
|
|
3230
|
+
{ name: "Switch to a cloud provider instead", value: "cloud" },
|
|
3231
|
+
{ name: "Skip for now (configure LLM later)", value: "skip" }
|
|
3232
|
+
]
|
|
3233
|
+
});
|
|
3234
|
+
if (next === "custom") {
|
|
3235
|
+
await setupCustomUrl();
|
|
3236
|
+
return;
|
|
3237
|
+
}
|
|
3238
|
+
if (next === "cloud") {
|
|
3239
|
+
await setupCloudProvider();
|
|
3240
|
+
return;
|
|
3241
|
+
}
|
|
3242
|
+
if (next === "skip") {
|
|
3243
|
+
console.log(kleur5.yellow("! Skipped. Run `codegraph config llm set` to configure later."));
|
|
3244
|
+
return;
|
|
3245
|
+
}
|
|
3246
|
+
const retrySpinner = makeSpinner("Checking Ollama").start();
|
|
3247
|
+
detection = await detectOllama();
|
|
3248
|
+
retrySpinner.stop();
|
|
3249
|
+
if (detection.status === "ready" || detection.status === "running-no-models") break;
|
|
3250
|
+
console.log(kleur5.red(" \u2717 Ollama still not reachable on :11434."));
|
|
3251
|
+
}
|
|
3252
|
+
}
|
|
3253
|
+
if (detection.status === "running-no-models") {
|
|
3254
|
+
console.log(kleur5.dim(` Ollama is running. Pulling required models\u2026`));
|
|
3255
|
+
try {
|
|
3256
|
+
await pullOllamaModels();
|
|
3257
|
+
} catch (err) {
|
|
3258
|
+
console.log(
|
|
3259
|
+
kleur5.yellow(`! Pull failed: ${err instanceof Error ? err.message : String(err)}`)
|
|
3260
|
+
);
|
|
3261
|
+
console.log(kleur5.dim(" You can pull manually: ollama pull qwen2.5-coder:1.5b && ollama pull nomic-embed-text"));
|
|
3262
|
+
}
|
|
3263
|
+
}
|
|
3264
|
+
if (detection.status === "ready") {
|
|
3265
|
+
console.log(`${kleur5.green("\u2713")} Ollama ready, required models present`);
|
|
3266
|
+
}
|
|
3267
|
+
const config = await loadConfig();
|
|
3268
|
+
const lookup = LLM_PRESETS["local-ollama"];
|
|
3269
|
+
config.llm = { ...config.llm, ...lookup };
|
|
3270
|
+
config.llm.baseUrl = void 0;
|
|
3271
|
+
await saveConfig(config);
|
|
3272
|
+
console.log(`${kleur5.green("\u2713")} preset saved ${kleur5.dim('"local-ollama" \u2192 ' + configPath())}`);
|
|
3273
|
+
console.log(`${kleur5.dim(" namespace ")}${namespaceLabel(config.llm)}`);
|
|
3274
|
+
}
|
|
3275
|
+
async function pullOllamaModels() {
|
|
3276
|
+
for (const model of [LOCAL_MODELS.generation, LOCAL_MODELS.embeddings]) {
|
|
3277
|
+
console.log(kleur5.dim(` pulling ${model}\u2026`));
|
|
3278
|
+
await new Promise((resolve2, reject) => {
|
|
3279
|
+
const child = spawn("ollama", ["pull", model], { stdio: "inherit" });
|
|
3280
|
+
child.on(
|
|
3281
|
+
"error",
|
|
3282
|
+
(err) => reject(new Error(`Failed to spawn ollama: ${err.message}`))
|
|
3283
|
+
);
|
|
3284
|
+
child.on("exit", (code) => {
|
|
3285
|
+
if (code === 0) resolve2();
|
|
3286
|
+
else reject(new Error(`ollama pull ${model} exited with code ${code}`));
|
|
3287
|
+
});
|
|
3288
|
+
});
|
|
3289
|
+
}
|
|
3290
|
+
console.log(`${kleur5.green("\u2713")} models ready`);
|
|
3291
|
+
}
|
|
3292
|
+
async function setupCustomUrl() {
|
|
3293
|
+
console.log();
|
|
3294
|
+
console.log(kleur5.dim(" Compatible servers: llama.cpp server, LM Studio, vLLM, LocalAI"));
|
|
3295
|
+
console.log(kleur5.dim(" Make sure your server exposes /v1/chat/completions and /v1/embeddings"));
|
|
3296
|
+
console.log();
|
|
3297
|
+
const baseUrl = await input2({
|
|
3298
|
+
message: "Server base URL:",
|
|
3299
|
+
default: "http://localhost:8080/v1",
|
|
3300
|
+
validate: (v) => v.trim().startsWith("http") ? true : "Must be an http(s) URL"
|
|
3301
|
+
});
|
|
3302
|
+
const genModel = await input2({
|
|
3303
|
+
message: "Generation model name (as loaded in your server):",
|
|
3304
|
+
default: "qwen2.5-coder"
|
|
3305
|
+
});
|
|
3306
|
+
const embedModel = await input2({
|
|
3307
|
+
message: "Embedding model name:",
|
|
3308
|
+
default: "nomic-embed-text"
|
|
3309
|
+
});
|
|
3310
|
+
const lookup = LLM_PRESETS["local-openai-compatible"];
|
|
3311
|
+
const config = await loadConfig();
|
|
3312
|
+
config.llm = {
|
|
3313
|
+
...config.llm,
|
|
3314
|
+
...lookup,
|
|
3315
|
+
generation: { provider: "openai", model: genModel.trim() },
|
|
3316
|
+
embeddings: { provider: "openai", model: embedModel.trim(), dimension: 768 },
|
|
3317
|
+
baseUrl: baseUrl.trim()
|
|
3318
|
+
};
|
|
3319
|
+
await saveConfig(config);
|
|
3320
|
+
console.log();
|
|
3321
|
+
console.log(
|
|
3322
|
+
`${kleur5.green("\u2713")} preset saved ${kleur5.dim('"local-openai-compatible" \u2192 ' + configPath())}`
|
|
3323
|
+
);
|
|
3324
|
+
console.log(`${kleur5.dim(" base URL ")}${kleur5.dim(baseUrl.trim())}`);
|
|
3325
|
+
console.log(
|
|
3326
|
+
kleur5.dim(
|
|
3327
|
+
" Tip: edit ~/.codegraph/config.json to adjust embeddings.dimension if your model differs from 768."
|
|
3328
|
+
)
|
|
3329
|
+
);
|
|
3330
|
+
}
|
|
3331
|
+
async function setupCloudProvider() {
|
|
3332
|
+
const preset = await select2({
|
|
3333
|
+
message: "Which cloud provider?",
|
|
3334
|
+
choices: [
|
|
3335
|
+
{
|
|
3336
|
+
name: "OpenAI",
|
|
3337
|
+
value: "byo-openai",
|
|
3338
|
+
description: "gpt-4o-mini + text-embedding-3-small"
|
|
3339
|
+
},
|
|
3340
|
+
{
|
|
3341
|
+
name: "Anthropic",
|
|
3342
|
+
value: "byo-anthropic",
|
|
3343
|
+
description: "claude-3-5-haiku (gen) + OpenAI text-embedding-3-small (embed)"
|
|
3344
|
+
},
|
|
3345
|
+
{
|
|
3346
|
+
name: "Google",
|
|
3347
|
+
value: "byo-google",
|
|
3348
|
+
description: "gemini-1.5-flash + text-embedding-004"
|
|
3349
|
+
}
|
|
3350
|
+
]
|
|
3351
|
+
});
|
|
3352
|
+
const lookup = LLM_PRESETS[preset];
|
|
3353
|
+
if (!lookup) throw new Error(`Unknown preset "${preset}".`);
|
|
3354
|
+
const config = await loadConfig();
|
|
3355
|
+
config.llm = { ...config.llm, ...lookup };
|
|
3356
|
+
config.llm.baseUrl = void 0;
|
|
3357
|
+
await saveConfig(config);
|
|
3358
|
+
console.log();
|
|
3359
|
+
console.log(`${kleur5.green("\u2713")} preset saved ${kleur5.dim(`"${preset}" \u2192 ${configPath()}`)}`);
|
|
3360
|
+
console.log(`${kleur5.dim(" namespace ")}${namespaceLabel(config.llm)}`);
|
|
3361
|
+
if (preset === "byo-anthropic") {
|
|
3362
|
+
console.log();
|
|
3363
|
+
console.log(kleur5.yellow("Note: Anthropic has no embedding API, so codegraph uses OpenAI embeddings."));
|
|
3364
|
+
console.log(kleur5.yellow("You'll need both ANTHROPIC_API_KEY and OPENAI_API_KEY set."));
|
|
3365
|
+
await promptApiKey("ANTHROPIC_API_KEY");
|
|
3366
|
+
await promptApiKey("OPENAI_API_KEY");
|
|
3367
|
+
return;
|
|
3368
|
+
}
|
|
3369
|
+
const envVar = apiKeyEnvVarFor2(preset);
|
|
3370
|
+
if (envVar) await promptApiKey(envVar);
|
|
3371
|
+
}
|
|
3372
|
+
function openUrl(url) {
|
|
3373
|
+
const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
3374
|
+
const child = spawn(cmd, [url], { detached: true, stdio: "ignore" });
|
|
3375
|
+
child.unref();
|
|
3376
|
+
}
|
|
3377
|
+
function apiKeyEnvVarFor2(preset) {
|
|
3378
|
+
if (preset === "byo-openai" || preset === "managed-stub") return "OPENAI_API_KEY";
|
|
3379
|
+
if (preset === "byo-anthropic") return "ANTHROPIC_API_KEY";
|
|
3380
|
+
if (preset === "byo-google") return "GOOGLE_GENERATIVE_AI_API_KEY";
|
|
3381
|
+
return null;
|
|
3382
|
+
}
|
|
3383
|
+
async function promptApiKey(envVar) {
|
|
3384
|
+
console.log();
|
|
3385
|
+
if (process.env[envVar]) {
|
|
3386
|
+
console.log(`${kleur5.green("\u2713")} ${envVar} ${kleur5.dim("already set in this shell")}`);
|
|
3387
|
+
return;
|
|
3388
|
+
}
|
|
3389
|
+
const provide = await confirm({
|
|
3390
|
+
message: `${envVar} is not set. Provide it now? (input will be hidden)`,
|
|
3391
|
+
default: true
|
|
3392
|
+
});
|
|
3393
|
+
if (!provide) {
|
|
3394
|
+
console.log(
|
|
3395
|
+
kleur5.yellow(`! Skipped. Export ${envVar} before \`codegraph index\` or \`codegraph serve\`.`)
|
|
3396
|
+
);
|
|
3397
|
+
return;
|
|
3398
|
+
}
|
|
3399
|
+
const value = await password2({ message: `${envVar}`, mask: "*" });
|
|
3400
|
+
if (!value) {
|
|
3401
|
+
console.log(kleur5.yellow(`! Empty value \u2014 skipping ${envVar}.`));
|
|
3402
|
+
return;
|
|
3403
|
+
}
|
|
3404
|
+
process.env[envVar] = value;
|
|
3405
|
+
console.log();
|
|
3406
|
+
console.log(kleur5.dim("Add this to your ~/.zshrc or ~/.bashrc so it persists:"));
|
|
3407
|
+
console.log(` ${kleur5.cyan(`export ${envVar}=${maskTail(value)}`)}`);
|
|
3408
|
+
}
|
|
3409
|
+
function maskTail(value) {
|
|
3410
|
+
if (value.length <= 8) return "*".repeat(value.length);
|
|
3411
|
+
return `${"*".repeat(value.length - 4)}${value.slice(-4)}`;
|
|
3412
|
+
}
|
|
3413
|
+
async function runLiveTest() {
|
|
3414
|
+
stepHeader(3, "Verify credentials");
|
|
3415
|
+
while (true) {
|
|
3416
|
+
const config = await loadConfig();
|
|
3417
|
+
console.log(
|
|
3418
|
+
kleur5.dim(
|
|
3419
|
+
` testing ${config.llm.generation.provider}:${config.llm.generation.model} + ${config.llm.embeddings.provider}:${config.llm.embeddings.model}`
|
|
3420
|
+
)
|
|
3421
|
+
);
|
|
3422
|
+
const spinner = makeSpinner("Calling provider").start();
|
|
3423
|
+
try {
|
|
3424
|
+
const router = await createLlmRouter({ config: config.llm });
|
|
3425
|
+
const result = await router.selfTest();
|
|
3426
|
+
spinner.succeed("LLM provider reachable");
|
|
3427
|
+
console.log(
|
|
3428
|
+
kleur5.dim(
|
|
3429
|
+
` embed=${result.embedLatencyMs}ms gen=${result.generateLatencyMs}ms dims=${result.embedDims}`
|
|
3430
|
+
)
|
|
3431
|
+
);
|
|
3432
|
+
return;
|
|
3433
|
+
} catch (err) {
|
|
3434
|
+
spinner.fail("LLM provider unreachable");
|
|
3435
|
+
console.log(kleur5.red(` ${err instanceof Error ? err.message : String(err)}`));
|
|
3436
|
+
const choice = await select2({
|
|
3437
|
+
message: "What now?",
|
|
3438
|
+
choices: [
|
|
3439
|
+
{ name: "Retry (I'll fix it in another terminal)", value: "retry" },
|
|
3440
|
+
{ name: "Skip \u2014 set up LLM later", value: "skip" }
|
|
3441
|
+
]
|
|
3442
|
+
});
|
|
3443
|
+
if (choice === "skip") {
|
|
3444
|
+
console.log(
|
|
3445
|
+
kleur5.yellow(
|
|
3446
|
+
"! Continuing without a verified LLM. Embeddings and semantic search will fail until you fix this."
|
|
3447
|
+
)
|
|
3448
|
+
);
|
|
3449
|
+
return;
|
|
3450
|
+
}
|
|
3451
|
+
}
|
|
3452
|
+
}
|
|
3453
|
+
}
|
|
3454
|
+
async function runIndexStep() {
|
|
3455
|
+
stepHeader(4, "Index this repo");
|
|
3456
|
+
const cwd = process.cwd();
|
|
3457
|
+
const display = abbreviateHome(cwd);
|
|
3458
|
+
const yes = await confirm({
|
|
3459
|
+
message: `Index the current directory (${display}) now?`,
|
|
3460
|
+
default: true
|
|
3461
|
+
});
|
|
3462
|
+
if (!yes) {
|
|
3463
|
+
console.log(kleur5.dim("Skipped. Run this when ready:"));
|
|
3464
|
+
console.log(` ${kleur5.cyan(`codegraph index ${display}`)}`);
|
|
3465
|
+
return;
|
|
3466
|
+
}
|
|
3467
|
+
console.log();
|
|
3468
|
+
const binPath = resolveBinPath();
|
|
3469
|
+
try {
|
|
3470
|
+
await spawnIndex(binPath, cwd);
|
|
3471
|
+
} catch (err) {
|
|
3472
|
+
console.log();
|
|
3473
|
+
console.log(
|
|
3474
|
+
kleur5.yellow(
|
|
3475
|
+
`! Index failed: ${err instanceof Error ? err.message : String(err)}`
|
|
3476
|
+
)
|
|
3477
|
+
);
|
|
3478
|
+
console.log(
|
|
3479
|
+
kleur5.dim(" You can retry later with `codegraph index <path>`. Continuing wizard.")
|
|
3480
|
+
);
|
|
3481
|
+
}
|
|
3482
|
+
}
|
|
3483
|
+
function resolveBinPath() {
|
|
3484
|
+
if (process.argv[1] && path7.isAbsolute(process.argv[1])) return process.argv[1];
|
|
3485
|
+
return fileURLToPath(new URL("./bin.js", import.meta.url));
|
|
3486
|
+
}
|
|
3487
|
+
function spawnIndex(binPath, repoPath) {
|
|
3488
|
+
return new Promise((resolvePromise, reject) => {
|
|
3489
|
+
const child = spawn(process.execPath, [binPath, "index", repoPath], {
|
|
3490
|
+
stdio: "inherit",
|
|
3491
|
+
env: process.env
|
|
3492
|
+
});
|
|
3493
|
+
child.on("error", reject);
|
|
3494
|
+
child.on("exit", (code, signal) => {
|
|
3495
|
+
if (signal) {
|
|
3496
|
+
reject(new Error(`codegraph index terminated by signal ${signal}`));
|
|
3497
|
+
return;
|
|
3498
|
+
}
|
|
3499
|
+
if (code !== 0) {
|
|
3500
|
+
reject(new Error(`codegraph index exited with code ${code}`));
|
|
3501
|
+
return;
|
|
3502
|
+
}
|
|
3503
|
+
resolvePromise();
|
|
3504
|
+
});
|
|
3505
|
+
});
|
|
3506
|
+
}
|
|
3507
|
+
async function runServeStep() {
|
|
3508
|
+
stepHeader(5, "Boot the MCP server");
|
|
3509
|
+
const { config: serverConfig, created } = await resolveServerConfig({});
|
|
3510
|
+
if (created) {
|
|
3511
|
+
console.log(kleur5.dim(" generated a new bearer token for this machine"));
|
|
3512
|
+
}
|
|
3513
|
+
const url = `http://${serverConfig.host}:${serverConfig.port}/mcp`;
|
|
3514
|
+
const body = [
|
|
3515
|
+
"Open a " + kleur5.bold("new terminal") + " and run:",
|
|
3516
|
+
"",
|
|
3517
|
+
` ${kleur5.cyan("codegraph serve")}`,
|
|
3518
|
+
"",
|
|
3519
|
+
kleur5.dim("Expected output:"),
|
|
3520
|
+
kleur5.dim(` codegraph mcp listening on ${url}`),
|
|
3521
|
+
"",
|
|
3522
|
+
kleur5.dim("Keep that terminal running while you use CodeGraph.")
|
|
3523
|
+
].join("\n");
|
|
3524
|
+
process.stdout.write(
|
|
3525
|
+
`${boxen2(body, {
|
|
3526
|
+
padding: { top: 0, bottom: 0, left: 2, right: 2 },
|
|
3527
|
+
margin: { top: 1, bottom: 1, left: 0, right: 0 },
|
|
3528
|
+
borderStyle: "round",
|
|
3529
|
+
borderColor: "cyan",
|
|
3530
|
+
title: kleur5.bold("Start the server"),
|
|
3531
|
+
titleAlignment: "left"
|
|
3532
|
+
})}
|
|
3533
|
+
`
|
|
3534
|
+
);
|
|
3535
|
+
await confirm({
|
|
3536
|
+
message: "Done \u2014 the server is running in another terminal?",
|
|
3537
|
+
default: true
|
|
3538
|
+
});
|
|
3539
|
+
return serverConfig.bearerToken;
|
|
3540
|
+
}
|
|
3541
|
+
async function runClientStep(bearerToken) {
|
|
3542
|
+
stepHeader(6, "Connect your AI client");
|
|
3543
|
+
const url = "http://127.0.0.1:3748/mcp";
|
|
3544
|
+
const client = await select2({
|
|
3545
|
+
message: "Which AI client are you connecting?",
|
|
3546
|
+
choices: [
|
|
3547
|
+
{ name: "Cursor", value: "cursor" },
|
|
3548
|
+
{ name: "Claude Code", value: "claude" },
|
|
3549
|
+
{ name: "Windsurf", value: "windsurf" },
|
|
3550
|
+
{ name: "Skip for now", value: "skip" }
|
|
3551
|
+
]
|
|
3552
|
+
});
|
|
3553
|
+
if (client === "skip") {
|
|
3554
|
+
console.log(
|
|
3555
|
+
kleur5.dim("Skipped. See docs/clients.md for per-client configuration when ready.")
|
|
3556
|
+
);
|
|
3557
|
+
return;
|
|
3558
|
+
}
|
|
3559
|
+
if (client === "cursor") {
|
|
3560
|
+
const snippet = JSON.stringify(
|
|
3561
|
+
{
|
|
3562
|
+
mcpServers: {
|
|
3563
|
+
codegraph: {
|
|
3564
|
+
url,
|
|
3565
|
+
headers: { Authorization: `Bearer ${bearerToken}` }
|
|
3566
|
+
}
|
|
3567
|
+
}
|
|
3568
|
+
},
|
|
3569
|
+
null,
|
|
3570
|
+
2
|
|
3571
|
+
);
|
|
3572
|
+
const body = [
|
|
3573
|
+
"Create or edit " + kleur5.cyan(".cursor/mcp.json") + " in your project root:",
|
|
3574
|
+
"",
|
|
3575
|
+
kleur5.green(snippet),
|
|
3576
|
+
"",
|
|
3577
|
+
kleur5.dim("Then open Cursor \u2192 Settings \u2192 MCP."),
|
|
3578
|
+
kleur5.dim("`codegraph` should appear as connected with 10 tools.")
|
|
3579
|
+
].join("\n");
|
|
3580
|
+
process.stdout.write(
|
|
3581
|
+
`${boxen2(body, {
|
|
3582
|
+
padding: 1,
|
|
3583
|
+
margin: { top: 1, bottom: 1, left: 0, right: 0 },
|
|
3584
|
+
borderStyle: "round",
|
|
3585
|
+
borderColor: "magenta",
|
|
3586
|
+
title: kleur5.bold("Cursor config"),
|
|
3587
|
+
titleAlignment: "left"
|
|
3588
|
+
})}
|
|
3589
|
+
`
|
|
3590
|
+
);
|
|
3591
|
+
} else if (client === "claude") {
|
|
3592
|
+
const oneliner = [
|
|
3593
|
+
"claude mcp add --transport sse codegraph \\",
|
|
3594
|
+
` ${url} \\`,
|
|
3595
|
+
` --header "Authorization: Bearer ${bearerToken}"`
|
|
3596
|
+
].join("\n");
|
|
3597
|
+
const body = [
|
|
3598
|
+
"Run this in your terminal:",
|
|
3599
|
+
"",
|
|
3600
|
+
kleur5.green(oneliner),
|
|
3601
|
+
"",
|
|
3602
|
+
kleur5.dim("Then type ") + kleur5.cyan("/mcp") + kleur5.dim(" in Claude Code to confirm 10 tools are listed.")
|
|
3603
|
+
].join("\n");
|
|
3604
|
+
process.stdout.write(
|
|
3605
|
+
`${boxen2(body, {
|
|
3606
|
+
padding: 1,
|
|
3607
|
+
margin: { top: 1, bottom: 1, left: 0, right: 0 },
|
|
3608
|
+
borderStyle: "round",
|
|
3609
|
+
borderColor: "magenta",
|
|
3610
|
+
title: kleur5.bold("Claude Code config"),
|
|
3611
|
+
titleAlignment: "left"
|
|
3612
|
+
})}
|
|
3613
|
+
`
|
|
3614
|
+
);
|
|
3615
|
+
} else {
|
|
3616
|
+
const snippet = JSON.stringify(
|
|
3617
|
+
{
|
|
3618
|
+
mcpServers: {
|
|
3619
|
+
codegraph: {
|
|
3620
|
+
serverUrl: url,
|
|
3621
|
+
headers: { Authorization: `Bearer ${bearerToken}` }
|
|
3622
|
+
}
|
|
3623
|
+
}
|
|
3624
|
+
},
|
|
3625
|
+
null,
|
|
3626
|
+
2
|
|
3627
|
+
);
|
|
3628
|
+
const body = [
|
|
3629
|
+
"Edit " + kleur5.cyan("~/.codeium/windsurf/mcp_config.json") + ":",
|
|
3630
|
+
"",
|
|
3631
|
+
kleur5.green(snippet),
|
|
3632
|
+
"",
|
|
3633
|
+
kleur5.dim("Then restart Windsurf or run ") + kleur5.cyan("/refresh-tools") + kleur5.dim(" in Cascade.")
|
|
3634
|
+
].join("\n");
|
|
3635
|
+
process.stdout.write(
|
|
3636
|
+
`${boxen2(body, {
|
|
3637
|
+
padding: 1,
|
|
3638
|
+
margin: { top: 1, bottom: 1, left: 0, right: 0 },
|
|
3639
|
+
borderStyle: "round",
|
|
3640
|
+
borderColor: "magenta",
|
|
3641
|
+
title: kleur5.bold("Windsurf config"),
|
|
3642
|
+
titleAlignment: "left"
|
|
3643
|
+
})}
|
|
3644
|
+
`
|
|
3645
|
+
);
|
|
3646
|
+
}
|
|
3647
|
+
await confirm({
|
|
3648
|
+
message: "Done \u2014 your client is configured?",
|
|
3649
|
+
default: true
|
|
3650
|
+
});
|
|
3651
|
+
}
|
|
3652
|
+
async function runCelebration() {
|
|
3653
|
+
console.log();
|
|
3654
|
+
console.log(kleur5.cyan(`\u2500\u2500\u2500\u2500 ${kleur5.bold("Almost there!")} \u2500\u2500\u2500\u2500`));
|
|
3655
|
+
console.log(
|
|
3656
|
+
kleur5.dim(
|
|
3657
|
+
'Ask your AI assistant something like: "What files are in this repo?"'
|
|
3658
|
+
)
|
|
3659
|
+
);
|
|
3660
|
+
console.log(
|
|
3661
|
+
kleur5.dim("It should call a `codegraph_*` MCP tool to answer.")
|
|
3662
|
+
);
|
|
3663
|
+
console.log();
|
|
3664
|
+
const choice = await select2({
|
|
3665
|
+
message: "How did it go?",
|
|
3666
|
+
choices: [
|
|
3667
|
+
{ name: "It worked!", value: "worked" },
|
|
3668
|
+
{ name: "Skip", value: "skip" }
|
|
3669
|
+
]
|
|
3670
|
+
});
|
|
3671
|
+
const header = choice === "worked" ? kleur5.green().bold("\u2713 CodeGraph is fully connected!") : kleur5.cyan().bold("\u2713 CodeGraph setup complete");
|
|
3672
|
+
const body = [
|
|
3673
|
+
header,
|
|
3674
|
+
"",
|
|
3675
|
+
"Your AI assistant can now call these tools:",
|
|
3676
|
+
` ${kleur5.cyan("\u2022")} find_callers \u2014 what calls a function`,
|
|
3677
|
+
` ${kleur5.cyan("\u2022")} get_component_tree \u2014 render dependency tree`,
|
|
3678
|
+
` ${kleur5.cyan("\u2022")} blast_radius \u2014 impact of a change`,
|
|
3679
|
+
` ${kleur5.cyan("\u2022")} search_semantic \u2014 symbol search by description`,
|
|
3680
|
+
` ${kleur5.cyan("\u2022")} nl_query \u2014 natural-language Cypher`,
|
|
3681
|
+
` ${kleur5.dim("\u2026plus 5 more (search_symbol, find_file, get_file_context,")}`,
|
|
3682
|
+
` ${kleur5.dim(" get_dependencies, affected_by)")}`,
|
|
3683
|
+
"",
|
|
3684
|
+
kleur5.dim("Visualise the graph:"),
|
|
3685
|
+
` ${kleur5.cyan("codegraph view")} ${kleur5.dim("\u2014 opens the interactive graph explorer in your browser")}`,
|
|
3686
|
+
` ${kleur5.dim("or visit")} ${kleur5.cyan("http://127.0.0.1:3748/viewer")} ${kleur5.dim("while the server is running")}`,
|
|
3687
|
+
"",
|
|
3688
|
+
kleur5.dim("Helpful next steps:"),
|
|
3689
|
+
` ${kleur5.cyan("codegraph doctor")} ${kleur5.dim("\u2014 verify environment + LLM + Kuzu")}`,
|
|
3690
|
+
` ${kleur5.cyan("codegraph status")} ${kleur5.dim("<path>")} ${kleur5.dim("\u2014 node/edge counts + embedding coverage")}`,
|
|
3691
|
+
"",
|
|
3692
|
+
kleur5.dim("Docs: https://github.com/leanlabsinnov/codegraph")
|
|
3693
|
+
].join("\n");
|
|
3694
|
+
process.stdout.write(
|
|
3695
|
+
`${boxen2(body, {
|
|
3696
|
+
padding: 1,
|
|
3697
|
+
margin: { top: 1, bottom: 1, left: 0, right: 0 },
|
|
3698
|
+
borderStyle: "round",
|
|
3699
|
+
borderColor: choice === "worked" ? "green" : "cyan"
|
|
3700
|
+
})}
|
|
3701
|
+
`
|
|
3702
|
+
);
|
|
3703
|
+
}
|
|
3704
|
+
function abbreviateHome(p) {
|
|
3705
|
+
const home = process.env.HOME ?? process.env.USERPROFILE;
|
|
3706
|
+
if (!home) return p;
|
|
3707
|
+
const rel = path7.relative(home, p);
|
|
3708
|
+
if (rel.startsWith("..")) return p;
|
|
3709
|
+
return rel ? `~${path7.sep}${rel}` : "~";
|
|
3710
|
+
}
|
|
3711
|
+
|
|
2355
3712
|
// src/commands/serve.ts
|
|
3713
|
+
import { realpathSync } from "fs";
|
|
3714
|
+
import { dirname as dirname4, join as join4 } from "path";
|
|
3715
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
3716
|
+
import kleur6 from "kleur";
|
|
3717
|
+
function resolveViewerDir() {
|
|
3718
|
+
try {
|
|
3719
|
+
const here = fileURLToPath2(import.meta.url);
|
|
3720
|
+
return join4(dirname4(here), "..", "viewer");
|
|
3721
|
+
} catch {
|
|
3722
|
+
}
|
|
3723
|
+
try {
|
|
3724
|
+
const bin = realpathSync(process.argv[1] ?? "");
|
|
3725
|
+
return join4(dirname4(bin), "..", "viewer");
|
|
3726
|
+
} catch {
|
|
3727
|
+
return join4(dirname4(process.argv[1] ?? ""), "..", "viewer");
|
|
3728
|
+
}
|
|
3729
|
+
}
|
|
3730
|
+
function providerEnvVar2(provider) {
|
|
3731
|
+
if (provider === "openai") return "OPENAI_API_KEY";
|
|
3732
|
+
if (provider === "anthropic") return "ANTHROPIC_API_KEY";
|
|
3733
|
+
if (provider === "google") return "GOOGLE_GENERATIVE_AI_API_KEY";
|
|
3734
|
+
return null;
|
|
3735
|
+
}
|
|
2356
3736
|
async function runServeCommand(opts = {}) {
|
|
3737
|
+
try {
|
|
3738
|
+
const config = await loadConfig();
|
|
3739
|
+
const missing = [];
|
|
3740
|
+
for (const provider of [
|
|
3741
|
+
config.llm.generation.provider,
|
|
3742
|
+
config.llm.embeddings.provider
|
|
3743
|
+
]) {
|
|
3744
|
+
const envVar = providerEnvVar2(provider);
|
|
3745
|
+
if (envVar && !process.env[envVar] && !missing.includes(envVar)) {
|
|
3746
|
+
missing.push(envVar);
|
|
3747
|
+
}
|
|
3748
|
+
}
|
|
3749
|
+
if (missing.length > 0) {
|
|
3750
|
+
process.stderr.write(
|
|
3751
|
+
`${kleur6.yellow(
|
|
3752
|
+
`! ${missing.join(", ")} not set in serve's environment. Semantic search and nl_query will fail until the variable is exported before \`codegraph serve\`.`
|
|
3753
|
+
)}
|
|
3754
|
+
`
|
|
3755
|
+
);
|
|
3756
|
+
}
|
|
3757
|
+
} catch {
|
|
3758
|
+
}
|
|
2357
3759
|
const spinner = makeSpinner("Booting MCP server").start();
|
|
2358
3760
|
let started;
|
|
2359
3761
|
try {
|
|
2360
3762
|
started = await startMcpServer({
|
|
2361
3763
|
...opts.port !== void 0 ? { port: opts.port } : {},
|
|
2362
3764
|
...opts.host !== void 0 ? { host: opts.host } : {},
|
|
2363
|
-
...opts.dbPath !== void 0 ? { dbPath: opts.dbPath } : {}
|
|
3765
|
+
...opts.dbPath !== void 0 ? { dbPath: opts.dbPath } : {},
|
|
3766
|
+
viewerDir: resolveViewerDir()
|
|
2364
3767
|
});
|
|
2365
3768
|
spinner.stop();
|
|
2366
3769
|
} catch (err) {
|
|
2367
3770
|
spinner.fail("Server failed to start");
|
|
2368
3771
|
throw err;
|
|
2369
3772
|
}
|
|
2370
|
-
const
|
|
3773
|
+
const base = `http://${started.address.host}:${started.address.port}`;
|
|
3774
|
+
const url = `${base}/mcp`;
|
|
3775
|
+
const viewerUrl = `${base}/viewer`;
|
|
2371
3776
|
const tokenHint = "bearer token at ~/.codegraph/config.json (codegraph config show to view)";
|
|
2372
|
-
process.stdout.write(`${renderServeBanner(url, tokenHint)}
|
|
3777
|
+
process.stdout.write(`${renderServeBanner(url, tokenHint, viewerUrl)}
|
|
2373
3778
|
`);
|
|
2374
3779
|
const shutdown = async (signal) => {
|
|
2375
3780
|
process.stderr.write(`
|
|
@@ -2391,16 +3796,70 @@ shutting down (${signal})...
|
|
|
2391
3796
|
});
|
|
2392
3797
|
}
|
|
2393
3798
|
|
|
3799
|
+
// src/commands/view.ts
|
|
3800
|
+
import { exec } from "child_process";
|
|
3801
|
+
import kleur7 from "kleur";
|
|
3802
|
+
var DEFAULT_HOST2 = "127.0.0.1";
|
|
3803
|
+
function openBrowser(url) {
|
|
3804
|
+
const cmd = process.platform === "darwin" ? `open "${url}"` : process.platform === "win32" ? `start "" "${url}"` : `xdg-open "${url}"`;
|
|
3805
|
+
exec(cmd, (err) => {
|
|
3806
|
+
if (err) {
|
|
3807
|
+
process.stderr.write(
|
|
3808
|
+
`${kleur7.yellow("!")} Could not open browser automatically. Open manually:
|
|
3809
|
+
${kleur7.cyan(url)}
|
|
3810
|
+
`
|
|
3811
|
+
);
|
|
3812
|
+
}
|
|
3813
|
+
});
|
|
3814
|
+
}
|
|
3815
|
+
async function isServerUp(host, port) {
|
|
3816
|
+
try {
|
|
3817
|
+
const res = await fetch(`http://${host}:${port}/healthz`, {
|
|
3818
|
+
signal: AbortSignal.timeout(1500)
|
|
3819
|
+
});
|
|
3820
|
+
return res.ok;
|
|
3821
|
+
} catch {
|
|
3822
|
+
return false;
|
|
3823
|
+
}
|
|
3824
|
+
}
|
|
3825
|
+
async function runViewCommand(opts = {}) {
|
|
3826
|
+
const host = opts.host ?? DEFAULT_HOST2;
|
|
3827
|
+
const port = opts.port ?? MCP_PORT;
|
|
3828
|
+
const viewerUrl = `http://${host}:${port}/viewer`;
|
|
3829
|
+
process.stdout.write(kleur7.dim(`Checking server at http://${host}:${port}/healthz \u2026
|
|
3830
|
+
`));
|
|
3831
|
+
const up = await isServerUp(host, port);
|
|
3832
|
+
if (!up) {
|
|
3833
|
+
process.stdout.write(
|
|
3834
|
+
[
|
|
3835
|
+
"",
|
|
3836
|
+
`${kleur7.red("\u2717")} CodeGraph server is not running.`,
|
|
3837
|
+
"",
|
|
3838
|
+
` Start it with: ${kleur7.cyan("codegraph serve")}`,
|
|
3839
|
+
` Then re-run: ${kleur7.cyan("codegraph view")}`,
|
|
3840
|
+
""
|
|
3841
|
+
].join("\n")
|
|
3842
|
+
);
|
|
3843
|
+
process.exit(1);
|
|
3844
|
+
}
|
|
3845
|
+
process.stdout.write(`${kleur7.green("\u2713")} Server is up. Opening ${kleur7.cyan(viewerUrl)} \u2026
|
|
3846
|
+
`);
|
|
3847
|
+
openBrowser(viewerUrl);
|
|
3848
|
+
}
|
|
3849
|
+
|
|
2394
3850
|
// src/program.ts
|
|
2395
3851
|
function buildProgram() {
|
|
2396
3852
|
const program = new Command();
|
|
2397
|
-
program.name("codegraph").description("Live, queryable knowledge graph for your codebase").version("0.1.
|
|
3853
|
+
program.name("codegraph").description("Live, queryable knowledge graph for your codebase").version("0.1.3").option("--verbose", "Print full stack traces on error").hook("preAction", (thisCommand) => {
|
|
2398
3854
|
const opts = thisCommand.optsWithGlobals();
|
|
2399
3855
|
if (opts.verbose) process.env.CODEGRAPH_VERBOSE = "1";
|
|
2400
3856
|
});
|
|
2401
3857
|
program.on("--help", () => {
|
|
2402
3858
|
printBanner();
|
|
2403
3859
|
});
|
|
3860
|
+
program.command("init").description("Interactive setup wizard \u2014 LLM, credentials, index, MCP, client connect").action(async () => {
|
|
3861
|
+
await runInitCommand();
|
|
3862
|
+
});
|
|
2404
3863
|
program.command("index").description("Parse a JS/TS repo into the local embedded graph").argument("<path>", "Path to the repo root").option("--no-embed", "Skip the embedding pass (faster, no LLM calls)").action(async (repoPath, opts) => {
|
|
2405
3864
|
await runIndexCommand({ repoPath, noEmbed: opts.embed === false });
|
|
2406
3865
|
});
|
|
@@ -2414,9 +3873,21 @@ function buildProgram() {
|
|
|
2414
3873
|
...opts.dbPath !== void 0 ? { dbPath: opts.dbPath } : {}
|
|
2415
3874
|
});
|
|
2416
3875
|
});
|
|
3876
|
+
program.command("view").description("Open the graph viewer in the browser (requires codegraph serve to be running)").option("--port <port>", "MCP server port to check (default 3748)", (v) => Number(v)).option("--host <host>", "MCP server host (default 127.0.0.1)").action(async (opts) => {
|
|
3877
|
+
await runViewCommand({
|
|
3878
|
+
...opts.port !== void 0 ? { port: opts.port } : {},
|
|
3879
|
+
...opts.host !== void 0 ? { host: opts.host } : {}
|
|
3880
|
+
});
|
|
3881
|
+
});
|
|
2417
3882
|
program.command("doctor").description("Check environment, config, LLM credentials, and Kuzu DB health").action(async () => {
|
|
2418
3883
|
await runDoctorCommand();
|
|
2419
3884
|
});
|
|
3885
|
+
program.command("wipe").description("Delete a single repo's slice (with [path]) or the entire on-disk graph").argument("[path]", "Repo path. Omit to wipe the whole graph directory.").option("--yes", "Skip the confirmation prompt", false).action(async (repoPath, opts) => {
|
|
3886
|
+
await runWipeCommand({
|
|
3887
|
+
...repoPath ? { repoPath } : {},
|
|
3888
|
+
yes: opts.yes === true
|
|
3889
|
+
});
|
|
3890
|
+
});
|
|
2420
3891
|
const configCmd = program.command("config").description("Manage ~/.codegraph/config.json");
|
|
2421
3892
|
configCmd.command("show").description("Print the resolved config").action(async () => {
|
|
2422
3893
|
await runConfigShow();
|
|
@@ -2440,4 +3911,4 @@ export {
|
|
|
2440
3911
|
renderError,
|
|
2441
3912
|
buildProgram
|
|
2442
3913
|
};
|
|
2443
|
-
//# sourceMappingURL=chunk-
|
|
3914
|
+
//# sourceMappingURL=chunk-64OLVRK3.js.map
|