@leanlabsinnov/codegraph 0.1.1 → 0.1.3

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.
@@ -5,7 +5,7 @@ import {
5
5
  import {
6
6
  GraphDb,
7
7
  defaultDbPath
8
- } from "./chunk-VFE242Y7.js";
8
+ } from "./chunk-AVP24SX5.js";
9
9
  import {
10
10
  DEFAULT_CONFIG,
11
11
  LLM_PRESETS,
@@ -374,20 +374,20 @@ function checkNodeVersion() {
374
374
  detail: `v${version} (codegraph requires >= 20)`
375
375
  };
376
376
  }
377
- async function checkWritable(name, path6) {
377
+ async function checkWritable(name, path8) {
378
378
  try {
379
- await mkdir2(dirname(path6), { recursive: true });
379
+ await mkdir2(dirname(path8), { recursive: true });
380
380
  try {
381
- await access(path6, constants.W_OK);
381
+ await access(path8, constants.W_OK);
382
382
  } catch {
383
- await access(dirname(path6), constants.W_OK);
383
+ await access(dirname(path8), constants.W_OK);
384
384
  }
385
- return { name, status: "ok", detail: path6 };
385
+ return { name, status: "ok", detail: path8 };
386
386
  } catch (err) {
387
387
  return {
388
388
  name,
389
389
  status: "fail",
390
- detail: `${path6} (${err instanceof Error ? err.message : String(err)})`
390
+ detail: `${path8} (${err instanceof Error ? err.message : String(err)})`
391
391
  };
392
392
  }
393
393
  }
@@ -433,17 +433,19 @@ async function selfTestKuzu(dbPath, embeddingDimension) {
433
433
  try {
434
434
  await db.connect();
435
435
  await db.migrate();
436
- const result = await db.query("RETURN 1 AS result");
437
- const vectorReady = db.hasVectorIndex();
436
+ const result = await db.query(
437
+ `RETURN array_cosine_similarity([1.0, 0.0], [1.0, 0.0]) AS similarity, true AS ok`
438
+ );
438
439
  await db.close();
439
- if (result.data[0]?.result === 1) {
440
- return {
441
- name: "kuzu round-trip",
442
- status: vectorReady ? "ok" : "warn",
443
- detail: vectorReady ? "ok (vector index ready)" : "ok (vector extension missing; semantic search disabled)"
444
- };
440
+ const row = result.data[0];
441
+ if (row?.ok !== true || typeof row.similarity !== "number") {
442
+ return { name: "kuzu round-trip", status: "fail", detail: "unexpected result" };
445
443
  }
446
- return { name: "kuzu round-trip", status: "fail", detail: "unexpected result" };
444
+ return {
445
+ name: "kuzu round-trip",
446
+ status: "ok",
447
+ detail: "ok (brute-force semantic search ready)"
448
+ };
447
449
  } catch (err) {
448
450
  await db.close().catch(() => {
449
451
  });
@@ -456,7 +458,8 @@ async function selfTestKuzu(dbPath, embeddingDimension) {
456
458
  }
457
459
 
458
460
  // src/commands/index.ts
459
- import path5 from "path";
461
+ import { rm } from "fs/promises";
462
+ import path6 from "path";
460
463
 
461
464
  // ../ingestion/src/parser.ts
462
465
  import { readFile as readFile2 } from "fs/promises";
@@ -877,9 +880,11 @@ function functionFromDeclaration(node, input, source) {
877
880
  if (!nameNode) return null;
878
881
  const name = nodeText(nameNode, source);
879
882
  const line = startLine(node);
883
+ const isComponent = (input.language === "tsx" || input.language === "jsx") && isPascalCase(name) && containsJsx(node.childForFieldName("body"));
884
+ const kind = isComponent ? "Component" : "Function";
880
885
  const id = makeNodeId({
881
886
  repoId: input.repoId,
882
- kind: "Function",
887
+ kind,
883
888
  path: input.relativePath,
884
889
  name,
885
890
  line
@@ -887,7 +892,7 @@ function functionFromDeclaration(node, input, source) {
887
892
  return {
888
893
  node: {
889
894
  id,
890
- kind: "Function",
895
+ kind,
891
896
  repoId: input.repoId,
892
897
  name,
893
898
  path: input.relativePath,
@@ -896,7 +901,7 @@ function functionFromDeclaration(node, input, source) {
896
901
  signature: extractSignature(node, source),
897
902
  leadingComment: leadingCommentFor(parentForLeadingComment(node), source),
898
903
  isExported: isExported(node),
899
- isAsync: hasAsyncModifier(node, source)
904
+ ...kind === "Function" ? { isAsync: hasAsyncModifier(node, source) } : {}
900
905
  }
901
906
  };
902
907
  }
@@ -966,6 +971,9 @@ function variableOrArrowFromDeclaration(node, input, source) {
966
971
  const nameNode = decl.childForFieldName("name");
967
972
  const value = decl.childForFieldName("value");
968
973
  if (!nameNode) return null;
974
+ if (nameNode.type !== "identifier" && nameNode.type !== "property_identifier") {
975
+ return null;
976
+ }
969
977
  const name = nodeText(nameNode, source);
970
978
  const line = startLine(decl);
971
979
  const isArrow = value?.type === "arrow_function" || value?.type === "function_expression";
@@ -1030,6 +1038,10 @@ function extractSignature(node, source) {
1030
1038
  function extractCalleeName(callee, source) {
1031
1039
  if (callee.type === "identifier") return nodeText(callee, source);
1032
1040
  if (callee.type === "member_expression") {
1041
+ const object = callee.childForFieldName("object");
1042
+ if (object && object.type === "identifier") {
1043
+ return nodeText(object, source);
1044
+ }
1033
1045
  const property = callee.childForFieldName("property");
1034
1046
  if (property) return nodeText(property, source);
1035
1047
  }
@@ -1144,7 +1156,8 @@ function resolveEdges(input) {
1144
1156
  repoId: input.repoId,
1145
1157
  fromPath: edge.fromPath ?? "",
1146
1158
  spec: edge.unresolvedTargetName,
1147
- known: input.knownFilePaths
1159
+ known: input.knownFilePaths,
1160
+ ...input.tsconfigPaths ? { tsconfigPaths: input.tsconfigPaths } : {}
1148
1161
  });
1149
1162
  if (!resolved) {
1150
1163
  dropped++;
@@ -1166,10 +1179,22 @@ function resolveEdges(input) {
1166
1179
  return { resolved: out, dropped };
1167
1180
  }
1168
1181
  function resolveImportPath(input) {
1169
- const { fromPath, spec, known } = input;
1170
- if (!spec.startsWith(".") && !spec.startsWith("/")) return null;
1171
- const baseDir = path2.posix.dirname(toPosix(fromPath));
1172
- const joined = path2.posix.normalize(path2.posix.join(baseDir, toPosix(spec)));
1182
+ const { fromPath, spec, known, tsconfigPaths } = input;
1183
+ if (spec.startsWith(".") || spec.startsWith("/")) {
1184
+ const baseDir = path2.posix.dirname(toPosix(fromPath));
1185
+ const joined = path2.posix.normalize(path2.posix.join(baseDir, toPosix(spec)));
1186
+ return firstMatchingCandidate(joined, known);
1187
+ }
1188
+ if (tsconfigPaths) {
1189
+ const candidates = expandAliasCandidates(spec, tsconfigPaths);
1190
+ for (const c of candidates) {
1191
+ const resolved = firstMatchingCandidate(c, known);
1192
+ if (resolved) return resolved;
1193
+ }
1194
+ }
1195
+ return null;
1196
+ }
1197
+ function firstMatchingCandidate(joined, known) {
1173
1198
  const candidates = [joined];
1174
1199
  const ext = path2.posix.extname(joined);
1175
1200
  const stem = ext ? joined.slice(0, -ext.length) : joined;
@@ -1180,12 +1205,37 @@ function resolveImportPath(input) {
1180
1205
  }
1181
1206
  return null;
1182
1207
  }
1208
+ function expandAliasCandidates(spec, cfg) {
1209
+ const sortedPatterns = Object.keys(cfg.paths).sort((a, b) => b.length - a.length);
1210
+ const out = [];
1211
+ for (const pattern of sortedPatterns) {
1212
+ const replacements = cfg.paths[pattern] ?? [];
1213
+ if (pattern.endsWith("/*")) {
1214
+ const prefix = pattern.slice(0, -1);
1215
+ if (!spec.startsWith(prefix)) continue;
1216
+ const tail = spec.slice(prefix.length);
1217
+ for (const r of replacements) {
1218
+ const rTail = r.endsWith("/*") ? r.slice(0, -1) : r.endsWith("*") ? r.slice(0, -1) : r;
1219
+ const joined = path2.posix.normalize(
1220
+ path2.posix.join(toPosix(cfg.baseUrl), `${rTail}${tail}`)
1221
+ );
1222
+ out.push(joined);
1223
+ }
1224
+ } else if (spec === pattern) {
1225
+ for (const r of replacements) {
1226
+ const joined = path2.posix.normalize(path2.posix.join(toPosix(cfg.baseUrl), r));
1227
+ out.push(joined);
1228
+ }
1229
+ }
1230
+ }
1231
+ return out;
1232
+ }
1183
1233
  function toPosix(p) {
1184
1234
  return p.split(path2.sep).join("/");
1185
1235
  }
1186
1236
 
1187
1237
  // ../ingestion/src/orchestrator.ts
1188
- import { readFile as readFile4, stat as stat2 } from "fs/promises";
1238
+ import { readFile as readFile5, stat as stat2 } from "fs/promises";
1189
1239
  import { cpus } from "os";
1190
1240
  import { join as join2 } from "path";
1191
1241
  import { createHash as createHash2 } from "crypto";
@@ -1224,25 +1274,127 @@ async function embedNodes(nodes, opts) {
1224
1274
  return result;
1225
1275
  }
1226
1276
 
1227
- // ../ingestion/src/repo-walker.ts
1228
- import { readFile as readFile3, readdir, stat } from "fs/promises";
1277
+ // ../ingestion/src/tsconfig.ts
1278
+ import { readFile as readFile3 } from "fs/promises";
1229
1279
  import path3 from "path";
1280
+ async function loadTsconfigPaths(repoRoot) {
1281
+ for (const name of ["tsconfig.json", "jsconfig.json"]) {
1282
+ const raw = await readIfExists(path3.join(repoRoot, name));
1283
+ if (!raw) continue;
1284
+ let parsed;
1285
+ try {
1286
+ parsed = JSON.parse(stripJsonComments(raw));
1287
+ } catch {
1288
+ continue;
1289
+ }
1290
+ const co = parsed.compilerOptions ?? {};
1291
+ const paths = co.paths ?? {};
1292
+ if (Object.keys(paths).length === 0) continue;
1293
+ const baseUrl = (co.baseUrl ?? ".").trim();
1294
+ return {
1295
+ baseUrl: toPosix2(baseUrl),
1296
+ paths
1297
+ };
1298
+ }
1299
+ return null;
1300
+ }
1301
+ async function readIfExists(filePath) {
1302
+ try {
1303
+ return await readFile3(filePath, "utf8");
1304
+ } catch {
1305
+ return null;
1306
+ }
1307
+ }
1308
+ function stripJsonComments(input) {
1309
+ let out = "";
1310
+ let i = 0;
1311
+ let inString = false;
1312
+ let stringChar = "";
1313
+ while (i < input.length) {
1314
+ const c = input[i];
1315
+ const next = input[i + 1];
1316
+ if (inString) {
1317
+ out += c;
1318
+ if (c === "\\" && next !== void 0) {
1319
+ out += next;
1320
+ i += 2;
1321
+ continue;
1322
+ }
1323
+ if (c === stringChar) inString = false;
1324
+ i++;
1325
+ continue;
1326
+ }
1327
+ if (c === '"' || c === "'") {
1328
+ inString = true;
1329
+ stringChar = c;
1330
+ out += c;
1331
+ i++;
1332
+ continue;
1333
+ }
1334
+ if (c === "/" && next === "/") {
1335
+ while (i < input.length && input[i] !== "\n") i++;
1336
+ continue;
1337
+ }
1338
+ if (c === "/" && next === "*") {
1339
+ i += 2;
1340
+ while (i < input.length && !(input[i] === "*" && input[i + 1] === "/")) i++;
1341
+ i += 2;
1342
+ continue;
1343
+ }
1344
+ out += c;
1345
+ i++;
1346
+ }
1347
+ return removeTrailingCommas(out);
1348
+ }
1349
+ function removeTrailingCommas(s) {
1350
+ return s.replace(/,\s*([}\]])/g, "$1");
1351
+ }
1352
+ function toPosix2(p) {
1353
+ return p.split(path3.sep).join("/");
1354
+ }
1355
+
1356
+ // ../ingestion/src/repo-walker.ts
1357
+ import { readFile as readFile4, readdir, stat } from "fs/promises";
1358
+ import path4 from "path";
1230
1359
  import ignore from "ignore";
1231
1360
  var ALWAYS_SKIP_DIRS = /* @__PURE__ */ new Set([
1232
1361
  ".git",
1362
+ ".hg",
1363
+ ".svn",
1233
1364
  "node_modules",
1234
1365
  ".next",
1366
+ ".nuxt",
1367
+ ".svelte-kit",
1235
1368
  "dist",
1236
1369
  "build",
1237
1370
  "out",
1238
1371
  ".turbo",
1239
1372
  ".cache",
1373
+ ".parcel-cache",
1240
1374
  "coverage",
1375
+ ".nyc_output",
1241
1376
  ".idea",
1242
- ".vscode"
1377
+ ".vscode",
1378
+ ".vs",
1379
+ "venv",
1380
+ ".venv",
1381
+ "env",
1382
+ "__pycache__",
1383
+ "site-packages",
1384
+ ".pytest_cache",
1385
+ ".mypy_cache",
1386
+ ".ruff_cache",
1387
+ ".tox",
1388
+ ".eggs",
1389
+ "target",
1390
+ "vendor",
1391
+ "Pods",
1392
+ "DerivedData",
1393
+ ".gradle",
1394
+ ".terraform"
1243
1395
  ]);
1244
1396
  async function walkRepo(root) {
1245
- const base = path3.resolve(root);
1397
+ const base = path4.resolve(root);
1246
1398
  const matcher = ignore();
1247
1399
  await loadGitignoreInto(matcher, base);
1248
1400
  const out = [];
@@ -1259,7 +1411,7 @@ async function walkDir(absDir, relDir, ig, out) {
1259
1411
  for (const entry of entries) {
1260
1412
  const name = entry.name;
1261
1413
  const rel = relDir ? `${relDir}/${name}` : name;
1262
- const abs = path3.join(absDir, name);
1414
+ const abs = path4.join(absDir, name);
1263
1415
  if (entry.isDirectory()) {
1264
1416
  if (ALWAYS_SKIP_DIRS.has(name)) continue;
1265
1417
  if (ig.ignores(`${rel}/`)) continue;
@@ -1273,9 +1425,9 @@ async function walkDir(absDir, relDir, ig, out) {
1273
1425
  }
1274
1426
  }
1275
1427
  async function loadGitignoreInto(ig, absDir) {
1276
- const file = path3.join(absDir, ".gitignore");
1428
+ const file = path4.join(absDir, ".gitignore");
1277
1429
  try {
1278
- const text = await readFile3(file, "utf8");
1430
+ const text = await readFile4(file, "utf8");
1279
1431
  ig.add(text);
1280
1432
  } catch {
1281
1433
  }
@@ -1305,7 +1457,7 @@ async function indexRepo(opts) {
1305
1457
  await runWithConcurrency(parsable, parallelism, async (entry) => {
1306
1458
  const abs = join2(opts.repoPath, entry.rel);
1307
1459
  try {
1308
- const source = await readFile4(abs, "utf8");
1460
+ const source = await readFile5(abs, "utf8");
1309
1461
  const result = await extractFile({
1310
1462
  repoId: opts.repoId,
1311
1463
  relativePath: entry.rel,
@@ -1322,25 +1474,33 @@ async function indexRepo(opts) {
1322
1474
  opts.onProgress?.({ type: "parse", parsed: parsedCount, total: parsable.length });
1323
1475
  }
1324
1476
  });
1477
+ const tsconfigPaths = await loadTsconfigPaths(opts.repoPath);
1325
1478
  const { resolved, dropped } = resolveEdges({
1326
1479
  repoId: opts.repoId,
1327
1480
  nodes: allNodes,
1328
1481
  edges: allEdges,
1329
- knownFilePaths
1482
+ knownFilePaths,
1483
+ ...tsconfigPaths ? { tsconfigPaths } : {}
1330
1484
  });
1331
- await opts.graphDb.deleteByRepo(opts.repoId);
1332
- await opts.graphDb.upsertNodes(allNodes);
1333
- await opts.graphDb.upsertEdges(resolved);
1334
- opts.onProgress?.({ type: "upsert", nodes: allNodes.length, edges: resolved.length });
1335
- let embeddingCount = 0;
1485
+ let embeddedById = /* @__PURE__ */ new Map();
1336
1486
  if (!opts.skipEmbeddings && opts.router) {
1337
1487
  const embedded = await embedNodes(allNodes, {
1338
1488
  router: opts.router,
1339
1489
  onBatch: ({ embedded: embedded2, total }) => opts.onProgress?.({ type: "embed", embedded: embedded2, total })
1340
1490
  });
1341
- await persistEmbeddings(opts.graphDb, embedded);
1342
- embeddingCount = embedded.length;
1491
+ embeddedById = new Map(embedded.map((e) => [e.id, e]));
1343
1492
  }
1493
+ const nodePayload = allNodes.map((n) => {
1494
+ const e = embeddedById.get(n.id);
1495
+ const base = n;
1496
+ if (!e) return base;
1497
+ return { ...base, embedding: e.embedding, embeddingNamespace: e.embeddingNamespace };
1498
+ });
1499
+ await opts.graphDb.deleteByRepo(opts.repoId);
1500
+ await opts.graphDb.upsertNodes(nodePayload);
1501
+ await opts.graphDb.upsertEdges(resolved);
1502
+ opts.onProgress?.({ type: "upsert", nodes: allNodes.length, edges: resolved.length });
1503
+ const embeddingCount = embeddedById.size;
1344
1504
  return {
1345
1505
  durationMs: Date.now() - start,
1346
1506
  parsedFiles: parsedCount - failed,
@@ -1351,22 +1511,6 @@ async function indexRepo(opts) {
1351
1511
  droppedEdges: dropped
1352
1512
  };
1353
1513
  }
1354
- async function persistEmbeddings(graphDb, embedded) {
1355
- if (embedded.length === 0) return;
1356
- const BATCH = 100;
1357
- for (let i = 0; i < embedded.length; i += BATCH) {
1358
- const batch = embedded.slice(i, i + BATCH);
1359
- await graphDb.query(
1360
- `
1361
- UNWIND $batch AS e
1362
- MATCH (n:Symbol { id: e.id })
1363
- SET n.embedding = e.embedding,
1364
- n.embeddingNamespace = e.embeddingNamespace
1365
- `,
1366
- { batch }
1367
- );
1368
- }
1369
- }
1370
1514
  async function runWithConcurrency(items, concurrency, fn) {
1371
1515
  let cursor = 0;
1372
1516
  const runners = Array.from({ length: Math.min(concurrency, items.length) }, async () => {
@@ -1385,10 +1529,10 @@ import kleur4 from "kleur";
1385
1529
 
1386
1530
  // src/repo-id.ts
1387
1531
  import { createHash as createHash3 } from "crypto";
1388
- import path4 from "path";
1532
+ import path5 from "path";
1389
1533
  function repoIdFromPath(absPath) {
1390
- const base = path4.basename(path4.resolve(absPath));
1391
- const sha = createHash3("sha1").update(path4.resolve(absPath)).digest("hex").slice(0, 8);
1534
+ const base = path5.basename(path5.resolve(absPath));
1535
+ const sha = createHash3("sha1").update(path5.resolve(absPath)).digest("hex").slice(0, 8);
1392
1536
  const safe = base.replace(/[^A-Za-z0-9_-]/g, "_");
1393
1537
  return `${safe}-${sha}`;
1394
1538
  }
@@ -1396,7 +1540,7 @@ function repoIdFromPath(absPath) {
1396
1540
  // src/commands/index.ts
1397
1541
  async function runIndexCommand(opts) {
1398
1542
  const config = await loadConfig();
1399
- const absolutePath = path5.resolve(opts.repoPath);
1543
+ const absolutePath = path6.resolve(opts.repoPath);
1400
1544
  const repoId = repoIdFromPath(absolutePath);
1401
1545
  const dbPath = config.data.dbPath ?? defaultDbPath();
1402
1546
  console.log(kleur4.dim(`repo: ${absolutePath}`));
@@ -1434,8 +1578,10 @@ async function runIndexCommand(opts) {
1434
1578
  router = void 0;
1435
1579
  }
1436
1580
  }
1437
- let parseBar = null;
1438
- let embedBar = null;
1581
+ const bars = {
1582
+ parse: null,
1583
+ embed: null
1584
+ };
1439
1585
  const result = await indexRepo({
1440
1586
  repoId,
1441
1587
  repoPath: absolutePath,
@@ -1447,8 +1593,8 @@ async function runIndexCommand(opts) {
1447
1593
  console.log(kleur4.dim(`walked ${event.files} files`));
1448
1594
  }
1449
1595
  if (event.type === "parse") {
1450
- if (!parseBar) parseBar = new ProgressBar("parsing", event.total);
1451
- parseBar.update(event.parsed);
1596
+ if (!bars.parse) bars.parse = new ProgressBar("parsing", event.total);
1597
+ bars.parse.update(event.parsed);
1452
1598
  }
1453
1599
  if (event.type === "upsert") {
1454
1600
  console.log(
@@ -1456,13 +1602,13 @@ async function runIndexCommand(opts) {
1456
1602
  );
1457
1603
  }
1458
1604
  if (event.type === "embed") {
1459
- if (!embedBar) embedBar = new ProgressBar("embedding", event.total);
1460
- embedBar.update(event.embedded);
1605
+ if (!bars.embed) bars.embed = new ProgressBar("embedding", event.total);
1606
+ bars.embed.update(event.embedded);
1461
1607
  }
1462
1608
  }
1463
1609
  });
1464
- parseBar?.done();
1465
- embedBar?.done();
1610
+ bars.parse?.done();
1611
+ bars.embed?.done();
1466
1612
  await graphDb.close();
1467
1613
  console.log();
1468
1614
  console.log(
@@ -1478,7 +1624,7 @@ async function runIndexCommand(opts) {
1478
1624
  }
1479
1625
  async function runStatusCommand(opts) {
1480
1626
  const config = await loadConfig();
1481
- const absolutePath = path5.resolve(opts.repoPath);
1627
+ const absolutePath = path6.resolve(opts.repoPath);
1482
1628
  const repoId = repoIdFromPath(absolutePath);
1483
1629
  const graphDb = new GraphDb({
1484
1630
  dbPath: config.data.dbPath ?? defaultDbPath(),
@@ -1496,10 +1642,62 @@ async function runStatusCommand(opts) {
1496
1642
  throw err;
1497
1643
  }
1498
1644
  }
1645
+ async function runWipeCommand(opts) {
1646
+ const config = await loadConfig();
1647
+ const dbPath = config.data.dbPath ?? defaultDbPath();
1648
+ if (opts.repoPath) {
1649
+ const absolutePath = path6.resolve(opts.repoPath);
1650
+ const repoId = repoIdFromPath(absolutePath);
1651
+ if (!opts.yes) {
1652
+ console.log(
1653
+ kleur4.yellow(
1654
+ `Will delete all graph rows for ${kleur4.bold(repoId)} (${absolutePath}). Pass --yes to confirm.`
1655
+ )
1656
+ );
1657
+ return;
1658
+ }
1659
+ const spinner2 = makeSpinner(`Wiping ${repoId}`).start();
1660
+ const graphDb = new GraphDb({
1661
+ dbPath,
1662
+ embeddingDimension: config.llm.embeddings.dimension
1663
+ });
1664
+ try {
1665
+ await graphDb.connect();
1666
+ await graphDb.migrate();
1667
+ await graphDb.deleteByRepo(repoId);
1668
+ await graphDb.close();
1669
+ spinner2.succeed(`Removed ${repoId} from ${dbPath}`);
1670
+ } catch (err) {
1671
+ spinner2.fail("Wipe failed");
1672
+ throw err;
1673
+ }
1674
+ return;
1675
+ }
1676
+ if (!opts.yes) {
1677
+ console.log(
1678
+ kleur4.yellow(
1679
+ `Will delete the entire graph directory at ${kleur4.bold(dbPath)}. Pass --yes to confirm.`
1680
+ )
1681
+ );
1682
+ return;
1683
+ }
1684
+ const spinner = makeSpinner(`Removing ${dbPath}`).start();
1685
+ try {
1686
+ await rm(dbPath, { recursive: true, force: true });
1687
+ spinner.succeed(`Graph directory removed. Re-run \`codegraph index <repo>\` to rebuild.`);
1688
+ } catch (err) {
1689
+ spinner.fail("Wipe failed");
1690
+ throw err;
1691
+ }
1692
+ }
1693
+
1694
+ // src/commands/init.ts
1695
+ import path7 from "path";
1696
+ import { confirm, password as password2, select as select2 } from "@inquirer/prompts";
1499
1697
 
1500
1698
  // ../mcp-server/dist/index.js
1501
1699
  import { createHash as createHash4 } from "crypto";
1502
- import { mkdir as mkdir3, readFile as readFile5, writeFile as writeFile2 } from "fs/promises";
1700
+ import { mkdir as mkdir3, readFile as readFile6, writeFile as writeFile2 } from "fs/promises";
1503
1701
  import { homedir } from "os";
1504
1702
  import { dirname as dirname3, resolve } from "path";
1505
1703
  import { randomUUID } from "crypto";
@@ -1595,8 +1793,8 @@ var affectedByTool = {
1595
1793
  inputSchema,
1596
1794
  annotations: { readOnlyHint: true, idempotentHint: true }
1597
1795
  },
1598
- handler: async ({ symbol, path: path6, depth, limit }, deps) => cachedJsonResult("affected_by", { symbol, path: path6, depth, limit }, deps, async () => {
1599
- const where = path6 ? "WHERE target.name = $symbol AND target.path = $path" : "WHERE target.name = $symbol";
1796
+ handler: async ({ symbol, path: path8, depth, limit }, deps) => cachedJsonResult("affected_by", { symbol, path: path8, depth, limit }, deps, async () => {
1797
+ const where = path8 ? "WHERE target.name = $symbol AND target.path = $path" : "WHERE target.name = $symbol";
1600
1798
  const rows = await deps.graph.query(
1601
1799
  `MATCH (target:Symbol) ${where}
1602
1800
  WITH target
@@ -1605,9 +1803,9 @@ var affectedByTool = {
1605
1803
  RETURN affected.id AS id, affected.name AS name, affected.kind AS kind,
1606
1804
  affected.path AS path, distance
1607
1805
  ORDER BY distance ASC, affected.path ASC LIMIT $limit`,
1608
- { symbol, path: path6 ?? null, limit }
1806
+ { symbol, path: path8 ?? null, limit }
1609
1807
  );
1610
- return { symbol, path: path6 ?? null, depth, count: rows.length, affected: rows };
1808
+ return { symbol, path: path8 ?? null, depth, count: rows.length, affected: rows };
1611
1809
  })
1612
1810
  };
1613
1811
  var inputSchema2 = {
@@ -1624,14 +1822,14 @@ var blastRadiusTool = {
1624
1822
  inputSchema: inputSchema2,
1625
1823
  annotations: { readOnlyHint: true, idempotentHint: true }
1626
1824
  },
1627
- handler: async ({ symbol, path: path6, depth, sampleSize }, deps) => cachedJsonResult("blast_radius", { symbol, path: path6, depth, sampleSize }, deps, async () => {
1628
- const where = path6 ? "WHERE target.name = $symbol AND target.path = $path" : "WHERE target.name = $symbol";
1825
+ handler: async ({ symbol, path: path8, depth, sampleSize }, deps) => cachedJsonResult("blast_radius", { symbol, path: path8, depth, sampleSize }, deps, async () => {
1826
+ const where = path8 ? "WHERE target.name = $symbol AND target.path = $path" : "WHERE target.name = $symbol";
1629
1827
  const [stats] = await deps.graph.query(
1630
1828
  `MATCH (target:Symbol) ${where}
1631
1829
  WITH target
1632
1830
  MATCH (dependent:Symbol)-[:CALLS|IMPORTS|RENDERS*1..${depth}]->(target)
1633
1831
  RETURN count(DISTINCT dependent) AS total`,
1634
- { symbol, path: path6 ?? null }
1832
+ { symbol, path: path8 ?? null }
1635
1833
  );
1636
1834
  const sample = await deps.graph.query(
1637
1835
  `MATCH (target:Symbol) ${where}
@@ -1641,11 +1839,11 @@ var blastRadiusTool = {
1641
1839
  RETURN dependent.id AS id, dependent.name AS name, dependent.kind AS kind,
1642
1840
  dependent.path AS path, distance
1643
1841
  ORDER BY distance ASC, dependent.path ASC LIMIT $sampleSize`,
1644
- { symbol, path: path6 ?? null, sampleSize }
1842
+ { symbol, path: path8 ?? null, sampleSize }
1645
1843
  );
1646
1844
  return {
1647
1845
  symbol,
1648
- path: path6 ?? null,
1846
+ path: path8 ?? null,
1649
1847
  depth,
1650
1848
  totalDependents: stats?.total ?? 0,
1651
1849
  sample
@@ -1712,15 +1910,46 @@ var getComponentTreeTool = {
1712
1910
  annotations: { readOnlyHint: true, idempotentHint: true }
1713
1911
  },
1714
1912
  handler: async ({ name, depth }, deps) => cachedJsonResult("get_component_tree", { name, depth }, deps, async () => {
1715
- const rows = await deps.graph.query(
1716
- `MATCH (root:Symbol {kind: 'Component', name: $name})
1717
- OPTIONAL MATCH path = (root)-[:RENDERS*1..${depth}]->(child:Symbol)
1718
- WHERE child.kind = 'Component'
1719
- WITH root, collect(DISTINCT { name: child.name, id: child.id, path: child.path, depth: length(path) }) AS children
1720
- RETURN root.id AS rootId, root.name AS rootName, root.path AS rootPath, children`,
1913
+ const rootRows = await deps.graph.query(
1914
+ `MATCH (root:Symbol)
1915
+ WHERE root.name = $name AND (root.kind = 'Component' OR root.kind = 'Function' OR root.kind = 'Class')
1916
+ RETURN root.id AS rootId, root.name AS rootName, root.path AS rootPath, root.kind AS rootKind
1917
+ ORDER BY CASE root.kind WHEN 'Component' THEN 0 WHEN 'Function' THEN 1 ELSE 2 END
1918
+ LIMIT 1`,
1721
1919
  { name }
1722
1920
  );
1723
- return { name, depth, tree: rows[0] ?? null };
1921
+ const root = rootRows[0];
1922
+ if (!root) {
1923
+ return {
1924
+ name,
1925
+ depth,
1926
+ found: false,
1927
+ root: null,
1928
+ children: []
1929
+ };
1930
+ }
1931
+ const children = await deps.graph.query(
1932
+ `MATCH (root:Symbol {id: $rootId})
1933
+ OPTIONAL MATCH p = (root)-[:RENDERS*1..${depth}]->(child:Symbol)
1934
+ WITH child, min(length(p)) AS distance
1935
+ WHERE child IS NOT NULL
1936
+ RETURN child.id AS id, child.name AS name, child.kind AS kind,
1937
+ child.path AS path, distance
1938
+ ORDER BY distance ASC, child.name ASC`,
1939
+ { rootId: root.rootId }
1940
+ );
1941
+ return {
1942
+ name,
1943
+ depth,
1944
+ found: true,
1945
+ root: {
1946
+ id: root.rootId,
1947
+ name: root.rootName,
1948
+ path: root.rootPath,
1949
+ kind: root.rootKind
1950
+ },
1951
+ children
1952
+ };
1724
1953
  })
1725
1954
  };
1726
1955
  var inputSchema6 = {
@@ -1736,7 +1965,7 @@ var getDependenciesTool = {
1736
1965
  inputSchema: inputSchema6,
1737
1966
  annotations: { readOnlyHint: true, idempotentHint: true }
1738
1967
  },
1739
- handler: async ({ path: path6, depth, limit }, deps) => cachedJsonResult("get_dependencies", { path: path6, depth, limit }, deps, async () => {
1968
+ handler: async ({ path: path8, depth, limit }, deps) => cachedJsonResult("get_dependencies", { path: path8, depth, limit }, deps, async () => {
1740
1969
  const rows = await deps.graph.query(
1741
1970
  `MATCH (root:Symbol)
1742
1971
  WHERE root.kind = 'File' AND root.path = $path
@@ -1746,9 +1975,9 @@ var getDependenciesTool = {
1746
1975
  WHERE dep IS NOT NULL
1747
1976
  RETURN dep.id AS id, dep.path AS path, distance
1748
1977
  ORDER BY distance ASC, dep.path ASC LIMIT $limit`,
1749
- { path: path6, limit }
1978
+ { path: path8, limit }
1750
1979
  );
1751
- return { path: path6, depth, count: rows.length, dependencies: rows };
1980
+ return { path: path8, depth, count: rows.length, dependencies: rows };
1752
1981
  })
1753
1982
  };
1754
1983
  var inputSchema7 = {
@@ -1763,15 +1992,15 @@ var getFileContextTool = {
1763
1992
  inputSchema: inputSchema7,
1764
1993
  annotations: { readOnlyHint: true, idempotentHint: true }
1765
1994
  },
1766
- handler: async ({ path: path6, symbolLimit }, deps) => cachedJsonResult("get_file_context", { path: path6, symbolLimit }, deps, async () => {
1995
+ handler: async ({ path: path8, symbolLimit }, deps) => cachedJsonResult("get_file_context", { path: path8, symbolLimit }, deps, async () => {
1767
1996
  const fileRows = await deps.graph.query(
1768
1997
  `MATCH (f:Symbol)
1769
1998
  WHERE f.kind = 'File' AND f.path = $path
1770
1999
  RETURN f.id AS id, f.path AS path, f.name AS name LIMIT 1`,
1771
- { path: path6 }
2000
+ { path: path8 }
1772
2001
  );
1773
2002
  if (fileRows.length === 0) {
1774
- return { path: path6, found: false };
2003
+ return { path: path8, found: false };
1775
2004
  }
1776
2005
  const [defined, imports, exports, importers] = await Promise.all([
1777
2006
  deps.graph.query(
@@ -1779,30 +2008,33 @@ var getFileContextTool = {
1779
2008
  WHERE f.kind = 'File' AND f.path = $path
1780
2009
  RETURN s.id AS id, s.name AS name, s.kind AS kind, s.lineStart AS line, s.signature AS signature
1781
2010
  ORDER BY s.lineStart LIMIT $limit`,
1782
- { path: path6, limit: symbolLimit }
2011
+ { path: path8, limit: symbolLimit }
1783
2012
  ),
1784
2013
  deps.graph.query(
2014
+ // Kuzu drops the node variable `t` from scope after `RETURN DISTINCT`, so the
2015
+ // ORDER BY references the projection alias instead - using `t.path` here
2016
+ // raises a "Variable t is not in scope" binder exception.
1785
2017
  `MATCH (f:Symbol)-[:IMPORTS]->(t:Symbol)
1786
2018
  WHERE f.kind = 'File' AND t.kind = 'File' AND f.path = $path
1787
- RETURN DISTINCT t.path AS path ORDER BY t.path`,
1788
- { path: path6 }
2019
+ RETURN DISTINCT t.path AS path ORDER BY path`,
2020
+ { path: path8 }
1789
2021
  ),
1790
2022
  deps.graph.query(
1791
2023
  `MATCH (f:Symbol)-[:EXPORTS]->(s:Symbol)
1792
2024
  WHERE f.kind = 'File' AND f.path = $path
1793
2025
  RETURN s.id AS id, s.name AS name, s.kind AS kind, s.lineStart AS line
1794
2026
  ORDER BY s.name`,
1795
- { path: path6 }
2027
+ { path: path8 }
1796
2028
  ),
1797
2029
  deps.graph.query(
1798
2030
  `MATCH (other:Symbol)-[:IMPORTS]->(f:Symbol)
1799
2031
  WHERE other.kind = 'File' AND f.kind = 'File' AND f.path = $path
1800
- RETURN DISTINCT other.path AS path ORDER BY other.path`,
1801
- { path: path6 }
2032
+ RETURN DISTINCT other.path AS path ORDER BY path`,
2033
+ { path: path8 }
1802
2034
  )
1803
2035
  ]);
1804
2036
  return {
1805
- path: path6,
2037
+ path: path8,
1806
2038
  found: true,
1807
2039
  file: fileRows[0],
1808
2040
  definedSymbols: defined,
@@ -2005,14 +2237,16 @@ var searchSemanticTool = {
2005
2237
  );
2006
2238
  }
2007
2239
  const namespace = `${deps.llm.embeddingNamespace.provider}:${deps.llm.embeddingNamespace.model}:${deps.llm.embeddingNamespace.dimension}`;
2240
+ const dim = deps.llm.embeddingNamespace.dimension;
2008
2241
  return cachedJsonResult("search_semantic", { description, k, namespace }, deps, async () => {
2009
2242
  const rows = await deps.graph.query(
2010
- `CALL QUERY_VECTOR_INDEX('Symbol', 'embedding_idx', $vec, $k)
2011
- WITH node, distance
2012
- WHERE node.embeddingNamespace = $ns
2013
- RETURN node.id AS id, node.name AS name, node.kind AS kind, node.path AS path,
2014
- node.lineStart AS line, node.signature AS signature, distance AS score
2015
- ORDER BY distance ASC`,
2243
+ `MATCH (s:Symbol)
2244
+ WHERE s.embeddingNamespace = $ns
2245
+ RETURN s.id AS id, s.name AS name, s.kind AS kind, s.path AS path,
2246
+ s.lineStart AS line, s.signature AS signature,
2247
+ array_cosine_similarity(s.embedding, CAST($vec AS FLOAT[${dim}])) AS score
2248
+ ORDER BY score DESC
2249
+ LIMIT $k`,
2016
2250
  { vec: embedding, k, ns: namespace }
2017
2251
  );
2018
2252
  return { description, k, namespace, count: rows.length, matches: rows };
@@ -2214,23 +2448,23 @@ var DEFAULT_TTL = 30;
2214
2448
  function defaultConfigPath() {
2215
2449
  return resolve(homedir(), ".codegraph", "config.json");
2216
2450
  }
2217
- async function readCodegraphConfig(path6 = defaultConfigPath()) {
2451
+ async function readCodegraphConfig(path8 = defaultConfigPath()) {
2218
2452
  try {
2219
- const raw = await readFile5(path6, "utf8");
2453
+ const raw = await readFile6(path8, "utf8");
2220
2454
  return JSON.parse(raw);
2221
2455
  } catch (err) {
2222
2456
  if (err.code === "ENOENT") return {};
2223
2457
  throw err;
2224
2458
  }
2225
2459
  }
2226
- async function writeCodegraphConfig(config, path6 = defaultConfigPath()) {
2227
- await mkdir3(dirname3(path6), { recursive: true });
2228
- await writeFile2(path6, `${JSON.stringify(config, null, 2)}
2460
+ async function writeCodegraphConfig(config, path8 = defaultConfigPath()) {
2461
+ await mkdir3(dirname3(path8), { recursive: true });
2462
+ await writeFile2(path8, `${JSON.stringify(config, null, 2)}
2229
2463
  `, "utf8");
2230
2464
  }
2231
2465
  async function resolveServerConfig(opts) {
2232
- const path6 = opts.configPath ?? defaultConfigPath();
2233
- const fileConfig = await readCodegraphConfig(path6);
2466
+ const path8 = opts.configPath ?? defaultConfigPath();
2467
+ const fileConfig = await readCodegraphConfig(path8);
2234
2468
  const env = process.env;
2235
2469
  let bearerToken = opts.overrides?.bearerToken ?? fileConfig.mcpToken ?? fileConfig.server?.bearerToken ?? env.CODEGRAPH_BEARER_TOKEN ?? "";
2236
2470
  let created = false;
@@ -2243,7 +2477,7 @@ async function resolveServerConfig(opts) {
2243
2477
  mcpToken: bearerToken,
2244
2478
  server: { ...fileConfig.server ?? {}, bearerToken }
2245
2479
  },
2246
- path6
2480
+ path8
2247
2481
  );
2248
2482
  }
2249
2483
  const config = {
@@ -2252,7 +2486,7 @@ async function resolveServerConfig(opts) {
2252
2486
  bearerToken,
2253
2487
  cacheTtlSeconds: opts.overrides?.cacheTtlSeconds ?? fileConfig.server?.cacheTtlSeconds ?? (env.CODEGRAPH_CACHE_TTL ? Number(env.CODEGRAPH_CACHE_TTL) : DEFAULT_TTL)
2254
2488
  };
2255
- return { config, configPath: path6, created };
2489
+ return { config, configPath: path8, created };
2256
2490
  }
2257
2491
  function resolveDbPath(fileConfig) {
2258
2492
  return fileConfig.data?.dbPath ?? process.env.CODEGRAPH_DB_PATH ?? void 0;
@@ -2300,7 +2534,7 @@ async function startMcpServer(portOrOptions) {
2300
2534
  async function loadGraphClient(dbPath) {
2301
2535
  let mod;
2302
2536
  try {
2303
- mod = await import("./src-7P6XREHJ.js");
2537
+ mod = await import("./src-M7HSEMBT.js");
2304
2538
  } catch (err) {
2305
2539
  throw new Error(
2306
2540
  `Failed to import @codegraph/graph-db. Run \`pnpm -r build\` first. Underlying error: ${err instanceof Error ? err.message : String(err)}`
@@ -2358,8 +2592,473 @@ function adaptLlmRouter(router) {
2358
2592
  };
2359
2593
  }
2360
2594
 
2595
+ // src/commands/init.ts
2596
+ import boxen2 from "boxen";
2597
+ import kleur5 from "kleur";
2598
+ var TOTAL_STEPS = 6;
2599
+ async function runInitCommand() {
2600
+ printWelcome();
2601
+ const start = await confirm({ message: "Ready to get started?", default: true });
2602
+ if (!start) {
2603
+ console.log(kleur5.dim("Aborted. Run `codegraph init` again any time."));
2604
+ return;
2605
+ }
2606
+ runEnvCheck();
2607
+ await runLlmSetup();
2608
+ await runLiveTest();
2609
+ await runIndexStep();
2610
+ const bearerToken = await runServeStep();
2611
+ await runClientStep(bearerToken);
2612
+ await runCelebration();
2613
+ }
2614
+ function printWelcome() {
2615
+ printBanner();
2616
+ const description = [
2617
+ kleur5.bold("CodeGraph") + " parses your JS/TS repo with tree-sitter, stores nodes (Function, Class,",
2618
+ "Route\u2026) and edges (CALLS, IMPORTS, RENDERS\u2026) in an embedded Kuzu graph with",
2619
+ "vector embeddings, then runs a local MCP server your AI assistant can call.",
2620
+ "",
2621
+ kleur5.dim("This wizard will:"),
2622
+ ` ${kleur5.cyan("1.")} Configure an LLM provider (cloud or local)`,
2623
+ ` ${kleur5.cyan("2.")} Verify your credentials with a live round-trip`,
2624
+ ` ${kleur5.cyan("3.")} Index this repo into the graph`,
2625
+ ` ${kleur5.cyan("4.")} Show you how to boot the MCP server`,
2626
+ ` ${kleur5.cyan("5.")} Print a copy-paste config for your AI client`
2627
+ ].join("\n");
2628
+ process.stdout.write(
2629
+ `${boxen2(description, {
2630
+ padding: 1,
2631
+ margin: { top: 0, bottom: 1, left: 0, right: 0 },
2632
+ borderStyle: "round",
2633
+ borderColor: "cyan",
2634
+ title: kleur5.bold("Welcome"),
2635
+ titleAlignment: "left"
2636
+ })}
2637
+ `
2638
+ );
2639
+ }
2640
+ function stepHeader(n, label) {
2641
+ console.log();
2642
+ console.log(
2643
+ kleur5.cyan(`\u2500\u2500\u2500\u2500 Step ${n}/${TOTAL_STEPS}: ${kleur5.bold(label)} ${"\u2500".repeat(Math.max(4, 40 - label.length))}`)
2644
+ );
2645
+ }
2646
+ function runEnvCheck() {
2647
+ stepHeader(1, "Environment check");
2648
+ const version = process.versions.node;
2649
+ const major = Number(version.split(".")[0] ?? 0);
2650
+ if (major < 20) {
2651
+ console.log(`${kleur5.red("\u2717")} node version v${version} ${kleur5.red("(codegraph requires >= 20)")}`);
2652
+ throw new Error(`Node ${version} is too old; install Node 20+ and re-run \`codegraph init\`.`);
2653
+ }
2654
+ console.log(`${kleur5.green("\u2713")} node version ${kleur5.dim(`v${version}`)}`);
2655
+ }
2656
+ async function runLlmSetup() {
2657
+ stepHeader(2, "LLM setup");
2658
+ const mode = await select2({
2659
+ message: "How would you like to power CodeGraph?",
2660
+ choices: [
2661
+ {
2662
+ name: "Cloud provider",
2663
+ value: "cloud",
2664
+ description: "OpenAI, Anthropic, or Google. Fastest setup, requires an API key."
2665
+ },
2666
+ {
2667
+ name: "Local (Ollama)",
2668
+ value: "local",
2669
+ description: "Fully private. Requires Ollama running on :11434."
2670
+ }
2671
+ ]
2672
+ });
2673
+ let preset;
2674
+ if (mode === "local") {
2675
+ preset = "local-ollama";
2676
+ } else {
2677
+ preset = await select2({
2678
+ message: "Which cloud provider?",
2679
+ choices: [
2680
+ {
2681
+ name: "OpenAI",
2682
+ value: "byo-openai",
2683
+ description: "gpt-4o-mini + text-embedding-3-small"
2684
+ },
2685
+ {
2686
+ name: "Anthropic",
2687
+ value: "byo-anthropic",
2688
+ description: "claude-3-5-haiku (gen) + OpenAI text-embedding-3-small (embed)"
2689
+ },
2690
+ {
2691
+ name: "Google",
2692
+ value: "byo-google",
2693
+ description: "gemini-1.5-flash + text-embedding-004"
2694
+ }
2695
+ ]
2696
+ });
2697
+ }
2698
+ const lookup = LLM_PRESETS[preset];
2699
+ if (!lookup) {
2700
+ throw new Error(`Unknown preset "${preset}".`);
2701
+ }
2702
+ const config = await loadConfig();
2703
+ config.llm = { ...config.llm, ...lookup };
2704
+ await saveConfig(config);
2705
+ console.log();
2706
+ console.log(`${kleur5.green("\u2713")} preset saved ${kleur5.dim(`"${preset}" \u2192 ${configPath()}`)}`);
2707
+ console.log(`${kleur5.dim(" namespace ")}${kleur5.dim(namespaceLabel(config.llm))}`);
2708
+ if (preset === "byo-anthropic") {
2709
+ console.log();
2710
+ console.log(
2711
+ kleur5.yellow(
2712
+ "Note: Anthropic has no embedding API, so codegraph uses OpenAI embeddings."
2713
+ )
2714
+ );
2715
+ console.log(
2716
+ kleur5.yellow("You'll need both ANTHROPIC_API_KEY and OPENAI_API_KEY set.")
2717
+ );
2718
+ await promptApiKey("ANTHROPIC_API_KEY");
2719
+ await promptApiKey("OPENAI_API_KEY");
2720
+ return;
2721
+ }
2722
+ const envVar = apiKeyEnvVarFor2(preset);
2723
+ if (envVar) await promptApiKey(envVar);
2724
+ }
2725
+ function apiKeyEnvVarFor2(preset) {
2726
+ if (preset === "byo-openai" || preset === "managed-stub") return "OPENAI_API_KEY";
2727
+ if (preset === "byo-anthropic") return "ANTHROPIC_API_KEY";
2728
+ if (preset === "byo-google") return "GOOGLE_GENERATIVE_AI_API_KEY";
2729
+ return null;
2730
+ }
2731
+ async function promptApiKey(envVar) {
2732
+ console.log();
2733
+ if (process.env[envVar]) {
2734
+ console.log(`${kleur5.green("\u2713")} ${envVar} ${kleur5.dim("already set in this shell")}`);
2735
+ return;
2736
+ }
2737
+ const provide = await confirm({
2738
+ message: `${envVar} is not set. Provide it now? (input will be hidden)`,
2739
+ default: true
2740
+ });
2741
+ if (!provide) {
2742
+ console.log(
2743
+ kleur5.yellow(`! Skipped. Export ${envVar} before \`codegraph index\` or \`codegraph serve\`.`)
2744
+ );
2745
+ return;
2746
+ }
2747
+ const value = await password2({ message: `${envVar}`, mask: "*" });
2748
+ if (!value) {
2749
+ console.log(kleur5.yellow(`! Empty value \u2014 skipping ${envVar}.`));
2750
+ return;
2751
+ }
2752
+ process.env[envVar] = value;
2753
+ console.log();
2754
+ console.log(kleur5.dim("Add this to your ~/.zshrc or ~/.bashrc so it persists:"));
2755
+ console.log(` ${kleur5.cyan(`export ${envVar}=${maskTail(value)}`)}`);
2756
+ }
2757
+ function maskTail(value) {
2758
+ if (value.length <= 8) return "*".repeat(value.length);
2759
+ return `${"*".repeat(value.length - 4)}${value.slice(-4)}`;
2760
+ }
2761
+ async function runLiveTest() {
2762
+ stepHeader(3, "Verify credentials");
2763
+ while (true) {
2764
+ const config = await loadConfig();
2765
+ console.log(
2766
+ kleur5.dim(
2767
+ ` testing ${config.llm.generation.provider}:${config.llm.generation.model} + ${config.llm.embeddings.provider}:${config.llm.embeddings.model}`
2768
+ )
2769
+ );
2770
+ const spinner = makeSpinner("Calling provider").start();
2771
+ try {
2772
+ const router = await createLlmRouter({ config: config.llm });
2773
+ const result = await router.selfTest();
2774
+ spinner.succeed("LLM provider reachable");
2775
+ console.log(
2776
+ kleur5.dim(
2777
+ ` embed=${result.embedLatencyMs}ms gen=${result.generateLatencyMs}ms dims=${result.embedDims}`
2778
+ )
2779
+ );
2780
+ return;
2781
+ } catch (err) {
2782
+ spinner.fail("LLM provider unreachable");
2783
+ console.log(kleur5.red(` ${err instanceof Error ? err.message : String(err)}`));
2784
+ const choice = await select2({
2785
+ message: "What now?",
2786
+ choices: [
2787
+ { name: "Retry (I'll fix it in another terminal)", value: "retry" },
2788
+ { name: "Skip \u2014 set up LLM later", value: "skip" }
2789
+ ]
2790
+ });
2791
+ if (choice === "skip") {
2792
+ console.log(
2793
+ kleur5.yellow(
2794
+ "! Continuing without a verified LLM. Embeddings and semantic search will fail until you fix this."
2795
+ )
2796
+ );
2797
+ return;
2798
+ }
2799
+ }
2800
+ }
2801
+ }
2802
+ async function runIndexStep() {
2803
+ stepHeader(4, "Index this repo");
2804
+ const cwd = process.cwd();
2805
+ const display = abbreviateHome(cwd);
2806
+ const yes = await confirm({
2807
+ message: `Index the current directory (${display}) now?`,
2808
+ default: true
2809
+ });
2810
+ if (!yes) {
2811
+ console.log(kleur5.dim("Skipped. Run this when ready:"));
2812
+ console.log(` ${kleur5.cyan(`codegraph index ${display}`)}`);
2813
+ return;
2814
+ }
2815
+ console.log();
2816
+ try {
2817
+ await runIndexCommand({ repoPath: cwd });
2818
+ } catch (err) {
2819
+ console.log();
2820
+ console.log(
2821
+ kleur5.yellow(
2822
+ `! Index failed: ${err instanceof Error ? err.message : String(err)}`
2823
+ )
2824
+ );
2825
+ console.log(
2826
+ kleur5.dim(" You can retry later with `codegraph index <path>`. Continuing wizard.")
2827
+ );
2828
+ }
2829
+ }
2830
+ async function runServeStep() {
2831
+ stepHeader(5, "Boot the MCP server");
2832
+ const { config: serverConfig, created } = await resolveServerConfig({});
2833
+ if (created) {
2834
+ console.log(kleur5.dim(" generated a new bearer token for this machine"));
2835
+ }
2836
+ const url = `http://${serverConfig.host}:${serverConfig.port}/mcp`;
2837
+ const body = [
2838
+ "Open a " + kleur5.bold("new terminal") + " and run:",
2839
+ "",
2840
+ ` ${kleur5.cyan("codegraph serve")}`,
2841
+ "",
2842
+ kleur5.dim("Expected output:"),
2843
+ kleur5.dim(` codegraph mcp listening on ${url}`),
2844
+ "",
2845
+ kleur5.dim("Keep that terminal running while you use CodeGraph.")
2846
+ ].join("\n");
2847
+ process.stdout.write(
2848
+ `${boxen2(body, {
2849
+ padding: { top: 0, bottom: 0, left: 2, right: 2 },
2850
+ margin: { top: 1, bottom: 1, left: 0, right: 0 },
2851
+ borderStyle: "round",
2852
+ borderColor: "cyan",
2853
+ title: kleur5.bold("Start the server"),
2854
+ titleAlignment: "left"
2855
+ })}
2856
+ `
2857
+ );
2858
+ await confirm({
2859
+ message: "Done \u2014 the server is running in another terminal?",
2860
+ default: true
2861
+ });
2862
+ return serverConfig.bearerToken;
2863
+ }
2864
+ async function runClientStep(bearerToken) {
2865
+ stepHeader(6, "Connect your AI client");
2866
+ const url = "http://127.0.0.1:3748/mcp";
2867
+ const client = await select2({
2868
+ message: "Which AI client are you connecting?",
2869
+ choices: [
2870
+ { name: "Cursor", value: "cursor" },
2871
+ { name: "Claude Code", value: "claude" },
2872
+ { name: "Windsurf", value: "windsurf" },
2873
+ { name: "Skip for now", value: "skip" }
2874
+ ]
2875
+ });
2876
+ if (client === "skip") {
2877
+ console.log(
2878
+ kleur5.dim("Skipped. See docs/clients.md for per-client configuration when ready.")
2879
+ );
2880
+ return;
2881
+ }
2882
+ if (client === "cursor") {
2883
+ const snippet = JSON.stringify(
2884
+ {
2885
+ mcpServers: {
2886
+ codegraph: {
2887
+ url,
2888
+ headers: { Authorization: `Bearer ${bearerToken}` }
2889
+ }
2890
+ }
2891
+ },
2892
+ null,
2893
+ 2
2894
+ );
2895
+ const body = [
2896
+ "Create or edit " + kleur5.cyan(".cursor/mcp.json") + " in your project root:",
2897
+ "",
2898
+ kleur5.green(snippet),
2899
+ "",
2900
+ kleur5.dim("Then open Cursor \u2192 Settings \u2192 MCP."),
2901
+ kleur5.dim("`codegraph` should appear as connected with 10 tools.")
2902
+ ].join("\n");
2903
+ process.stdout.write(
2904
+ `${boxen2(body, {
2905
+ padding: 1,
2906
+ margin: { top: 1, bottom: 1, left: 0, right: 0 },
2907
+ borderStyle: "round",
2908
+ borderColor: "magenta",
2909
+ title: kleur5.bold("Cursor config"),
2910
+ titleAlignment: "left"
2911
+ })}
2912
+ `
2913
+ );
2914
+ } else if (client === "claude") {
2915
+ const oneliner = [
2916
+ "claude mcp add --transport sse codegraph \\",
2917
+ ` ${url} \\`,
2918
+ ` --header "Authorization: Bearer ${bearerToken}"`
2919
+ ].join("\n");
2920
+ const body = [
2921
+ "Run this in your terminal:",
2922
+ "",
2923
+ kleur5.green(oneliner),
2924
+ "",
2925
+ kleur5.dim("Then type ") + kleur5.cyan("/mcp") + kleur5.dim(" in Claude Code to confirm 10 tools are listed.")
2926
+ ].join("\n");
2927
+ process.stdout.write(
2928
+ `${boxen2(body, {
2929
+ padding: 1,
2930
+ margin: { top: 1, bottom: 1, left: 0, right: 0 },
2931
+ borderStyle: "round",
2932
+ borderColor: "magenta",
2933
+ title: kleur5.bold("Claude Code config"),
2934
+ titleAlignment: "left"
2935
+ })}
2936
+ `
2937
+ );
2938
+ } else {
2939
+ const snippet = JSON.stringify(
2940
+ {
2941
+ mcpServers: {
2942
+ codegraph: {
2943
+ serverUrl: url,
2944
+ headers: { Authorization: `Bearer ${bearerToken}` }
2945
+ }
2946
+ }
2947
+ },
2948
+ null,
2949
+ 2
2950
+ );
2951
+ const body = [
2952
+ "Edit " + kleur5.cyan("~/.codeium/windsurf/mcp_config.json") + ":",
2953
+ "",
2954
+ kleur5.green(snippet),
2955
+ "",
2956
+ kleur5.dim("Then restart Windsurf or run ") + kleur5.cyan("/refresh-tools") + kleur5.dim(" in Cascade.")
2957
+ ].join("\n");
2958
+ process.stdout.write(
2959
+ `${boxen2(body, {
2960
+ padding: 1,
2961
+ margin: { top: 1, bottom: 1, left: 0, right: 0 },
2962
+ borderStyle: "round",
2963
+ borderColor: "magenta",
2964
+ title: kleur5.bold("Windsurf config"),
2965
+ titleAlignment: "left"
2966
+ })}
2967
+ `
2968
+ );
2969
+ }
2970
+ await confirm({
2971
+ message: "Done \u2014 your client is configured?",
2972
+ default: true
2973
+ });
2974
+ }
2975
+ async function runCelebration() {
2976
+ console.log();
2977
+ console.log(kleur5.cyan(`\u2500\u2500\u2500\u2500 ${kleur5.bold("Almost there!")} \u2500\u2500\u2500\u2500`));
2978
+ console.log(
2979
+ kleur5.dim(
2980
+ 'Ask your AI assistant something like: "What files are in this repo?"'
2981
+ )
2982
+ );
2983
+ console.log(
2984
+ kleur5.dim("It should call a `codegraph_*` MCP tool to answer.")
2985
+ );
2986
+ console.log();
2987
+ const choice = await select2({
2988
+ message: "How did it go?",
2989
+ choices: [
2990
+ { name: "It worked!", value: "worked" },
2991
+ { name: "Skip", value: "skip" }
2992
+ ]
2993
+ });
2994
+ const header = choice === "worked" ? kleur5.green().bold("\u2713 CodeGraph is fully connected!") : kleur5.cyan().bold("\u2713 CodeGraph setup complete");
2995
+ const body = [
2996
+ header,
2997
+ "",
2998
+ "Your AI assistant can now call these tools:",
2999
+ ` ${kleur5.cyan("\u2022")} find_callers \u2014 what calls a function`,
3000
+ ` ${kleur5.cyan("\u2022")} get_component_tree \u2014 render dependency tree`,
3001
+ ` ${kleur5.cyan("\u2022")} blast_radius \u2014 impact of a change`,
3002
+ ` ${kleur5.cyan("\u2022")} search_semantic \u2014 symbol search by description`,
3003
+ ` ${kleur5.cyan("\u2022")} nl_query \u2014 natural-language Cypher`,
3004
+ ` ${kleur5.dim("\u2026plus 5 more (search_symbol, find_file, get_file_context,")}`,
3005
+ ` ${kleur5.dim(" get_dependencies, affected_by)")}`,
3006
+ "",
3007
+ kleur5.dim("Helpful next steps:"),
3008
+ ` ${kleur5.cyan("codegraph doctor")} ${kleur5.dim("\u2014 verify environment + LLM + Kuzu")}`,
3009
+ ` ${kleur5.cyan("codegraph status")} ${kleur5.dim("<path>")} ${kleur5.dim("\u2014 node/edge counts + embedding coverage")}`,
3010
+ "",
3011
+ kleur5.dim("Docs: https://github.com/leanlabsinnov/codegraph")
3012
+ ].join("\n");
3013
+ process.stdout.write(
3014
+ `${boxen2(body, {
3015
+ padding: 1,
3016
+ margin: { top: 1, bottom: 1, left: 0, right: 0 },
3017
+ borderStyle: "round",
3018
+ borderColor: choice === "worked" ? "green" : "cyan"
3019
+ })}
3020
+ `
3021
+ );
3022
+ }
3023
+ function abbreviateHome(p) {
3024
+ const home = process.env.HOME ?? process.env.USERPROFILE;
3025
+ if (!home) return p;
3026
+ const rel = path7.relative(home, p);
3027
+ if (rel.startsWith("..")) return p;
3028
+ return rel ? `~${path7.sep}${rel}` : "~";
3029
+ }
3030
+
2361
3031
  // src/commands/serve.ts
3032
+ import kleur6 from "kleur";
3033
+ function providerEnvVar2(provider) {
3034
+ if (provider === "openai") return "OPENAI_API_KEY";
3035
+ if (provider === "anthropic") return "ANTHROPIC_API_KEY";
3036
+ if (provider === "google") return "GOOGLE_GENERATIVE_AI_API_KEY";
3037
+ return null;
3038
+ }
2362
3039
  async function runServeCommand(opts = {}) {
3040
+ try {
3041
+ const config = await loadConfig();
3042
+ const missing = [];
3043
+ for (const provider of [
3044
+ config.llm.generation.provider,
3045
+ config.llm.embeddings.provider
3046
+ ]) {
3047
+ const envVar = providerEnvVar2(provider);
3048
+ if (envVar && !process.env[envVar] && !missing.includes(envVar)) {
3049
+ missing.push(envVar);
3050
+ }
3051
+ }
3052
+ if (missing.length > 0) {
3053
+ process.stderr.write(
3054
+ `${kleur6.yellow(
3055
+ `! ${missing.join(", ")} not set in serve's environment. Semantic search and nl_query will fail until the variable is exported before \`codegraph serve\`.`
3056
+ )}
3057
+ `
3058
+ );
3059
+ }
3060
+ } catch {
3061
+ }
2363
3062
  const spinner = makeSpinner("Booting MCP server").start();
2364
3063
  let started;
2365
3064
  try {
@@ -2400,13 +3099,16 @@ shutting down (${signal})...
2400
3099
  // src/program.ts
2401
3100
  function buildProgram() {
2402
3101
  const program = new Command();
2403
- program.name("codegraph").description("Live, queryable knowledge graph for your codebase").version("0.1.0").option("--verbose", "Print full stack traces on error").hook("preAction", (thisCommand) => {
3102
+ 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) => {
2404
3103
  const opts = thisCommand.optsWithGlobals();
2405
3104
  if (opts.verbose) process.env.CODEGRAPH_VERBOSE = "1";
2406
3105
  });
2407
3106
  program.on("--help", () => {
2408
3107
  printBanner();
2409
3108
  });
3109
+ program.command("init").description("Interactive setup wizard \u2014 LLM, credentials, index, MCP, client connect").action(async () => {
3110
+ await runInitCommand();
3111
+ });
2410
3112
  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) => {
2411
3113
  await runIndexCommand({ repoPath, noEmbed: opts.embed === false });
2412
3114
  });
@@ -2423,6 +3125,12 @@ function buildProgram() {
2423
3125
  program.command("doctor").description("Check environment, config, LLM credentials, and Kuzu DB health").action(async () => {
2424
3126
  await runDoctorCommand();
2425
3127
  });
3128
+ 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) => {
3129
+ await runWipeCommand({
3130
+ ...repoPath ? { repoPath } : {},
3131
+ yes: opts.yes === true
3132
+ });
3133
+ });
2426
3134
  const configCmd = program.command("config").description("Manage ~/.codegraph/config.json");
2427
3135
  configCmd.command("show").description("Print the resolved config").action(async () => {
2428
3136
  await runConfigShow();
@@ -2446,4 +3154,4 @@ export {
2446
3154
  renderError,
2447
3155
  buildProgram
2448
3156
  };
2449
- //# sourceMappingURL=chunk-AMJXGXLM.js.map
3157
+ //# sourceMappingURL=chunk-5WYXRWEY.js.map