@leanlabsinnov/codegraph 0.1.2 → 0.1.4

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