@leanlabsinnov/codegraph 0.1.3 → 0.1.5
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-AVP24SX5.js → chunk-C2AULDUQ.js} +2 -2
- package/dist/{chunk-B2TIVKUB.js → chunk-GOJIV25M.js} +10 -7
- package/dist/chunk-GOJIV25M.js.map +1 -0
- package/dist/{chunk-5WYXRWEY.js → chunk-KYPDPBI5.js} +933 -176
- package/dist/chunk-KYPDPBI5.js.map +1 -0
- package/dist/{chunk-XGPZDCQ4.js → chunk-Z6DQLXRR.js} +18 -3
- package/dist/chunk-Z6DQLXRR.js.map +1 -0
- package/dist/index.js +4 -4
- package/dist/src-HB4UDUBX.js +10 -0
- package/dist/{src-M7HSEMBT.js → src-IKWDKNPH.js} +3 -3
- package/package.json +2 -1
- package/viewer/index.html +1466 -0
- package/dist/chunk-5WYXRWEY.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/{chunk-AVP24SX5.js.map → chunk-C2AULDUQ.js.map} +0 -0
- /package/dist/{src-M7HSEMBT.js.map → src-HB4UDUBX.js.map} +0 -0
- /package/dist/{src-UVET6JHH.js.map → src-IKWDKNPH.js.map} +0 -0
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
import {
|
|
2
2
|
createLlmRouter,
|
|
3
3
|
namespaceLabel
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-GOJIV25M.js";
|
|
5
5
|
import {
|
|
6
6
|
GraphDb,
|
|
7
7
|
defaultDbPath
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-C2AULDUQ.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-Z6DQLXRR.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));
|
|
@@ -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 });
|
|
@@ -471,7 +562,8 @@ var GRAMMAR_FILE = {
|
|
|
471
562
|
typescript: "tree-sitter-typescript.wasm",
|
|
472
563
|
tsx: "tree-sitter-tsx.wasm",
|
|
473
564
|
javascript: "tree-sitter-javascript.wasm",
|
|
474
|
-
jsx: "tree-sitter-javascript.wasm"
|
|
565
|
+
jsx: "tree-sitter-javascript.wasm",
|
|
566
|
+
python: "tree-sitter-python.wasm"
|
|
475
567
|
};
|
|
476
568
|
var initPromise = null;
|
|
477
569
|
var languageCache = /* @__PURE__ */ new Map();
|
|
@@ -510,6 +602,8 @@ function detectLanguage(filePath) {
|
|
|
510
602
|
return "javascript";
|
|
511
603
|
case ".jsx":
|
|
512
604
|
return "jsx";
|
|
605
|
+
case ".py":
|
|
606
|
+
return "python";
|
|
513
607
|
default:
|
|
514
608
|
return null;
|
|
515
609
|
}
|
|
@@ -531,8 +625,8 @@ async function parseSource(source, language) {
|
|
|
531
625
|
}
|
|
532
626
|
|
|
533
627
|
// ../ingestion/src/extractors/extract.ts
|
|
534
|
-
import { createHash } from "crypto";
|
|
535
|
-
import { basename } from "path";
|
|
628
|
+
import { createHash as createHash2 } from "crypto";
|
|
629
|
+
import { basename as basename2 } from "path";
|
|
536
630
|
|
|
537
631
|
// ../ingestion/src/walker.ts
|
|
538
632
|
function* walk(node) {
|
|
@@ -591,6 +685,325 @@ function isPascalCase(name) {
|
|
|
591
685
|
return /^[A-Z][A-Za-z0-9]*$/.test(name);
|
|
592
686
|
}
|
|
593
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
|
+
|
|
594
1007
|
// ../ingestion/src/extractors/routes.ts
|
|
595
1008
|
var EXPRESS_METHODS = /* @__PURE__ */ new Set([
|
|
596
1009
|
"get",
|
|
@@ -612,44 +1025,44 @@ var HTTP_VERBS = /* @__PURE__ */ new Set([
|
|
|
612
1025
|
"OPTIONS",
|
|
613
1026
|
"HEAD"
|
|
614
1027
|
]);
|
|
615
|
-
function detectRoutes(
|
|
1028
|
+
function detectRoutes(input3) {
|
|
616
1029
|
return [
|
|
617
|
-
...detectExpressRoutes(
|
|
618
|
-
...detectNextAppRouterRoutes(
|
|
619
|
-
...detectNextPagesApiRoutes(
|
|
1030
|
+
...detectExpressRoutes(input3),
|
|
1031
|
+
...detectNextAppRouterRoutes(input3),
|
|
1032
|
+
...detectNextPagesApiRoutes(input3)
|
|
620
1033
|
];
|
|
621
1034
|
}
|
|
622
|
-
function detectExpressRoutes(
|
|
1035
|
+
function detectExpressRoutes(input3) {
|
|
623
1036
|
const out = [];
|
|
624
|
-
for (const node of walk(
|
|
1037
|
+
for (const node of walk(input3.rootNode)) {
|
|
625
1038
|
if (node.type !== "call_expression") continue;
|
|
626
1039
|
const callee = node.childForFieldName("function");
|
|
627
1040
|
if (!callee || callee.type !== "member_expression") continue;
|
|
628
1041
|
const obj = callee.childForFieldName("object");
|
|
629
1042
|
const prop = callee.childForFieldName("property");
|
|
630
1043
|
if (!obj || !prop) continue;
|
|
631
|
-
const method = nodeText(prop,
|
|
1044
|
+
const method = nodeText(prop, input3.source).toLowerCase();
|
|
632
1045
|
if (!EXPRESS_METHODS.has(method)) continue;
|
|
633
1046
|
const args = node.childForFieldName("arguments");
|
|
634
1047
|
if (!args) continue;
|
|
635
1048
|
const firstArg = args.namedChild(0);
|
|
636
1049
|
if (!firstArg || firstArg.type !== "string") continue;
|
|
637
|
-
const routePath = stripQuotes(nodeText(firstArg,
|
|
1050
|
+
const routePath = stripQuotes(nodeText(firstArg, input3.source));
|
|
638
1051
|
if (!routePath.startsWith("/")) continue;
|
|
639
1052
|
const line = startLine(node);
|
|
640
1053
|
const name = `${method.toUpperCase()} ${routePath}`;
|
|
641
1054
|
out.push({
|
|
642
1055
|
id: makeNodeId({
|
|
643
|
-
repoId:
|
|
1056
|
+
repoId: input3.repoId,
|
|
644
1057
|
kind: "Route",
|
|
645
|
-
path:
|
|
1058
|
+
path: input3.relativePath,
|
|
646
1059
|
name,
|
|
647
1060
|
line
|
|
648
1061
|
}),
|
|
649
1062
|
kind: "Route",
|
|
650
|
-
repoId:
|
|
1063
|
+
repoId: input3.repoId,
|
|
651
1064
|
name,
|
|
652
|
-
path:
|
|
1065
|
+
path: input3.relativePath,
|
|
653
1066
|
lineStart: line,
|
|
654
1067
|
lineEnd: endLine(node),
|
|
655
1068
|
method: method.toUpperCase(),
|
|
@@ -659,29 +1072,29 @@ function detectExpressRoutes(input) {
|
|
|
659
1072
|
}
|
|
660
1073
|
return out;
|
|
661
1074
|
}
|
|
662
|
-
function detectNextAppRouterRoutes(
|
|
663
|
-
if (!/(^|\/)app\/.+\/route\.(ts|tsx|js|jsx|mjs|cjs)$/.test(
|
|
664
|
-
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);
|
|
665
1078
|
const out = [];
|
|
666
|
-
for (const node of walk(
|
|
1079
|
+
for (const node of walk(input3.rootNode)) {
|
|
667
1080
|
if (node.type !== "export_statement") continue;
|
|
668
1081
|
const decl = findChildByType(node, "function_declaration") ?? findChildByType(node, "lexical_declaration");
|
|
669
1082
|
if (!decl) continue;
|
|
670
|
-
const name = extractTopLevelName(decl,
|
|
1083
|
+
const name = extractTopLevelName(decl, input3.source);
|
|
671
1084
|
if (!name || !HTTP_VERBS.has(name)) continue;
|
|
672
1085
|
const line = startLine(node);
|
|
673
1086
|
out.push({
|
|
674
1087
|
id: makeNodeId({
|
|
675
|
-
repoId:
|
|
1088
|
+
repoId: input3.repoId,
|
|
676
1089
|
kind: "Route",
|
|
677
|
-
path:
|
|
1090
|
+
path: input3.relativePath,
|
|
678
1091
|
name: `${name} ${routePath}`,
|
|
679
1092
|
line
|
|
680
1093
|
}),
|
|
681
1094
|
kind: "Route",
|
|
682
|
-
repoId:
|
|
1095
|
+
repoId: input3.repoId,
|
|
683
1096
|
name: `${name} ${routePath}`,
|
|
684
|
-
path:
|
|
1097
|
+
path: input3.relativePath,
|
|
685
1098
|
lineStart: line,
|
|
686
1099
|
lineEnd: endLine(node),
|
|
687
1100
|
method: name,
|
|
@@ -691,27 +1104,27 @@ function detectNextAppRouterRoutes(input) {
|
|
|
691
1104
|
}
|
|
692
1105
|
return out;
|
|
693
1106
|
}
|
|
694
|
-
function detectNextPagesApiRoutes(
|
|
695
|
-
if (!/(^|\/)pages\/api\//.test(
|
|
696
|
-
const routePath = pagesApiPathFor(
|
|
697
|
-
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)) {
|
|
698
1111
|
if (node.type !== "export_statement") continue;
|
|
699
|
-
const text = nodeText(node,
|
|
1112
|
+
const text = nodeText(node, input3.source);
|
|
700
1113
|
if (!/default/.test(text)) continue;
|
|
701
1114
|
const line = startLine(node);
|
|
702
1115
|
return [
|
|
703
1116
|
{
|
|
704
1117
|
id: makeNodeId({
|
|
705
|
-
repoId:
|
|
1118
|
+
repoId: input3.repoId,
|
|
706
1119
|
kind: "Route",
|
|
707
|
-
path:
|
|
1120
|
+
path: input3.relativePath,
|
|
708
1121
|
name: `ANY ${routePath}`,
|
|
709
1122
|
line
|
|
710
1123
|
}),
|
|
711
1124
|
kind: "Route",
|
|
712
|
-
repoId:
|
|
1125
|
+
repoId: input3.repoId,
|
|
713
1126
|
name: `ANY ${routePath}`,
|
|
714
|
-
path:
|
|
1127
|
+
path: input3.relativePath,
|
|
715
1128
|
lineStart: line,
|
|
716
1129
|
lineEnd: endLine(node),
|
|
717
1130
|
method: "ANY",
|
|
@@ -755,26 +1168,32 @@ function pagesApiPathFor(relativePath) {
|
|
|
755
1168
|
}
|
|
756
1169
|
|
|
757
1170
|
// ../ingestion/src/extractors/extract.ts
|
|
758
|
-
async function extractFile(
|
|
759
|
-
|
|
760
|
-
|
|
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 });
|
|
761
1180
|
const file = {
|
|
762
1181
|
id: fileId,
|
|
763
1182
|
kind: "File",
|
|
764
|
-
repoId:
|
|
765
|
-
name:
|
|
766
|
-
path:
|
|
1183
|
+
repoId: input3.repoId,
|
|
1184
|
+
name: basename2(input3.relativePath),
|
|
1185
|
+
path: input3.relativePath,
|
|
767
1186
|
lineStart: 1,
|
|
768
1187
|
lineEnd: Math.max(1, parsed.rootNode.endPosition.row + 1),
|
|
769
|
-
language:
|
|
770
|
-
sizeBytes: Buffer.byteLength(
|
|
771
|
-
contentHash:
|
|
1188
|
+
language: input3.language,
|
|
1189
|
+
sizeBytes: Buffer.byteLength(input3.source, "utf8"),
|
|
1190
|
+
contentHash: sha12(input3.source)
|
|
772
1191
|
};
|
|
773
1192
|
const nodes = [];
|
|
774
1193
|
const edges = [];
|
|
775
1194
|
const localSymbols = /* @__PURE__ */ new Map();
|
|
776
1195
|
for (const node of walk(parsed.rootNode)) {
|
|
777
|
-
const symbol = extractSymbol(node,
|
|
1196
|
+
const symbol = extractSymbol(node, input3, parsed.source);
|
|
778
1197
|
if (!symbol) continue;
|
|
779
1198
|
nodes.push(symbol.node);
|
|
780
1199
|
localSymbols.set(symbol.node.name, symbol.node.id);
|
|
@@ -802,7 +1221,7 @@ async function extractFile(input) {
|
|
|
802
1221
|
fromId: fileId,
|
|
803
1222
|
toId: "",
|
|
804
1223
|
line: startLine(node),
|
|
805
|
-
fromPath:
|
|
1224
|
+
fromPath: input3.relativePath,
|
|
806
1225
|
toPath: target,
|
|
807
1226
|
unresolvedTargetName: target
|
|
808
1227
|
});
|
|
@@ -812,9 +1231,9 @@ async function extractFile(input) {
|
|
|
812
1231
|
if (node.type !== "call_expression") continue;
|
|
813
1232
|
const callee = node.childForFieldName("function");
|
|
814
1233
|
if (!callee) continue;
|
|
815
|
-
const calleeName =
|
|
1234
|
+
const calleeName = extractCalleeName2(callee, parsed.source);
|
|
816
1235
|
if (!calleeName) continue;
|
|
817
|
-
const enclosing = findEnclosingSymbolId(node,
|
|
1236
|
+
const enclosing = findEnclosingSymbolId(node, input3, parsed.source, localSymbols);
|
|
818
1237
|
if (!enclosing) continue;
|
|
819
1238
|
edges.push({
|
|
820
1239
|
kind: "CALLS",
|
|
@@ -824,14 +1243,14 @@ async function extractFile(input) {
|
|
|
824
1243
|
unresolvedTargetName: calleeName
|
|
825
1244
|
});
|
|
826
1245
|
}
|
|
827
|
-
if (
|
|
1246
|
+
if (input3.language === "tsx" || input3.language === "jsx") {
|
|
828
1247
|
for (const node of walk(parsed.rootNode)) {
|
|
829
1248
|
if (node.type !== "jsx_opening_element" && node.type !== "jsx_self_closing_element") continue;
|
|
830
1249
|
const ident = node.childForFieldName("name") ?? findChildByType(node, "identifier");
|
|
831
1250
|
if (!ident) continue;
|
|
832
1251
|
const tag = nodeText(ident, parsed.source);
|
|
833
1252
|
if (!isPascalCase(tag)) continue;
|
|
834
|
-
const enclosing = findEnclosingSymbolId(node,
|
|
1253
|
+
const enclosing = findEnclosingSymbolId(node, input3, parsed.source, localSymbols);
|
|
835
1254
|
if (!enclosing) continue;
|
|
836
1255
|
edges.push({
|
|
837
1256
|
kind: "RENDERS",
|
|
@@ -843,13 +1262,13 @@ async function extractFile(input) {
|
|
|
843
1262
|
}
|
|
844
1263
|
}
|
|
845
1264
|
const routes = detectRoutes({
|
|
846
|
-
repoId:
|
|
847
|
-
relativePath:
|
|
848
|
-
absolutePath:
|
|
1265
|
+
repoId: input3.repoId,
|
|
1266
|
+
relativePath: input3.relativePath,
|
|
1267
|
+
absolutePath: input3.absolutePath,
|
|
849
1268
|
fileId,
|
|
850
1269
|
rootNode: parsed.rootNode,
|
|
851
1270
|
source: parsed.source,
|
|
852
|
-
language:
|
|
1271
|
+
language: input3.language
|
|
853
1272
|
});
|
|
854
1273
|
for (const route of routes) {
|
|
855
1274
|
nodes.push(route);
|
|
@@ -857,35 +1276,35 @@ async function extractFile(input) {
|
|
|
857
1276
|
}
|
|
858
1277
|
return { file, nodes, edges };
|
|
859
1278
|
}
|
|
860
|
-
function extractSymbol(node,
|
|
1279
|
+
function extractSymbol(node, input3, source) {
|
|
861
1280
|
switch (node.type) {
|
|
862
1281
|
case "function_declaration":
|
|
863
1282
|
case "generator_function_declaration":
|
|
864
|
-
return functionFromDeclaration(node,
|
|
1283
|
+
return functionFromDeclaration(node, input3, source);
|
|
865
1284
|
case "class_declaration":
|
|
866
|
-
return classFromDeclaration(node,
|
|
1285
|
+
return classFromDeclaration(node, input3, source);
|
|
867
1286
|
case "interface_declaration":
|
|
868
|
-
return interfaceFromDeclaration(node,
|
|
1287
|
+
return interfaceFromDeclaration(node, input3, source, "Interface");
|
|
869
1288
|
case "type_alias_declaration":
|
|
870
|
-
return interfaceFromDeclaration(node,
|
|
1289
|
+
return interfaceFromDeclaration(node, input3, source, "Interface");
|
|
871
1290
|
case "lexical_declaration":
|
|
872
1291
|
case "variable_declaration":
|
|
873
|
-
return variableOrArrowFromDeclaration(node,
|
|
1292
|
+
return variableOrArrowFromDeclaration(node, input3, source);
|
|
874
1293
|
default:
|
|
875
1294
|
return null;
|
|
876
1295
|
}
|
|
877
1296
|
}
|
|
878
|
-
function functionFromDeclaration(node,
|
|
1297
|
+
function functionFromDeclaration(node, input3, source) {
|
|
879
1298
|
const nameNode = node.childForFieldName("name");
|
|
880
1299
|
if (!nameNode) return null;
|
|
881
1300
|
const name = nodeText(nameNode, source);
|
|
882
1301
|
const line = startLine(node);
|
|
883
|
-
const isComponent = (
|
|
1302
|
+
const isComponent = (input3.language === "tsx" || input3.language === "jsx") && isPascalCase(name) && containsJsx(node.childForFieldName("body"));
|
|
884
1303
|
const kind = isComponent ? "Component" : "Function";
|
|
885
1304
|
const id = makeNodeId({
|
|
886
|
-
repoId:
|
|
1305
|
+
repoId: input3.repoId,
|
|
887
1306
|
kind,
|
|
888
|
-
path:
|
|
1307
|
+
path: input3.relativePath,
|
|
889
1308
|
name,
|
|
890
1309
|
line
|
|
891
1310
|
});
|
|
@@ -893,9 +1312,9 @@ function functionFromDeclaration(node, input, source) {
|
|
|
893
1312
|
node: {
|
|
894
1313
|
id,
|
|
895
1314
|
kind,
|
|
896
|
-
repoId:
|
|
1315
|
+
repoId: input3.repoId,
|
|
897
1316
|
name,
|
|
898
|
-
path:
|
|
1317
|
+
path: input3.relativePath,
|
|
899
1318
|
lineStart: line,
|
|
900
1319
|
lineEnd: endLine(node),
|
|
901
1320
|
signature: extractSignature(node, source),
|
|
@@ -905,7 +1324,7 @@ function functionFromDeclaration(node, input, source) {
|
|
|
905
1324
|
}
|
|
906
1325
|
};
|
|
907
1326
|
}
|
|
908
|
-
function classFromDeclaration(node,
|
|
1327
|
+
function classFromDeclaration(node, input3, source) {
|
|
909
1328
|
const nameNode = node.childForFieldName("name");
|
|
910
1329
|
if (!nameNode) return null;
|
|
911
1330
|
const name = nodeText(nameNode, source);
|
|
@@ -913,9 +1332,9 @@ function classFromDeclaration(node, input, source) {
|
|
|
913
1332
|
const isComponent = looksLikeComponentClass(node, name, source);
|
|
914
1333
|
const kind = isComponent ? "Component" : "Class";
|
|
915
1334
|
const id = makeNodeId({
|
|
916
|
-
repoId:
|
|
1335
|
+
repoId: input3.repoId,
|
|
917
1336
|
kind,
|
|
918
|
-
path:
|
|
1337
|
+
path: input3.relativePath,
|
|
919
1338
|
name,
|
|
920
1339
|
line
|
|
921
1340
|
});
|
|
@@ -924,9 +1343,9 @@ function classFromDeclaration(node, input, source) {
|
|
|
924
1343
|
node: {
|
|
925
1344
|
id,
|
|
926
1345
|
kind,
|
|
927
|
-
repoId:
|
|
1346
|
+
repoId: input3.repoId,
|
|
928
1347
|
name,
|
|
929
|
-
path:
|
|
1348
|
+
path: input3.relativePath,
|
|
930
1349
|
lineStart: line,
|
|
931
1350
|
lineEnd: endLine(node),
|
|
932
1351
|
signature: `class ${name}${parentClass ? ` extends ${parentClass}` : ""}`,
|
|
@@ -936,15 +1355,15 @@ function classFromDeclaration(node, input, source) {
|
|
|
936
1355
|
...parentClass !== void 0 ? { parentClass } : {}
|
|
937
1356
|
};
|
|
938
1357
|
}
|
|
939
|
-
function interfaceFromDeclaration(node,
|
|
1358
|
+
function interfaceFromDeclaration(node, input3, source, kind) {
|
|
940
1359
|
const nameNode = node.childForFieldName("name");
|
|
941
1360
|
if (!nameNode) return null;
|
|
942
1361
|
const name = nodeText(nameNode, source);
|
|
943
1362
|
const line = startLine(node);
|
|
944
1363
|
const id = makeNodeId({
|
|
945
|
-
repoId:
|
|
1364
|
+
repoId: input3.repoId,
|
|
946
1365
|
kind,
|
|
947
|
-
path:
|
|
1366
|
+
path: input3.relativePath,
|
|
948
1367
|
name,
|
|
949
1368
|
line
|
|
950
1369
|
});
|
|
@@ -952,9 +1371,9 @@ function interfaceFromDeclaration(node, input, source, kind) {
|
|
|
952
1371
|
node: {
|
|
953
1372
|
id,
|
|
954
1373
|
kind,
|
|
955
|
-
repoId:
|
|
1374
|
+
repoId: input3.repoId,
|
|
956
1375
|
name,
|
|
957
|
-
path:
|
|
1376
|
+
path: input3.relativePath,
|
|
958
1377
|
lineStart: line,
|
|
959
1378
|
lineEnd: endLine(node),
|
|
960
1379
|
signature: nodeText(node, source).split("\n")[0]?.slice(0, 200) ?? "",
|
|
@@ -963,7 +1382,7 @@ function interfaceFromDeclaration(node, input, source, kind) {
|
|
|
963
1382
|
}
|
|
964
1383
|
};
|
|
965
1384
|
}
|
|
966
|
-
function variableOrArrowFromDeclaration(node,
|
|
1385
|
+
function variableOrArrowFromDeclaration(node, input3, source) {
|
|
967
1386
|
const declarators = findChildrenByType(node, "variable_declarator");
|
|
968
1387
|
if (declarators.length === 0) return null;
|
|
969
1388
|
const decl = declarators[0];
|
|
@@ -981,9 +1400,9 @@ function variableOrArrowFromDeclaration(node, input, source) {
|
|
|
981
1400
|
const isComponent = isPascalCase(name) && containsJsx(value);
|
|
982
1401
|
const kind = isComponent ? "Component" : "Function";
|
|
983
1402
|
const id2 = makeNodeId({
|
|
984
|
-
repoId:
|
|
1403
|
+
repoId: input3.repoId,
|
|
985
1404
|
kind,
|
|
986
|
-
path:
|
|
1405
|
+
path: input3.relativePath,
|
|
987
1406
|
name,
|
|
988
1407
|
line
|
|
989
1408
|
});
|
|
@@ -991,9 +1410,9 @@ function variableOrArrowFromDeclaration(node, input, source) {
|
|
|
991
1410
|
node: {
|
|
992
1411
|
id: id2,
|
|
993
1412
|
kind,
|
|
994
|
-
repoId:
|
|
1413
|
+
repoId: input3.repoId,
|
|
995
1414
|
name,
|
|
996
|
-
path:
|
|
1415
|
+
path: input3.relativePath,
|
|
997
1416
|
lineStart: line,
|
|
998
1417
|
lineEnd: endLine(decl),
|
|
999
1418
|
signature: extractSignature(value, source),
|
|
@@ -1004,9 +1423,9 @@ function variableOrArrowFromDeclaration(node, input, source) {
|
|
|
1004
1423
|
};
|
|
1005
1424
|
}
|
|
1006
1425
|
const id = makeNodeId({
|
|
1007
|
-
repoId:
|
|
1426
|
+
repoId: input3.repoId,
|
|
1008
1427
|
kind: "Variable",
|
|
1009
|
-
path:
|
|
1428
|
+
path: input3.relativePath,
|
|
1010
1429
|
name,
|
|
1011
1430
|
line
|
|
1012
1431
|
});
|
|
@@ -1014,9 +1433,9 @@ function variableOrArrowFromDeclaration(node, input, source) {
|
|
|
1014
1433
|
node: {
|
|
1015
1434
|
id,
|
|
1016
1435
|
kind: "Variable",
|
|
1017
|
-
repoId:
|
|
1436
|
+
repoId: input3.repoId,
|
|
1018
1437
|
name,
|
|
1019
|
-
path:
|
|
1438
|
+
path: input3.relativePath,
|
|
1020
1439
|
lineStart: line,
|
|
1021
1440
|
lineEnd: endLine(decl),
|
|
1022
1441
|
signature: nodeText(decl, source).split("\n")[0]?.slice(0, 200) ?? "",
|
|
@@ -1035,7 +1454,7 @@ function extractSignature(node, source) {
|
|
|
1035
1454
|
const rs = ret ? ` ${nodeText(ret, source)}` : "";
|
|
1036
1455
|
return `${head}${ps}${rs}`.trim().slice(0, 200);
|
|
1037
1456
|
}
|
|
1038
|
-
function
|
|
1457
|
+
function extractCalleeName2(callee, source) {
|
|
1039
1458
|
if (callee.type === "identifier") return nodeText(callee, source);
|
|
1040
1459
|
if (callee.type === "member_expression") {
|
|
1041
1460
|
const object = callee.childForFieldName("object");
|
|
@@ -1098,7 +1517,7 @@ function parentForLeadingComment(node) {
|
|
|
1098
1517
|
}
|
|
1099
1518
|
return cursor;
|
|
1100
1519
|
}
|
|
1101
|
-
function findEnclosingSymbolId(node,
|
|
1520
|
+
function findEnclosingSymbolId(node, input3, source, localSymbols) {
|
|
1102
1521
|
let cursor = node.parent;
|
|
1103
1522
|
while (cursor) {
|
|
1104
1523
|
if (cursor.type === "function_declaration" || cursor.type === "method_definition" || cursor.type === "class_declaration" || cursor.type === "arrow_function" || cursor.type === "function_expression") {
|
|
@@ -1110,7 +1529,7 @@ function findEnclosingSymbolId(node, input, source, localSymbols) {
|
|
|
1110
1529
|
}
|
|
1111
1530
|
cursor = cursor.parent;
|
|
1112
1531
|
}
|
|
1113
|
-
return makeFileId({ repoId:
|
|
1532
|
+
return makeFileId({ repoId: input3.repoId, path: input3.relativePath });
|
|
1114
1533
|
}
|
|
1115
1534
|
function enclosingDeclarationName(node, source) {
|
|
1116
1535
|
const nameField = node.childForFieldName("name");
|
|
@@ -1127,22 +1546,31 @@ function enclosingDeclarationName(node, source) {
|
|
|
1127
1546
|
}
|
|
1128
1547
|
return null;
|
|
1129
1548
|
}
|
|
1130
|
-
function
|
|
1131
|
-
return
|
|
1549
|
+
function sha12(s) {
|
|
1550
|
+
return createHash2("sha1").update(s).digest("hex");
|
|
1132
1551
|
}
|
|
1133
1552
|
|
|
1134
1553
|
// ../ingestion/src/extractors/resolve.ts
|
|
1135
1554
|
import path2 from "path";
|
|
1136
|
-
var EXTENSIONS = [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"];
|
|
1137
|
-
|
|
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) {
|
|
1138
1566
|
const byName = /* @__PURE__ */ new Map();
|
|
1139
|
-
for (const n of
|
|
1567
|
+
for (const n of input3.nodes) {
|
|
1140
1568
|
if (n.kind === "File") continue;
|
|
1141
1569
|
if (!byName.has(n.name)) byName.set(n.name, n.id);
|
|
1142
1570
|
}
|
|
1143
1571
|
const out = [];
|
|
1144
1572
|
let dropped = 0;
|
|
1145
|
-
for (const edge of
|
|
1573
|
+
for (const edge of input3.edges) {
|
|
1146
1574
|
if (edge.toId) {
|
|
1147
1575
|
out.push(edge);
|
|
1148
1576
|
continue;
|
|
@@ -1153,17 +1581,17 @@ function resolveEdges(input) {
|
|
|
1153
1581
|
}
|
|
1154
1582
|
if (edge.kind === "IMPORTS") {
|
|
1155
1583
|
const resolved = resolveImportPath({
|
|
1156
|
-
repoId:
|
|
1584
|
+
repoId: input3.repoId,
|
|
1157
1585
|
fromPath: edge.fromPath ?? "",
|
|
1158
1586
|
spec: edge.unresolvedTargetName,
|
|
1159
|
-
known:
|
|
1160
|
-
...
|
|
1587
|
+
known: input3.knownFilePaths,
|
|
1588
|
+
...input3.tsconfigPaths ? { tsconfigPaths: input3.tsconfigPaths } : {}
|
|
1161
1589
|
});
|
|
1162
1590
|
if (!resolved) {
|
|
1163
1591
|
dropped++;
|
|
1164
1592
|
continue;
|
|
1165
1593
|
}
|
|
1166
|
-
const targetId2 = makeFileId({ repoId:
|
|
1594
|
+
const targetId2 = makeFileId({ repoId: input3.repoId, path: resolved });
|
|
1167
1595
|
const { unresolvedTargetName: _unused2, ...rest2 } = edge;
|
|
1168
1596
|
out.push({ ...rest2, toId: targetId2, toPath: resolved });
|
|
1169
1597
|
continue;
|
|
@@ -1178,8 +1606,8 @@ function resolveEdges(input) {
|
|
|
1178
1606
|
}
|
|
1179
1607
|
return { resolved: out, dropped };
|
|
1180
1608
|
}
|
|
1181
|
-
function resolveImportPath(
|
|
1182
|
-
const { fromPath, spec, known, tsconfigPaths } =
|
|
1609
|
+
function resolveImportPath(input3) {
|
|
1610
|
+
const { fromPath, spec, known, tsconfigPaths } = input3;
|
|
1183
1611
|
if (spec.startsWith(".") || spec.startsWith("/")) {
|
|
1184
1612
|
const baseDir = path2.posix.dirname(toPosix(fromPath));
|
|
1185
1613
|
const joined = path2.posix.normalize(path2.posix.join(baseDir, toPosix(spec)));
|
|
@@ -1192,6 +1620,8 @@ function resolveImportPath(input) {
|
|
|
1192
1620
|
if (resolved) return resolved;
|
|
1193
1621
|
}
|
|
1194
1622
|
}
|
|
1623
|
+
const fromRoot = firstMatchingCandidate(toPosix(spec), known);
|
|
1624
|
+
if (fromRoot) return fromRoot;
|
|
1195
1625
|
return null;
|
|
1196
1626
|
}
|
|
1197
1627
|
function firstMatchingCandidate(joined, known) {
|
|
@@ -1199,7 +1629,7 @@ function firstMatchingCandidate(joined, known) {
|
|
|
1199
1629
|
const ext = path2.posix.extname(joined);
|
|
1200
1630
|
const stem = ext ? joined.slice(0, -ext.length) : joined;
|
|
1201
1631
|
for (const e of EXTENSIONS) candidates.push(`${stem}${e}`);
|
|
1202
|
-
for (const
|
|
1632
|
+
for (const indexFile of DIRECTORY_INDEX_FILES) candidates.push(`${stem}/${indexFile}`);
|
|
1203
1633
|
for (const c of candidates) {
|
|
1204
1634
|
if (known.has(c)) return c;
|
|
1205
1635
|
}
|
|
@@ -1238,7 +1668,7 @@ function toPosix(p) {
|
|
|
1238
1668
|
import { readFile as readFile5, stat as stat2 } from "fs/promises";
|
|
1239
1669
|
import { cpus } from "os";
|
|
1240
1670
|
import { join as join2 } from "path";
|
|
1241
|
-
import { createHash as
|
|
1671
|
+
import { createHash as createHash3 } from "crypto";
|
|
1242
1672
|
import ignore2 from "ignore";
|
|
1243
1673
|
|
|
1244
1674
|
// ../ingestion/src/embedder.ts
|
|
@@ -1252,8 +1682,8 @@ ${comment}`.trim();
|
|
|
1252
1682
|
async function embedNodes(nodes, opts) {
|
|
1253
1683
|
const batchSize = opts.batchSize ?? 100;
|
|
1254
1684
|
const candidates = nodes.filter((n) => n.kind !== "File");
|
|
1685
|
+
const totalSymbols = candidates.length;
|
|
1255
1686
|
const result = [];
|
|
1256
|
-
const total = Math.ceil(candidates.length / batchSize);
|
|
1257
1687
|
const namespace = `${opts.router.config.embeddingNamespace.provider}:${opts.router.config.embeddingNamespace.model}:${opts.router.config.embeddingNamespace.dimension}`;
|
|
1258
1688
|
for (let i = 0; i < candidates.length; i += batchSize) {
|
|
1259
1689
|
const slice = candidates.slice(i, i + batchSize);
|
|
@@ -1267,7 +1697,7 @@ async function embedNodes(nodes, opts) {
|
|
|
1267
1697
|
}
|
|
1268
1698
|
opts.onBatch?.({
|
|
1269
1699
|
batchIndex: i / batchSize,
|
|
1270
|
-
total,
|
|
1700
|
+
total: totalSymbols,
|
|
1271
1701
|
embedded: result.length
|
|
1272
1702
|
});
|
|
1273
1703
|
}
|
|
@@ -1305,14 +1735,14 @@ async function readIfExists(filePath) {
|
|
|
1305
1735
|
return null;
|
|
1306
1736
|
}
|
|
1307
1737
|
}
|
|
1308
|
-
function stripJsonComments(
|
|
1738
|
+
function stripJsonComments(input3) {
|
|
1309
1739
|
let out = "";
|
|
1310
1740
|
let i = 0;
|
|
1311
1741
|
let inString = false;
|
|
1312
1742
|
let stringChar = "";
|
|
1313
|
-
while (i <
|
|
1314
|
-
const c =
|
|
1315
|
-
const next =
|
|
1743
|
+
while (i < input3.length) {
|
|
1744
|
+
const c = input3[i];
|
|
1745
|
+
const next = input3[i + 1];
|
|
1316
1746
|
if (inString) {
|
|
1317
1747
|
out += c;
|
|
1318
1748
|
if (c === "\\" && next !== void 0) {
|
|
@@ -1332,12 +1762,12 @@ function stripJsonComments(input) {
|
|
|
1332
1762
|
continue;
|
|
1333
1763
|
}
|
|
1334
1764
|
if (c === "/" && next === "/") {
|
|
1335
|
-
while (i <
|
|
1765
|
+
while (i < input3.length && input3[i] !== "\n") i++;
|
|
1336
1766
|
continue;
|
|
1337
1767
|
}
|
|
1338
1768
|
if (c === "/" && next === "*") {
|
|
1339
1769
|
i += 2;
|
|
1340
|
-
while (i <
|
|
1770
|
+
while (i < input3.length && !(input3[i] === "*" && input3[i + 1] === "/")) i++;
|
|
1341
1771
|
i += 2;
|
|
1342
1772
|
continue;
|
|
1343
1773
|
}
|
|
@@ -1528,11 +1958,11 @@ async function runWithConcurrency(items, concurrency, fn) {
|
|
|
1528
1958
|
import kleur4 from "kleur";
|
|
1529
1959
|
|
|
1530
1960
|
// src/repo-id.ts
|
|
1531
|
-
import { createHash as
|
|
1961
|
+
import { createHash as createHash4 } from "crypto";
|
|
1532
1962
|
import path5 from "path";
|
|
1533
1963
|
function repoIdFromPath(absPath) {
|
|
1534
1964
|
const base = path5.basename(path5.resolve(absPath));
|
|
1535
|
-
const sha =
|
|
1965
|
+
const sha = createHash4("sha1").update(path5.resolve(absPath)).digest("hex").slice(0, 8);
|
|
1536
1966
|
const safe = base.replace(/[^A-Za-z0-9_-]/g, "_");
|
|
1537
1967
|
return `${safe}-${sha}`;
|
|
1538
1968
|
}
|
|
@@ -1692,18 +2122,21 @@ async function runWipeCommand(opts) {
|
|
|
1692
2122
|
}
|
|
1693
2123
|
|
|
1694
2124
|
// src/commands/init.ts
|
|
2125
|
+
import { spawn } from "child_process";
|
|
1695
2126
|
import path7 from "path";
|
|
1696
|
-
import {
|
|
2127
|
+
import { fileURLToPath } from "url";
|
|
2128
|
+
import { confirm, input as input2, password as password2, select as select2 } from "@inquirer/prompts";
|
|
1697
2129
|
|
|
1698
2130
|
// ../mcp-server/dist/index.js
|
|
1699
|
-
import { createHash as
|
|
2131
|
+
import { createHash as createHash5 } from "crypto";
|
|
1700
2132
|
import { mkdir as mkdir3, readFile as readFile6, writeFile as writeFile2 } from "fs/promises";
|
|
1701
2133
|
import { homedir } from "os";
|
|
1702
2134
|
import { dirname as dirname3, resolve } from "path";
|
|
1703
2135
|
import { randomUUID } from "crypto";
|
|
2136
|
+
import { join as join3 } from "path";
|
|
1704
2137
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
1705
2138
|
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
|
|
1706
|
-
import express from "express";
|
|
2139
|
+
import express, { Router } from "express";
|
|
1707
2140
|
import { z } from "zod";
|
|
1708
2141
|
import { z as z2 } from "zod";
|
|
1709
2142
|
import { z as z3 } from "zod";
|
|
@@ -1716,7 +2149,7 @@ import { z as z9 } from "zod";
|
|
|
1716
2149
|
import { z as z10 } from "zod";
|
|
1717
2150
|
function buildCacheKey(toolName, args, namespace = "v1") {
|
|
1718
2151
|
const serialized = stableStringify(args);
|
|
1719
|
-
const hash =
|
|
2152
|
+
const hash = createHash5("sha1").update(serialized).digest("hex");
|
|
1720
2153
|
return `codegraph:${namespace}:tool:${toolName}:${hash}`;
|
|
1721
2154
|
}
|
|
1722
2155
|
function stableStringify(value) {
|
|
@@ -2079,11 +2512,11 @@ function findForbiddenKeyword(upper) {
|
|
|
2079
2512
|
}
|
|
2080
2513
|
return null;
|
|
2081
2514
|
}
|
|
2082
|
-
function validateReadOnlyCypher(
|
|
2083
|
-
if (typeof
|
|
2515
|
+
function validateReadOnlyCypher(input3) {
|
|
2516
|
+
if (typeof input3 !== "string" || input3.trim().length === 0) {
|
|
2084
2517
|
return { ok: false, reason: "Empty Cypher statement." };
|
|
2085
2518
|
}
|
|
2086
|
-
const withoutComments = stripComments(
|
|
2519
|
+
const withoutComments = stripComments(input3);
|
|
2087
2520
|
const sanitized = stripStringLiterals(withoutComments).trim();
|
|
2088
2521
|
const withoutTrailingSemicolon = sanitized.replace(/;\s*$/, "");
|
|
2089
2522
|
if (withoutTrailingSemicolon.includes(";")) {
|
|
@@ -2313,6 +2746,63 @@ function registerAllTools(server, deps) {
|
|
|
2313
2746
|
});
|
|
2314
2747
|
}
|
|
2315
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
|
+
}
|
|
2316
2806
|
function createMcpServer(opts) {
|
|
2317
2807
|
const info = opts.serverInfo ?? { name: "codegraph", version: "0.0.0" };
|
|
2318
2808
|
const server = new McpServer(info, {
|
|
@@ -2329,6 +2819,23 @@ async function startSseServer(opts) {
|
|
|
2329
2819
|
app.get("/healthz", (_req, res) => {
|
|
2330
2820
|
res.status(200).json({ ok: true });
|
|
2331
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
|
+
}
|
|
2332
2839
|
app.use(bearerAuthMiddleware(config.bearerToken, logger));
|
|
2333
2840
|
const transports = /* @__PURE__ */ new Map();
|
|
2334
2841
|
app.get("/mcp", async (_req, res) => {
|
|
@@ -2529,12 +3036,12 @@ async function startMcpServer(portOrOptions) {
|
|
|
2529
3036
|
cacheTtlSeconds: config.cacheTtlSeconds,
|
|
2530
3037
|
logger
|
|
2531
3038
|
};
|
|
2532
|
-
return startSseServer({ deps, config });
|
|
3039
|
+
return startSseServer({ deps, config, viewerDir: options.viewerDir });
|
|
2533
3040
|
}
|
|
2534
3041
|
async function loadGraphClient(dbPath) {
|
|
2535
3042
|
let mod;
|
|
2536
3043
|
try {
|
|
2537
|
-
mod = await import("./src-
|
|
3044
|
+
mod = await import("./src-IKWDKNPH.js");
|
|
2538
3045
|
} catch (err) {
|
|
2539
3046
|
throw new Error(
|
|
2540
3047
|
`Failed to import @codegraph/graph-db. Run \`pnpm -r build\` first. Underlying error: ${err instanceof Error ? err.message : String(err)}`
|
|
@@ -2560,7 +3067,7 @@ async function loadGraphClient(dbPath) {
|
|
|
2560
3067
|
async function loadLlmRouter(configPath2) {
|
|
2561
3068
|
let mod;
|
|
2562
3069
|
try {
|
|
2563
|
-
mod = await import("./src-
|
|
3070
|
+
mod = await import("./src-HB4UDUBX.js");
|
|
2564
3071
|
} catch (err) {
|
|
2565
3072
|
throw new Error(
|
|
2566
3073
|
`Failed to import @codegraph/llm-router. Run \`pnpm -r build\` first. Underlying error: ${err instanceof Error ? err.message : String(err)}`
|
|
@@ -2659,62 +3166,202 @@ async function runLlmSetup() {
|
|
|
2659
3166
|
message: "How would you like to power CodeGraph?",
|
|
2660
3167
|
choices: [
|
|
2661
3168
|
{
|
|
2662
|
-
name: "
|
|
2663
|
-
value: "
|
|
2664
|
-
description: "
|
|
3169
|
+
name: "Local, zero cost",
|
|
3170
|
+
value: "local",
|
|
3171
|
+
description: "Runs entirely on your laptop. No API keys. Uses Ollama (guided install)."
|
|
2665
3172
|
},
|
|
2666
3173
|
{
|
|
2667
|
-
name: "
|
|
2668
|
-
value: "
|
|
2669
|
-
description: "
|
|
3174
|
+
name: "Cloud provider",
|
|
3175
|
+
value: "cloud",
|
|
3176
|
+
description: "OpenAI, Anthropic, or Google. Better quality, requires an API key."
|
|
2670
3177
|
}
|
|
2671
3178
|
]
|
|
2672
3179
|
});
|
|
2673
|
-
let preset;
|
|
2674
3180
|
if (mode === "local") {
|
|
2675
|
-
|
|
3181
|
+
await setupLocalOllama();
|
|
2676
3182
|
} else {
|
|
2677
|
-
|
|
2678
|
-
|
|
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?",
|
|
2679
3198
|
choices: [
|
|
2680
|
-
{
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
},
|
|
2685
|
-
{
|
|
2686
|
-
name: "Anthropic",
|
|
2687
|
-
value: "byo-anthropic",
|
|
2688
|
-
description: "claude-3-5-haiku (gen) + OpenAI text-embedding-3-small (embed)"
|
|
2689
|
-
},
|
|
2690
|
-
{
|
|
2691
|
-
name: "Google",
|
|
2692
|
-
value: "byo-google",
|
|
2693
|
-
description: "gemini-1.5-flash + text-embedding-004"
|
|
2694
|
-
}
|
|
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" }
|
|
2695
3203
|
]
|
|
2696
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;
|
|
2697
3219
|
}
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
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`);
|
|
2701
3266
|
}
|
|
2702
3267
|
const config = await loadConfig();
|
|
3268
|
+
const lookup = LLM_PRESETS["local-ollama"];
|
|
2703
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;
|
|
2704
3357
|
await saveConfig(config);
|
|
2705
3358
|
console.log();
|
|
2706
3359
|
console.log(`${kleur5.green("\u2713")} preset saved ${kleur5.dim(`"${preset}" \u2192 ${configPath()}`)}`);
|
|
2707
|
-
console.log(`${kleur5.dim(" namespace
|
|
3360
|
+
console.log(`${kleur5.dim(" namespace ")}${namespaceLabel(config.llm)}`);
|
|
2708
3361
|
if (preset === "byo-anthropic") {
|
|
2709
3362
|
console.log();
|
|
2710
|
-
console.log(
|
|
2711
|
-
|
|
2712
|
-
"Note: Anthropic has no embedding API, so codegraph uses OpenAI embeddings."
|
|
2713
|
-
)
|
|
2714
|
-
);
|
|
2715
|
-
console.log(
|
|
2716
|
-
kleur5.yellow("You'll need both ANTHROPIC_API_KEY and OPENAI_API_KEY set.")
|
|
2717
|
-
);
|
|
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."));
|
|
2718
3365
|
await promptApiKey("ANTHROPIC_API_KEY");
|
|
2719
3366
|
await promptApiKey("OPENAI_API_KEY");
|
|
2720
3367
|
return;
|
|
@@ -2722,6 +3369,11 @@ async function runLlmSetup() {
|
|
|
2722
3369
|
const envVar = apiKeyEnvVarFor2(preset);
|
|
2723
3370
|
if (envVar) await promptApiKey(envVar);
|
|
2724
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
|
+
}
|
|
2725
3377
|
function apiKeyEnvVarFor2(preset) {
|
|
2726
3378
|
if (preset === "byo-openai" || preset === "managed-stub") return "OPENAI_API_KEY";
|
|
2727
3379
|
if (preset === "byo-anthropic") return "ANTHROPIC_API_KEY";
|
|
@@ -2813,8 +3465,9 @@ async function runIndexStep() {
|
|
|
2813
3465
|
return;
|
|
2814
3466
|
}
|
|
2815
3467
|
console.log();
|
|
3468
|
+
const binPath = resolveBinPath();
|
|
2816
3469
|
try {
|
|
2817
|
-
await
|
|
3470
|
+
await spawnIndex(binPath, cwd);
|
|
2818
3471
|
} catch (err) {
|
|
2819
3472
|
console.log();
|
|
2820
3473
|
console.log(
|
|
@@ -2827,6 +3480,30 @@ async function runIndexStep() {
|
|
|
2827
3480
|
);
|
|
2828
3481
|
}
|
|
2829
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
|
+
}
|
|
2830
3507
|
async function runServeStep() {
|
|
2831
3508
|
stepHeader(5, "Boot the MCP server");
|
|
2832
3509
|
const { config: serverConfig, created } = await resolveServerConfig({});
|
|
@@ -3004,6 +3681,10 @@ async function runCelebration() {
|
|
|
3004
3681
|
` ${kleur5.dim("\u2026plus 5 more (search_symbol, find_file, get_file_context,")}`,
|
|
3005
3682
|
` ${kleur5.dim(" get_dependencies, affected_by)")}`,
|
|
3006
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
|
+
"",
|
|
3007
3688
|
kleur5.dim("Helpful next steps:"),
|
|
3008
3689
|
` ${kleur5.cyan("codegraph doctor")} ${kleur5.dim("\u2014 verify environment + LLM + Kuzu")}`,
|
|
3009
3690
|
` ${kleur5.cyan("codegraph status")} ${kleur5.dim("<path>")} ${kleur5.dim("\u2014 node/edge counts + embedding coverage")}`,
|
|
@@ -3029,7 +3710,23 @@ function abbreviateHome(p) {
|
|
|
3029
3710
|
}
|
|
3030
3711
|
|
|
3031
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";
|
|
3032
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
|
+
}
|
|
3033
3730
|
function providerEnvVar2(provider) {
|
|
3034
3731
|
if (provider === "openai") return "OPENAI_API_KEY";
|
|
3035
3732
|
if (provider === "anthropic") return "ANTHROPIC_API_KEY";
|
|
@@ -3065,16 +3762,19 @@ async function runServeCommand(opts = {}) {
|
|
|
3065
3762
|
started = await startMcpServer({
|
|
3066
3763
|
...opts.port !== void 0 ? { port: opts.port } : {},
|
|
3067
3764
|
...opts.host !== void 0 ? { host: opts.host } : {},
|
|
3068
|
-
...opts.dbPath !== void 0 ? { dbPath: opts.dbPath } : {}
|
|
3765
|
+
...opts.dbPath !== void 0 ? { dbPath: opts.dbPath } : {},
|
|
3766
|
+
viewerDir: resolveViewerDir()
|
|
3069
3767
|
});
|
|
3070
3768
|
spinner.stop();
|
|
3071
3769
|
} catch (err) {
|
|
3072
3770
|
spinner.fail("Server failed to start");
|
|
3073
3771
|
throw err;
|
|
3074
3772
|
}
|
|
3075
|
-
const
|
|
3773
|
+
const base = `http://${started.address.host}:${started.address.port}`;
|
|
3774
|
+
const url = `${base}/mcp`;
|
|
3775
|
+
const viewerUrl = `${base}/viewer`;
|
|
3076
3776
|
const tokenHint = "bearer token at ~/.codegraph/config.json (codegraph config show to view)";
|
|
3077
|
-
process.stdout.write(`${renderServeBanner(url, tokenHint)}
|
|
3777
|
+
process.stdout.write(`${renderServeBanner(url, tokenHint, viewerUrl)}
|
|
3078
3778
|
`);
|
|
3079
3779
|
const shutdown = async (signal) => {
|
|
3080
3780
|
process.stderr.write(`
|
|
@@ -3096,6 +3796,57 @@ shutting down (${signal})...
|
|
|
3096
3796
|
});
|
|
3097
3797
|
}
|
|
3098
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
|
+
|
|
3099
3850
|
// src/program.ts
|
|
3100
3851
|
function buildProgram() {
|
|
3101
3852
|
const program = new Command();
|
|
@@ -3122,6 +3873,12 @@ function buildProgram() {
|
|
|
3122
3873
|
...opts.dbPath !== void 0 ? { dbPath: opts.dbPath } : {}
|
|
3123
3874
|
});
|
|
3124
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
|
+
});
|
|
3125
3882
|
program.command("doctor").description("Check environment, config, LLM credentials, and Kuzu DB health").action(async () => {
|
|
3126
3883
|
await runDoctorCommand();
|
|
3127
3884
|
});
|
|
@@ -3154,4 +3911,4 @@ export {
|
|
|
3154
3911
|
renderError,
|
|
3155
3912
|
buildProgram
|
|
3156
3913
|
};
|
|
3157
|
-
//# sourceMappingURL=chunk-
|
|
3914
|
+
//# sourceMappingURL=chunk-KYPDPBI5.js.map
|