@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.
@@ -1,17 +1,18 @@
1
1
  import {
2
2
  createLlmRouter,
3
3
  namespaceLabel
4
- } from "./chunk-B2TIVKUB.js";
4
+ } from "./chunk-GOJIV25M.js";
5
5
  import {
6
6
  GraphDb,
7
7
  defaultDbPath
8
- } from "./chunk-AVP24SX5.js";
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-XGPZDCQ4.js";
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 body = [
191
- `${kleur.green("\u2713")} codegraph mcp listening on ${kleur.cyan(url)}`,
192
- "",
193
- kleur.dim(tokenHint),
194
- kleur.dim("Ctrl-C to stop.")
195
- ].join("\n");
196
- return boxen(body, {
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(input) {
1028
+ function detectRoutes(input3) {
616
1029
  return [
617
- ...detectExpressRoutes(input),
618
- ...detectNextAppRouterRoutes(input),
619
- ...detectNextPagesApiRoutes(input)
1030
+ ...detectExpressRoutes(input3),
1031
+ ...detectNextAppRouterRoutes(input3),
1032
+ ...detectNextPagesApiRoutes(input3)
620
1033
  ];
621
1034
  }
622
- function detectExpressRoutes(input) {
1035
+ function detectExpressRoutes(input3) {
623
1036
  const out = [];
624
- for (const node of walk(input.rootNode)) {
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, input.source).toLowerCase();
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, input.source));
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: input.repoId,
1056
+ repoId: input3.repoId,
644
1057
  kind: "Route",
645
- path: input.relativePath,
1058
+ path: input3.relativePath,
646
1059
  name,
647
1060
  line
648
1061
  }),
649
1062
  kind: "Route",
650
- repoId: input.repoId,
1063
+ repoId: input3.repoId,
651
1064
  name,
652
- path: input.relativePath,
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(input) {
663
- if (!/(^|\/)app\/.+\/route\.(ts|tsx|js|jsx|mjs|cjs)$/.test(input.relativePath)) return [];
664
- const routePath = appRoutePathFor(input.relativePath);
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(input.rootNode)) {
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, input.source);
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: input.repoId,
1088
+ repoId: input3.repoId,
676
1089
  kind: "Route",
677
- path: input.relativePath,
1090
+ path: input3.relativePath,
678
1091
  name: `${name} ${routePath}`,
679
1092
  line
680
1093
  }),
681
1094
  kind: "Route",
682
- repoId: input.repoId,
1095
+ repoId: input3.repoId,
683
1096
  name: `${name} ${routePath}`,
684
- path: input.relativePath,
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(input) {
695
- if (!/(^|\/)pages\/api\//.test(input.relativePath)) return [];
696
- const routePath = pagesApiPathFor(input.relativePath);
697
- for (const node of walk(input.rootNode)) {
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, input.source);
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: input.repoId,
1118
+ repoId: input3.repoId,
706
1119
  kind: "Route",
707
- path: input.relativePath,
1120
+ path: input3.relativePath,
708
1121
  name: `ANY ${routePath}`,
709
1122
  line
710
1123
  }),
711
1124
  kind: "Route",
712
- repoId: input.repoId,
1125
+ repoId: input3.repoId,
713
1126
  name: `ANY ${routePath}`,
714
- path: input.relativePath,
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(input) {
759
- const parsed = await parseSource(input.source, input.language);
760
- const fileId = makeFileId({ repoId: input.repoId, path: input.relativePath });
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: input.repoId,
765
- name: basename(input.relativePath),
766
- path: input.relativePath,
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: input.language,
770
- sizeBytes: Buffer.byteLength(input.source, "utf8"),
771
- contentHash: sha1(input.source)
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, input, parsed.source);
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: input.relativePath,
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 = extractCalleeName(callee, parsed.source);
1234
+ const calleeName = extractCalleeName2(callee, parsed.source);
816
1235
  if (!calleeName) continue;
817
- const enclosing = findEnclosingSymbolId(node, input, parsed.source, localSymbols);
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 (input.language === "tsx" || input.language === "jsx") {
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, input, parsed.source, localSymbols);
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: input.repoId,
847
- relativePath: input.relativePath,
848
- absolutePath: input.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: input.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, input, source) {
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, input, source);
1283
+ return functionFromDeclaration(node, input3, source);
865
1284
  case "class_declaration":
866
- return classFromDeclaration(node, input, source);
1285
+ return classFromDeclaration(node, input3, source);
867
1286
  case "interface_declaration":
868
- return interfaceFromDeclaration(node, input, source, "Interface");
1287
+ return interfaceFromDeclaration(node, input3, source, "Interface");
869
1288
  case "type_alias_declaration":
870
- return interfaceFromDeclaration(node, input, source, "Interface");
1289
+ return interfaceFromDeclaration(node, input3, source, "Interface");
871
1290
  case "lexical_declaration":
872
1291
  case "variable_declaration":
873
- return variableOrArrowFromDeclaration(node, input, source);
1292
+ return variableOrArrowFromDeclaration(node, input3, source);
874
1293
  default:
875
1294
  return null;
876
1295
  }
877
1296
  }
878
- function functionFromDeclaration(node, input, source) {
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 = (input.language === "tsx" || input.language === "jsx") && isPascalCase(name) && containsJsx(node.childForFieldName("body"));
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: input.repoId,
1305
+ repoId: input3.repoId,
887
1306
  kind,
888
- path: input.relativePath,
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: input.repoId,
1315
+ repoId: input3.repoId,
897
1316
  name,
898
- path: input.relativePath,
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, input, source) {
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: input.repoId,
1335
+ repoId: input3.repoId,
917
1336
  kind,
918
- path: input.relativePath,
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: input.repoId,
1346
+ repoId: input3.repoId,
928
1347
  name,
929
- path: input.relativePath,
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, input, source, kind) {
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: input.repoId,
1364
+ repoId: input3.repoId,
946
1365
  kind,
947
- path: input.relativePath,
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: input.repoId,
1374
+ repoId: input3.repoId,
956
1375
  name,
957
- path: input.relativePath,
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, input, source) {
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: input.repoId,
1403
+ repoId: input3.repoId,
985
1404
  kind,
986
- path: input.relativePath,
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: input.repoId,
1413
+ repoId: input3.repoId,
995
1414
  name,
996
- path: input.relativePath,
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: input.repoId,
1426
+ repoId: input3.repoId,
1008
1427
  kind: "Variable",
1009
- path: input.relativePath,
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: input.repoId,
1436
+ repoId: input3.repoId,
1018
1437
  name,
1019
- path: input.relativePath,
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 extractCalleeName(callee, source) {
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, input, source, localSymbols) {
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: input.repoId, path: input.relativePath });
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 sha1(s) {
1131
- return createHash("sha1").update(s).digest("hex");
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
- function resolveEdges(input) {
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 input.nodes) {
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 input.edges) {
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: input.repoId,
1584
+ repoId: input3.repoId,
1157
1585
  fromPath: edge.fromPath ?? "",
1158
1586
  spec: edge.unresolvedTargetName,
1159
- known: input.knownFilePaths,
1160
- ...input.tsconfigPaths ? { tsconfigPaths: input.tsconfigPaths } : {}
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: input.repoId, path: resolved });
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(input) {
1182
- const { fromPath, spec, known, tsconfigPaths } = input;
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 e of EXTENSIONS) candidates.push(`${stem}/index${e}`);
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 createHash2 } from "crypto";
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(input) {
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 < input.length) {
1314
- const c = input[i];
1315
- const next = input[i + 1];
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 < input.length && input[i] !== "\n") 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 < input.length && !(input[i] === "*" && input[i + 1] === "/")) 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 createHash3 } from "crypto";
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 = createHash3("sha1").update(path5.resolve(absPath)).digest("hex").slice(0, 8);
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 { confirm, password as password2, select as select2 } from "@inquirer/prompts";
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 createHash4 } from "crypto";
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 = createHash4("sha1").update(serialized).digest("hex");
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(input) {
2083
- if (typeof input !== "string" || input.trim().length === 0) {
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(input);
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-M7HSEMBT.js");
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-UVET6JHH.js");
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: "Cloud provider",
2663
- value: "cloud",
2664
- description: "OpenAI, Anthropic, or Google. Fastest setup, requires an API key."
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: "Local (Ollama)",
2668
- value: "local",
2669
- description: "Fully private. Requires Ollama running on :11434."
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
- preset = "local-ollama";
3181
+ await setupLocalOllama();
2676
3182
  } else {
2677
- preset = await select2({
2678
- message: "Which cloud provider?",
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
- name: "OpenAI",
2682
- value: "byo-openai",
2683
- description: "gpt-4o-mini + text-embedding-3-small"
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
- const lookup = LLM_PRESETS[preset];
2699
- if (!lookup) {
2700
- throw new Error(`Unknown preset "${preset}".`);
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 ")}${kleur5.dim(namespaceLabel(config.llm))}`);
3360
+ console.log(`${kleur5.dim(" namespace ")}${namespaceLabel(config.llm)}`);
2708
3361
  if (preset === "byo-anthropic") {
2709
3362
  console.log();
2710
- console.log(
2711
- kleur5.yellow(
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 runIndexCommand({ repoPath: cwd });
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 url = `http://${started.address.host}:${started.address.port}/mcp`;
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-5WYXRWEY.js.map
3914
+ //# sourceMappingURL=chunk-KYPDPBI5.js.map