@launchsecure/launch-kit 0.0.2 → 0.0.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.
@@ -6341,6 +6341,7 @@ function getTs() {
6341
6341
  }
6342
6342
  return tsModule;
6343
6343
  }
6344
+ var HTTP_METHODS = /* @__PURE__ */ new Set(["get", "post", "put", "patch", "delete", "head", "options"]);
6344
6345
  function parseFile(absPath) {
6345
6346
  const ts = getTs();
6346
6347
  const content = (0, import_node_fs.readFileSync)(absPath, "utf-8");
@@ -6363,6 +6364,8 @@ function parseFile(absPath) {
6363
6364
  const reExports = [];
6364
6365
  const jsxElements = /* @__PURE__ */ new Set();
6365
6366
  const navigations = [];
6367
+ const fetchCalls = [];
6368
+ const includeConcat = process.env.LAUNCH_CHART_INCLUDE_CONCAT_FETCHES === "1";
6366
6369
  function addExport(name2, kind) {
6367
6370
  if (!exportsSet.has(name2)) {
6368
6371
  exportsSet.add(name2);
@@ -6385,6 +6388,33 @@ function parseFile(absPath) {
6385
6388
  }
6386
6389
  return null;
6387
6390
  }
6391
+ function looksLikeUrl(s) {
6392
+ return s.startsWith("/") || /^(https?:)?\/\//i.test(s);
6393
+ }
6394
+ function templateStartsWithSlash(expr) {
6395
+ const head = expr.head.text;
6396
+ return head.startsWith("/");
6397
+ }
6398
+ function extractUrlFromFetchArg(arg) {
6399
+ if (ts.isStringLiteral(arg) || ts.isNoSubstitutionTemplateLiteral(arg)) {
6400
+ if (!looksLikeUrl(arg.text)) return null;
6401
+ return { url: arg.text, isTemplate: false };
6402
+ }
6403
+ if (ts.isTemplateExpression(arg)) {
6404
+ if (!templateStartsWithSlash(arg)) return null;
6405
+ return { url: arg.getText(sourceFile), isTemplate: true };
6406
+ }
6407
+ if (includeConcat && ts.isBinaryExpression(arg) && arg.operatorToken.kind === ts.SyntaxKind.PlusToken) {
6408
+ let leftmost = arg;
6409
+ while (ts.isBinaryExpression(leftmost) && leftmost.operatorToken.kind === ts.SyntaxKind.PlusToken) {
6410
+ leftmost = leftmost.left;
6411
+ }
6412
+ if ((ts.isStringLiteral(leftmost) || ts.isNoSubstitutionTemplateLiteral(leftmost)) && leftmost.text.startsWith("/")) {
6413
+ return { url: arg.getText(sourceFile), isTemplate: false, isConcat: true };
6414
+ }
6415
+ }
6416
+ return null;
6417
+ }
6388
6418
  function visit(node) {
6389
6419
  if (ts.isImportDeclaration(node)) {
6390
6420
  const moduleSpec = node.moduleSpecifier;
@@ -6408,6 +6438,8 @@ function parseFile(absPath) {
6408
6438
  }
6409
6439
  if (names.length > 0 || isTypeOnly) {
6410
6440
  imports.push({ names, specifier, isTypeOnly, typeNames });
6441
+ } else if (!clause) {
6442
+ imports.push({ names: [], specifier, isTypeOnly: false, typeNames: /* @__PURE__ */ new Set() });
6411
6443
  }
6412
6444
  }
6413
6445
  }
@@ -6421,6 +6453,19 @@ function parseFile(absPath) {
6421
6453
  reExports.push({ name: exportedName, from: fromSpec });
6422
6454
  }
6423
6455
  }
6456
+ } else if (!node.exportClause && fromSpec) {
6457
+ reExports.push({ name: "*", from: fromSpec, isWildcard: true });
6458
+ }
6459
+ }
6460
+ if (ts.isCallExpression(node) && node.expression.kind === ts.SyntaxKind.ImportKeyword) {
6461
+ const arg = node.arguments[0];
6462
+ if (arg && ts.isStringLiteral(arg)) {
6463
+ imports.push({
6464
+ names: [],
6465
+ specifier: arg.text,
6466
+ isTypeOnly: false,
6467
+ typeNames: /* @__PURE__ */ new Set()
6468
+ });
6424
6469
  }
6425
6470
  }
6426
6471
  if (ts.isExportAssignment(node) && !node.isExportEquals) {
@@ -6482,6 +6527,36 @@ function parseFile(absPath) {
6482
6527
  }
6483
6528
  }
6484
6529
  }
6530
+ if (ts.isCallExpression(node) && node.arguments.length > 0) {
6531
+ const expr = node.expression;
6532
+ const firstArg = node.arguments[0];
6533
+ if (ts.isIdentifier(expr) && expr.text === "fetch") {
6534
+ const extracted = extractUrlFromFetchArg(firstArg);
6535
+ if (extracted) {
6536
+ fetchCalls.push({
6537
+ url: extracted.url,
6538
+ isTemplate: extracted.isTemplate,
6539
+ ...extracted.isConcat ? { isConcat: true } : {},
6540
+ kind: "fetch"
6541
+ });
6542
+ }
6543
+ }
6544
+ if (ts.isPropertyAccessExpression(expr) && ts.isIdentifier(expr.name)) {
6545
+ const methodName = expr.name.text;
6546
+ if (HTTP_METHODS.has(methodName)) {
6547
+ const extracted = extractUrlFromFetchArg(firstArg);
6548
+ if (extracted) {
6549
+ fetchCalls.push({
6550
+ method: methodName.toUpperCase(),
6551
+ url: extracted.url,
6552
+ isTemplate: extracted.isTemplate,
6553
+ ...extracted.isConcat ? { isConcat: true } : {},
6554
+ kind: "client-method"
6555
+ });
6556
+ }
6557
+ }
6558
+ }
6559
+ }
6485
6560
  if (ts.isJsxOpeningElement(node) || ts.isJsxSelfClosingElement(node)) {
6486
6561
  const tagName = node.tagName;
6487
6562
  if (ts.isIdentifier(tagName) && tagName.text === "Link") {
@@ -6531,7 +6606,8 @@ function parseFile(absPath) {
6531
6606
  imports,
6532
6607
  reExports,
6533
6608
  jsxElements,
6534
- navigations
6609
+ navigations,
6610
+ fetchCalls
6535
6611
  };
6536
6612
  }
6537
6613
  var MUTATION_METHODS = /* @__PURE__ */ new Set([
@@ -6610,6 +6686,19 @@ function walk(dir, exts) {
6610
6686
  }
6611
6687
  return results;
6612
6688
  }
6689
+ function walkWithIgnore(dir, exts, ignoreDirs) {
6690
+ const results = [];
6691
+ if (!(0, import_node_fs2.existsSync)(dir)) return results;
6692
+ for (const entry of (0, import_node_fs2.readdirSync)(dir, { withFileTypes: true })) {
6693
+ if (entry.isDirectory()) {
6694
+ if (ignoreDirs.has(entry.name)) continue;
6695
+ results.push(...walkWithIgnore((0, import_node_path2.join)(dir, entry.name), exts, ignoreDirs));
6696
+ } else if (exts.includes((0, import_node_path2.extname)(entry.name))) {
6697
+ results.push((0, import_node_path2.join)(dir, entry.name));
6698
+ }
6699
+ }
6700
+ return results;
6701
+ }
6613
6702
  function toNodeId(srcDir, absPath) {
6614
6703
  return (0, import_node_path2.relative)(srcDir, absPath).replace(/\\/g, "/");
6615
6704
  }
@@ -6629,20 +6718,58 @@ function resolveRelativeImport(fromFile, specifier) {
6629
6718
  }
6630
6719
  return null;
6631
6720
  }
6721
+ function resolveBarrelMap(barrelAbsPath, parsedByPath, memo, visiting) {
6722
+ const cached = memo.get(barrelAbsPath);
6723
+ if (cached) return cached;
6724
+ if (visiting.has(barrelAbsPath)) return /* @__PURE__ */ new Map();
6725
+ visiting.add(barrelAbsPath);
6726
+ const parsed = parsedByPath.get(barrelAbsPath);
6727
+ const map = /* @__PURE__ */ new Map();
6728
+ if (!parsed) {
6729
+ visiting.delete(barrelAbsPath);
6730
+ memo.set(barrelAbsPath, map);
6731
+ return map;
6732
+ }
6733
+ for (const re of parsed.reExports) {
6734
+ if (!re.from.startsWith(".")) continue;
6735
+ const resolved = resolveRelativeImport(barrelAbsPath, re.from);
6736
+ if (!resolved) continue;
6737
+ if (re.isWildcard) {
6738
+ const targetBn = (0, import_node_path2.basename)(resolved);
6739
+ const targetIsBarrel = targetBn === "index.ts" || targetBn === "index.tsx";
6740
+ if (targetIsBarrel) {
6741
+ const nested = resolveBarrelMap(resolved, parsedByPath, memo, visiting);
6742
+ for (const [name, target] of nested) {
6743
+ if (!map.has(name)) map.set(name, target);
6744
+ }
6745
+ } else {
6746
+ const targetParsed = parsedByPath.get(resolved);
6747
+ if (targetParsed) {
6748
+ for (const exp of targetParsed.exports) {
6749
+ if (!map.has(exp)) map.set(exp, resolved);
6750
+ }
6751
+ }
6752
+ }
6753
+ } else {
6754
+ if (!map.has(re.name)) map.set(re.name, resolved);
6755
+ }
6756
+ }
6757
+ visiting.delete(barrelAbsPath);
6758
+ memo.set(barrelAbsPath, map);
6759
+ return map;
6760
+ }
6632
6761
  function buildAllBarrelMaps(srcDir, parsedByPath) {
6633
6762
  const barrels = /* @__PURE__ */ new Map();
6763
+ const memo = /* @__PURE__ */ new Map();
6634
6764
  for (const [absPath, parsed] of parsedByPath) {
6635
6765
  const bn = (0, import_node_path2.basename)(absPath);
6636
6766
  if (bn !== "index.ts" && bn !== "index.tsx") continue;
6637
6767
  if (parsed.reExports.length === 0) continue;
6638
- const barrelId = (0, import_node_path2.relative)(srcDir, (0, import_node_path2.dirname)(absPath)).replace(/\\/g, "/");
6639
- const map = /* @__PURE__ */ new Map();
6640
- for (const re of parsed.reExports) {
6641
- if (!re.from.startsWith(".")) continue;
6642
- const resolved = resolveRelativeImport(absPath, re.from);
6643
- if (resolved) map.set(re.name, resolved);
6768
+ const map = resolveBarrelMap(absPath, parsedByPath, memo, /* @__PURE__ */ new Set());
6769
+ if (map.size > 0) {
6770
+ const barrelId = (0, import_node_path2.relative)(srcDir, (0, import_node_path2.dirname)(absPath)).replace(/\\/g, "/");
6771
+ barrels.set(barrelId, map);
6644
6772
  }
6645
- if (map.size > 0) barrels.set(barrelId, map);
6646
6773
  }
6647
6774
  return barrels;
6648
6775
  }
@@ -6780,6 +6907,105 @@ function matchRouteToPage(route, routeToNodeId) {
6780
6907
  if (routeToNodeId.has(normalized)) return routeToNodeId.get(normalized);
6781
6908
  return null;
6782
6909
  }
6910
+ function loadApiRoutes(rootDir) {
6911
+ const apiJsonPath = (0, import_node_path2.join)(rootDir, ".launchsecure", "graphs", "api.json");
6912
+ if (!(0, import_node_fs2.existsSync)(apiJsonPath)) return [];
6913
+ try {
6914
+ const parsed = JSON.parse((0, import_node_fs2.readFileSync)(apiJsonPath, "utf-8"));
6915
+ const routes = [];
6916
+ for (const n of parsed.nodes ?? []) {
6917
+ const path9 = n.path;
6918
+ if (!path9 || typeof path9 !== "string") continue;
6919
+ routes.push({
6920
+ path: path9,
6921
+ nodeId: n.id,
6922
+ segments: path9.split("/").filter(Boolean)
6923
+ });
6924
+ }
6925
+ return routes;
6926
+ } catch {
6927
+ return [];
6928
+ }
6929
+ }
6930
+ function buildApiPathMap(routes) {
6931
+ const map = /* @__PURE__ */ new Map();
6932
+ for (const r of routes) {
6933
+ if (!map.has(r.path)) map.set(r.path, r.nodeId);
6934
+ }
6935
+ return map;
6936
+ }
6937
+ function normalizeFetchUrl(raw) {
6938
+ let s = raw.replace(/^`|`$/g, "");
6939
+ const qIdx = s.indexOf("?");
6940
+ if (qIdx >= 0) s = s.slice(0, qIdx);
6941
+ const hIdx = s.indexOf("#");
6942
+ if (hIdx >= 0) s = s.slice(0, hIdx);
6943
+ let hadInterpolation = false;
6944
+ s = s.replace(/\$\{([^}]+)\}/g, (_, expr) => {
6945
+ hadInterpolation = true;
6946
+ const cleaned = expr.trim();
6947
+ const last = cleaned.split(".").pop() ?? cleaned;
6948
+ const name = last.replace(/[^\w]/g, "") || "param";
6949
+ return ":" + name;
6950
+ });
6951
+ s = s.replace(/\/+/g, "/");
6952
+ if (s.length > 1 && s.endsWith("/")) s = s.slice(0, -1);
6953
+ return { path: s || "/", hadInterpolation };
6954
+ }
6955
+ function scoreApiRouteMatch(candidate, known) {
6956
+ if (candidate.length !== known.length) return -1;
6957
+ let score = 0;
6958
+ for (let i = 0; i < candidate.length; i++) {
6959
+ const a = candidate[i];
6960
+ const b = known[i];
6961
+ if (a === b) {
6962
+ score += 3;
6963
+ continue;
6964
+ }
6965
+ if (a.startsWith(":") && b.startsWith(":")) {
6966
+ score += 2;
6967
+ continue;
6968
+ }
6969
+ if (a.startsWith(":") || b.startsWith(":")) {
6970
+ score += 1;
6971
+ continue;
6972
+ }
6973
+ return -1;
6974
+ }
6975
+ return score;
6976
+ }
6977
+ function resolveFetchCall(call, apiPathMap, apiRoutes) {
6978
+ const raw = call.url;
6979
+ if (/^(https?:)?\/\//i.test(raw)) {
6980
+ return { kind: "external", normalizedUrl: raw };
6981
+ }
6982
+ if (call.isConcat) {
6983
+ return { kind: "dynamic", normalizedUrl: raw };
6984
+ }
6985
+ const { path: path9, hadInterpolation } = normalizeFetchUrl(raw);
6986
+ if (!path9.startsWith("/")) {
6987
+ return { kind: "unresolved", normalizedUrl: path9 };
6988
+ }
6989
+ const segs = path9.split("/").filter(Boolean);
6990
+ if (hadInterpolation && segs.length > 0 && segs[0].startsWith(":")) {
6991
+ return { kind: "dynamic", normalizedUrl: path9 };
6992
+ }
6993
+ const exact = apiPathMap.get(path9);
6994
+ if (exact) return { kind: "resolved", nodeId: exact, normalizedUrl: path9 };
6995
+ let bestScore = -1;
6996
+ let bestId = null;
6997
+ for (const r of apiRoutes) {
6998
+ const score = scoreApiRouteMatch(segs, r.segments);
6999
+ if (score > bestScore) {
7000
+ bestScore = score;
7001
+ bestId = r.nodeId;
7002
+ }
7003
+ }
7004
+ if (bestId && bestScore > 0) {
7005
+ return { kind: "resolved", nodeId: bestId, normalizedUrl: path9 };
7006
+ }
7007
+ return { kind: "unresolved", normalizedUrl: path9 };
7008
+ }
6783
7009
  function extractEdges(srcDir, absPath, sourceId, parsed, nodeIdSet, nodeTypeMap, barrelMaps, routeToNodeId) {
6784
7010
  const edges = [];
6785
7011
  const flagged = [];
@@ -6884,7 +7110,7 @@ function detect(rootDir) {
6884
7110
  function generate(rootDir) {
6885
7111
  const srcDir = (0, import_node_path2.join)(rootDir, "src");
6886
7112
  const appFiles = walk((0, import_node_path2.join)(srcDir, "app"), [".tsx", ".ts"]).filter(
6887
- (f) => f.endsWith("/page.tsx") || f.endsWith("/layout.tsx")
7113
+ (f) => (0, import_node_path2.basename)(f) !== "route.ts" && (0, import_node_path2.basename)(f) !== "route.tsx"
6888
7114
  );
6889
7115
  const clientFiles = walk((0, import_node_path2.join)(srcDir, "client"), [".tsx", ".ts"]);
6890
7116
  const serverFiles = walk((0, import_node_path2.join)(srcDir, "server"), [".ts", ".tsx"]).filter(
@@ -6917,6 +7143,7 @@ function generate(rootDir) {
6917
7143
  }
6918
7144
  const allEdges = [];
6919
7145
  const allFlagged = [];
7146
+ const crossRefs = [];
6920
7147
  for (const absPath of fileSet) {
6921
7148
  const sourceId = toNodeId(srcDir, absPath);
6922
7149
  const parsed = parsedByPath.get(absPath);
@@ -6933,6 +7160,139 @@ function generate(rootDir) {
6933
7160
  allEdges.push(...edges);
6934
7161
  allFlagged.push(...flagged);
6935
7162
  }
7163
+ const apiRoutes = loadApiRoutes(rootDir);
7164
+ const apiPathMap = buildApiPathMap(apiRoutes);
7165
+ const includeExternalFetches = process.env.LAUNCH_CHART_INCLUDE_EXTERNAL_FETCHES === "1";
7166
+ const fetchSeen = /* @__PURE__ */ new Set();
7167
+ let fetchResolvedCount = 0;
7168
+ let fetchDynamicCount = 0;
7169
+ let fetchUnresolvedCount = 0;
7170
+ let fetchExternalCount = 0;
7171
+ for (const absPath of fileSet) {
7172
+ const sourceId = toNodeId(srcDir, absPath);
7173
+ const parsed = parsedByPath.get(absPath);
7174
+ if (parsed.fetchCalls.length === 0) continue;
7175
+ for (const call of parsed.fetchCalls) {
7176
+ const result = resolveFetchCall(call, apiPathMap, apiRoutes);
7177
+ const methodTag = call.method ?? (call.kind === "fetch" ? "GET?" : "?");
7178
+ if (result.kind === "resolved" && result.nodeId) {
7179
+ const key = `${sourceId}\u2192${result.nodeId}\u2192calls_api`;
7180
+ if (fetchSeen.has(key)) continue;
7181
+ fetchSeen.add(key);
7182
+ crossRefs.push({
7183
+ source: sourceId,
7184
+ target: result.nodeId,
7185
+ type: "calls_api",
7186
+ layer: "api"
7187
+ });
7188
+ fetchResolvedCount++;
7189
+ continue;
7190
+ }
7191
+ if (result.kind === "dynamic") {
7192
+ fetchDynamicCount++;
7193
+ allFlagged.push({
7194
+ source: sourceId,
7195
+ target: "DYNAMIC",
7196
+ type: "calls_api",
7197
+ label: call.isConcat ? `${methodTag} fetch with concat: ${call.url}` : `${methodTag} fetch with template: ${call.url}`,
7198
+ confidence: call.isConcat ? "low" : "medium"
7199
+ });
7200
+ continue;
7201
+ }
7202
+ if (result.kind === "external") {
7203
+ fetchExternalCount++;
7204
+ if (!includeExternalFetches) continue;
7205
+ allFlagged.push({
7206
+ source: sourceId,
7207
+ target: "EXTERNAL",
7208
+ type: "calls_external",
7209
+ label: `${methodTag} external fetch: ${call.url}`,
7210
+ confidence: "high"
7211
+ });
7212
+ continue;
7213
+ }
7214
+ fetchUnresolvedCount++;
7215
+ allFlagged.push({
7216
+ source: sourceId,
7217
+ target: "UNRESOLVED",
7218
+ type: "calls_api",
7219
+ label: `${methodTag} fetch to unknown path: ${result.normalizedUrl}`,
7220
+ confidence: "medium"
7221
+ });
7222
+ }
7223
+ }
7224
+ const externalScanned = new Set(allDiscovered.map((f) => f.replace(/\\/g, "/")));
7225
+ const IGNORE_DIRS = /* @__PURE__ */ new Set([
7226
+ "node_modules",
7227
+ ".next",
7228
+ "dist",
7229
+ ".launchsecure",
7230
+ ".git",
7231
+ "src",
7232
+ "coverage",
7233
+ ".turbo",
7234
+ "build",
7235
+ "out",
7236
+ ".vercel"
7237
+ ]);
7238
+ const externalCandidates = walkWithIgnore(rootDir, [".ts", ".tsx"], IGNORE_DIRS);
7239
+ for (const absPath of externalCandidates) {
7240
+ const normalized = absPath.replace(/\\/g, "/");
7241
+ if (externalScanned.has(normalized)) continue;
7242
+ let parsed;
7243
+ try {
7244
+ parsed = parseFile(absPath);
7245
+ } catch {
7246
+ continue;
7247
+ }
7248
+ const externalId = (0, import_node_path2.relative)(rootDir, absPath).replace(/\\/g, "/");
7249
+ const edgesFromThis = [];
7250
+ const seen = /* @__PURE__ */ new Set();
7251
+ for (const imp of parsed.imports) {
7252
+ const { specifier, isTypeOnly, names } = imp;
7253
+ let resolved = null;
7254
+ if (specifier.startsWith("@/")) {
7255
+ const relToSrc = specifier.slice(2);
7256
+ const barrelMap = barrelMaps.get(relToSrc);
7257
+ if (barrelMap && names.length > 0) {
7258
+ for (const name of names) {
7259
+ const targetAbs = barrelMap.get(name);
7260
+ if (!targetAbs) continue;
7261
+ const targetId2 = toNodeId(srcDir, targetAbs);
7262
+ if (!nodeIdSet.has(targetId2)) continue;
7263
+ const key2 = `${externalId}\u2192${targetId2}`;
7264
+ if (seen.has(key2)) continue;
7265
+ seen.add(key2);
7266
+ edgesFromThis.push({ source: externalId, target: targetId2, type: "imports" });
7267
+ }
7268
+ continue;
7269
+ }
7270
+ resolved = resolveImport(srcDir, specifier);
7271
+ } else if (specifier.startsWith(".")) {
7272
+ resolved = resolveRelativeImport(absPath, specifier);
7273
+ }
7274
+ if (!resolved) continue;
7275
+ const targetId = toNodeId(srcDir, resolved);
7276
+ if (!nodeIdSet.has(targetId)) continue;
7277
+ if (targetId.endsWith("/index.ts") || targetId.endsWith("/index.tsx")) continue;
7278
+ const key = `${externalId}\u2192${targetId}\u2192${isTypeOnly ? "type" : "value"}`;
7279
+ if (seen.has(key)) continue;
7280
+ seen.add(key);
7281
+ edgesFromThis.push({ source: externalId, target: targetId, type: "imports" });
7282
+ }
7283
+ if (edgesFromThis.length === 0) continue;
7284
+ nodes.push({
7285
+ id: externalId,
7286
+ type: "external",
7287
+ name: parsed.name || nameFromFilename(absPath),
7288
+ route: null,
7289
+ module: "external",
7290
+ exports: parsed.exports
7291
+ });
7292
+ nodeIdSet.add(externalId);
7293
+ nodeTypeMap.set(externalId, "external");
7294
+ allEdges.push(...edgesFromThis);
7295
+ }
6936
7296
  const flaggedSet = /* @__PURE__ */ new Set();
6937
7297
  const dedupedFlagged = allFlagged.filter((f) => {
6938
7298
  const key = `${f.source}\u2192${f.target}\u2192${f.label}`;
@@ -6964,6 +7324,7 @@ function generate(rootDir) {
6964
7324
  total_configs: byType("config"),
6965
7325
  total_utils: byType("util"),
6966
7326
  total_libs: byType("lib"),
7327
+ total_external: byType("external"),
6967
7328
  total_edges: allEdges.length,
6968
7329
  total_flagged: dedupedFlagged.length
6969
7330
  };
@@ -6975,11 +7336,20 @@ function generate(rootDir) {
6975
7336
  layer: "ui",
6976
7337
  parser: "react-nextjs-ast",
6977
7338
  ...stats,
7339
+ api_call_detection: {
7340
+ includeExternalFetches,
7341
+ includeConcatFetches: process.env.LAUNCH_CHART_INCLUDE_CONCAT_FETCHES === "1",
7342
+ apiRoutesLoaded: apiRoutes.length,
7343
+ resolved: fetchResolvedCount,
7344
+ dynamic: fetchDynamicCount,
7345
+ unresolved: fetchUnresolvedCount,
7346
+ external: fetchExternalCount
7347
+ },
6978
7348
  notes: "Auto-generated via TypeScript AST \u2014 edges derived from actual imports, renders from JSX usage, navigations from router/Link calls."
6979
7349
  },
6980
7350
  nodes,
6981
7351
  edges: allEdges,
6982
- cross_refs: [],
7352
+ cross_refs: crossRefs,
6983
7353
  contradictions: [],
6984
7354
  warnings: [],
6985
7355
  flagged_edges: dedupedFlagged,
@@ -7004,7 +7374,7 @@ var reactNextjsParser = {
7004
7374
  // src/server/graph/parsers/api/nextjs-routes.ts
7005
7375
  var import_node_fs3 = require("node:fs");
7006
7376
  var import_node_path3 = require("node:path");
7007
- var HTTP_METHODS = /* @__PURE__ */ new Set(["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"]);
7377
+ var HTTP_METHODS2 = /* @__PURE__ */ new Set(["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"]);
7008
7378
  function walk2(dir) {
7009
7379
  const results = [];
7010
7380
  if (!(0, import_node_fs3.existsSync)(dir)) return results;
@@ -7048,7 +7418,7 @@ function generate2(rootDir) {
7048
7418
  const authWrappers = extractAuthWrappers(absPath);
7049
7419
  const methods = [];
7050
7420
  for (const exp of parsed.exports) {
7051
- if (HTTP_METHODS.has(exp)) methods.push(exp);
7421
+ if (HTTP_METHODS2.has(exp)) methods.push(exp);
7052
7422
  }
7053
7423
  const routePath = filePathToRoute(apiDir, absPath);
7054
7424
  const relPath = (0, import_node_path3.relative)(rootDir, absPath).replace(/\\/g, "/");
@@ -7118,7 +7488,7 @@ function generate2(rootDir) {
7118
7488
  flagged_edges: [],
7119
7489
  patterns: {
7120
7490
  total_endpoints: nodes.length,
7121
- methods_breakdown: [...HTTP_METHODS].reduce((acc, m) => {
7491
+ methods_breakdown: [...HTTP_METHODS2].reduce((acc, m) => {
7122
7492
  acc[m] = nodes.filter((n) => n.methods.includes(m)).length;
7123
7493
  return acc;
7124
7494
  }, {}),
@@ -7310,13 +7680,14 @@ function generateLayer(rootDir, layer) {
7310
7680
  };
7311
7681
  }
7312
7682
  function generateAll(rootDir) {
7313
- const layers = ["ui", "api", "db"];
7683
+ const layers = ["api", "db", "ui"];
7314
7684
  const results = [];
7315
7685
  for (const layer of layers) {
7316
7686
  const result = generateLayer(rootDir, layer);
7317
7687
  if (result) results.push(result);
7318
7688
  }
7319
- return results;
7689
+ const byLayer = new Map(results.map((r) => [r.layer, r]));
7690
+ return ["ui", "api", "db"].map((l) => byLayer.get(l)).filter((r) => !!r);
7320
7691
  }
7321
7692
 
7322
7693
  // src/server/graph/index.ts
@@ -7421,8 +7792,69 @@ function handleGraphCommand(subcommand, args) {
7421
7792
  }
7422
7793
 
7423
7794
  // src/server/graph-mcp.ts
7795
+ var import_node_fs7 = require("node:fs");
7796
+ var import_node_path7 = require("node:path");
7797
+
7798
+ // src/server/lockfile.ts
7799
+ var import_node_child_process = require("node:child_process");
7424
7800
  var import_node_fs6 = require("node:fs");
7801
+ var import_node_os = require("node:os");
7425
7802
  var import_node_path6 = require("node:path");
7803
+ function lockDir() {
7804
+ return (0, import_node_path6.join)((0, import_node_os.homedir)(), ".launchsecure");
7805
+ }
7806
+ function lockPath() {
7807
+ return (0, import_node_path6.join)(lockDir(), "launch-chart.lock");
7808
+ }
7809
+ function readLock() {
7810
+ const p = lockPath();
7811
+ if (!(0, import_node_fs6.existsSync)(p)) return null;
7812
+ try {
7813
+ const data = JSON.parse((0, import_node_fs6.readFileSync)(p, "utf-8"));
7814
+ if (typeof data.pid !== "number" || typeof data.port !== "number") return null;
7815
+ return data;
7816
+ } catch {
7817
+ return null;
7818
+ }
7819
+ }
7820
+ function isPidAlive(pid) {
7821
+ try {
7822
+ process.kill(pid, 0);
7823
+ return true;
7824
+ } catch {
7825
+ return false;
7826
+ }
7827
+ }
7828
+ function getListenerPid(port) {
7829
+ try {
7830
+ const out = (0, import_node_child_process.execFileSync)("lsof", ["-nP", "-iTCP:" + port, "-sTCP:LISTEN", "-t"], {
7831
+ encoding: "utf-8",
7832
+ stdio: ["ignore", "pipe", "ignore"],
7833
+ timeout: 500
7834
+ }).trim();
7835
+ if (!out) return null;
7836
+ const pid = parseInt(out.split("\n")[0], 10);
7837
+ return Number.isFinite(pid) ? pid : null;
7838
+ } catch {
7839
+ return null;
7840
+ }
7841
+ }
7842
+ function getLiveLock() {
7843
+ const lock = readLock();
7844
+ if (!lock) return null;
7845
+ const listenerPid = getListenerPid(lock.port);
7846
+ const live = listenerPid !== null ? listenerPid === lock.pid : isPidAlive(lock.pid);
7847
+ if (!live) {
7848
+ try {
7849
+ (0, import_node_fs6.unlinkSync)(lockPath());
7850
+ } catch {
7851
+ }
7852
+ return null;
7853
+ }
7854
+ return lock;
7855
+ }
7856
+
7857
+ // src/server/graph-mcp.ts
7426
7858
  var SERVER_INFO = {
7427
7859
  name: "launchsecure-graph",
7428
7860
  version: "0.0.1"
@@ -7444,7 +7876,7 @@ var TOOLS = [
7444
7876
  },
7445
7877
  {
7446
7878
  name: "read_graph",
7447
- description: 'Query the structural project graph \u2014 a smart Glob replacement that locates files by type/module/name and returns structural metadata (imports, renders, routes, relations). \n\nUSE THIS FOR: "where is X", "what files are in module Y", "what pages exist under /admin", "what components does Z render", "what tables relate to User", "list all hooks in auth module". \n\nDO NOT USE FOR: finding text/code content (use Grep), reading actual source code (use Read), understanding behavior/logic/patterns (graph has no code semantics \u2014 only names and edges). \n\nQUERY PARAMS (at least one required for node data \u2014 unfiltered calls return summary only to stay in context):\n- search: substring match on node id, name, or route\n- type: filter by node type (ui layer: page, layout, component, ui, hook, context, config, util; api layer: endpoint; db layer: table, enum)\n- module: filter by module (ui layer only: auth, admin, project, org, settings, integrations, shared-ui, layout, root)\n- node_id: return this node + its neighborhood (incoming+outgoing edges within `hops`)\n- hops: neighborhood radius when node_id is set (default 1)\n- minimal: return only id/type/name/module/route per node (skip heavy fields like columns, exports)\n\nBATCH MODE: pass `queries` (array of query objects) to run multiple independent queries in a single call. Each query object uses the same params (layer/search/type/module/node_id/hops/minimal). Returns { batch: true, count, results: [{index, query, result}, ...] }. Use this when you need multiple graph views up-front (e.g. scoping a feature across ui+api+db layers) to save round-trips. When batch mode is used, top-level params are ignored.\n\nReturns: filtered nodes + edges between them. If no filter given, returns per-layer counts and type breakdown only.',
7879
+ description: 'Query the structural project graph \u2014 a smart Glob replacement that locates files by type/module/name and returns structural metadata (imports, renders, routes, relations). \n\nUSE THIS FOR: "where is X", "what files are in module Y", "what pages exist under /admin", "what components does Z render", "what tables relate to User", "list all hooks in auth module". \n\nDO NOT USE FOR: finding text/code content (use Grep), reading actual source code (use Read), understanding behavior/logic/patterns (graph has no code semantics \u2014 only names and edges). \n\nQUERY PARAMS (at least one required for node data \u2014 unfiltered calls return summary only to stay in context):\n- search: substring match on node id, name, or route\n- type: filter by node type (ui layer: page, layout, component, ui, hook, context, config, util; api layer: endpoint; db layer: table, enum)\n- module: filter by module (ui layer only: auth, admin, project, org, settings, integrations, shared-ui, layout, root)\n- node_id: return this node + its neighborhood (incoming+outgoing edges within `hops`)\n- hops: neighborhood radius when node_id is set (default 1)\n- minimal: return only id/type/name/module/route per node (skip heavy fields like columns, exports)\n- include_edges: return the actual edge list. Default: TRUE for neighborhood queries (node_id), FALSE for filter queries (search/type/module). Filter responses always include `edge_count`; only pass include_edges:true when you actually need to inspect individual edges (e.g. "which components render X"). This default cuts typical filter responses in half.\n\nBATCH MODE: pass `queries` (array of query objects) to run multiple independent queries in a single call. Each query object uses the same params (layer/search/type/module/node_id/hops/minimal). Returns { batch: true, count, results: [{index, query, result}, ...] }. Use this when you need multiple graph views up-front (e.g. scoping a feature across ui+api+db layers) to save round-trips. When batch mode is used, top-level params are ignored.\n\nReturns: filtered nodes + edges between them. If no filter given, returns per-layer counts and type breakdown only.\n\nWIRE FORMAT (compact): responses that include nodes/edges use short keys and edge-by-index refs to cut payload ~40-60%. Every such response carries a `_schema` legend. Quick reference:\n nodes[]: { i: id, t: type, n: name, m: module, r: route, mt: methods, x: exports, c: columns }\n edges[]: { s: source_node_index, d: target_node_index, t: type, l: label }\nedges.s / edges.d are 0-based indices into THIS response\'s nodes array. If a referenced node is not in the response (boundary case), s/d may instead contain the full node id string \u2014 always check the type.\n\nBUDGET GUARDS:\n- Neighborhood queries stop expanding when the projected response exceeds budget. The response then contains `budget_exceeded: true` plus `hops_traversed < hops_requested`. When this happens, drill into a specific neighbor with another node_id call rather than retrying with larger hops \u2014 it will just truncate again.\n- Batch mode caps total response size. Once the budget is hit, later queries return `{skipped: true, reason: "batch_budget_exhausted"}` and you must re-run them individually.',
7448
7880
  inputSchema: {
7449
7881
  type: "object",
7450
7882
  properties: {
@@ -7475,11 +7907,15 @@ var TOOLS = [
7475
7907
  },
7476
7908
  minimal: {
7477
7909
  type: "boolean",
7478
- description: "Return minimal node fields only (id, type, name, module, route). Default false."
7910
+ description: "Return minimal node fields only (id, type, name, module, route). Default false, except db-layer filter queries default to true (db table nodes carry heavy `columns` arrays). Pass minimal:false on a db filter query if you want columns."
7911
+ },
7912
+ include_edges: {
7913
+ type: "boolean",
7914
+ description: "Include the edge list in the response. Default TRUE for neighborhood queries (node_id), FALSE for filter queries. Filter responses always include edge_count. Only set true on filter queries when you actually need edge data."
7479
7915
  },
7480
7916
  queries: {
7481
7917
  type: "array",
7482
- description: "Batch mode \u2014 array of query objects to run in a single call. Each uses the same param schema (layer/search/type/module/node_id/hops/minimal). When set, top-level params are ignored.",
7918
+ description: "Batch mode \u2014 array of query objects to run in a single call. Each uses the same param schema. When set, top-level params are ignored. Subject to an aggregate size budget \u2014 later queries may return a skipped stub.",
7483
7919
  items: {
7484
7920
  type: "object",
7485
7921
  properties: {
@@ -7489,7 +7925,8 @@ var TOOLS = [
7489
7925
  module: { type: "string" },
7490
7926
  node_id: { type: "string" },
7491
7927
  hops: { type: "number" },
7492
- minimal: { type: "boolean" }
7928
+ minimal: { type: "boolean" },
7929
+ include_edges: { type: "boolean" }
7493
7930
  }
7494
7931
  }
7495
7932
  }
@@ -7543,6 +7980,14 @@ Returns: { pattern, filter, files_searched, total_matches, matches: [{file, line
7543
7980
  },
7544
7981
  required: ["layer", "pattern"]
7545
7982
  }
7983
+ },
7984
+ {
7985
+ name: "get_graph_ui_url",
7986
+ description: 'Return the URL of a running launch-chart UI server if one exists. The UI is a visual, interactive view of the merged UI+API+DB project graph served by `launch-chart serve` (or auto-started via LAUNCH_CHART_AUTOSERVE=1). \n\nReturns: { running: boolean, url?: string, port?: number, pid?: number, startedAt?: string, cwd?: string }. If running is false, no server is currently live \u2014 suggest the user run `launch-chart serve` to start one. \n\nUse this when the user asks "open the graph", "show me the project graph UI", "where\'s the chart", etc.',
7987
+ inputSchema: {
7988
+ type: "object",
7989
+ properties: {}
7990
+ }
7546
7991
  }
7547
7992
  ];
7548
7993
  function matchesSearch(node, query) {
@@ -7556,30 +8001,134 @@ function matchesSearch(node, query) {
7556
8001
  function toMinimal(nodes) {
7557
8002
  return nodes.map((n) => {
7558
8003
  const out = { id: n.id, type: n.type, name: n.name };
7559
- if (n.module) out.module = n.module;
7560
- if (n.route) out.route = n.route;
7561
- if (n.methods) out.methods = n.methods;
8004
+ if (n.module != null) out.module = n.module;
8005
+ if (n.route != null) out.route = n.route;
8006
+ if (n.methods != null) out.methods = n.methods;
7562
8007
  return out;
7563
8008
  });
7564
8009
  }
7565
- function neighborhood(graph, centerId, hops) {
8010
+ var COMPACT_SCHEMA = {
8011
+ nodes: {
8012
+ i: "id",
8013
+ t: "type",
8014
+ n: "name",
8015
+ m: "module",
8016
+ r: "route",
8017
+ mt: "methods",
8018
+ x: "exports",
8019
+ c: "columns"
8020
+ },
8021
+ edges: {
8022
+ s: "source_node_index",
8023
+ d: "target_node_index",
8024
+ t: "type",
8025
+ l: "label"
8026
+ },
8027
+ note: "edges.s/d are 0-based indices into this response's nodes array. If a referenced node is outside the response (boundary case), s/d may contain the full node id string instead of an index."
8028
+ };
8029
+ var COMPACT_NODE_KNOWN_KEYS = /* @__PURE__ */ new Set([
8030
+ "id",
8031
+ "type",
8032
+ "name",
8033
+ "module",
8034
+ "route",
8035
+ "methods",
8036
+ "exports",
8037
+ "columns"
8038
+ ]);
8039
+ var EST_CHARS_PER_NODE_FULL = {
8040
+ ui: 300,
8041
+ api: 300,
8042
+ db: 3500
8043
+ };
8044
+ var EST_CHARS_PER_NODE_MIN = {
8045
+ ui: 150,
8046
+ api: 200,
8047
+ db: 120
8048
+ };
8049
+ var EST_CHARS_PER_EDGE = {
8050
+ ui: 65,
8051
+ api: 65,
8052
+ db: 65
8053
+ };
8054
+ var NEIGHBORHOOD_BUDGET_CHARS = 55e3;
8055
+ var BATCH_BUDGET_CHARS = 6e4;
8056
+ function toCompactNode(n) {
8057
+ const out = { i: n.id, t: n.type, n: n.name };
8058
+ if (n.module != null) out.m = n.module;
8059
+ if (n.route != null) out.r = n.route;
8060
+ if (n.methods != null) out.mt = n.methods;
8061
+ if (n.exports != null) out.x = n.exports;
8062
+ if (n.columns != null) out.c = n.columns;
8063
+ for (const k of Object.keys(n)) {
8064
+ if (!COMPACT_NODE_KNOWN_KEYS.has(k) && n[k] != null) out[k] = n[k];
8065
+ }
8066
+ return out;
8067
+ }
8068
+ function toCompactEdges(edges, idx) {
8069
+ return edges.map((e) => {
8070
+ const s = idx.get(e.source);
8071
+ const d = idx.get(e.target);
8072
+ const o = {
8073
+ s: s ?? e.source,
8074
+ d: d ?? e.target,
8075
+ t: e.type
8076
+ };
8077
+ if (e.label != null) o.l = e.label;
8078
+ return o;
8079
+ });
8080
+ }
8081
+ function compactResult(raw) {
8082
+ const nodes = raw.nodes;
8083
+ if (!nodes) return raw;
8084
+ const idx = /* @__PURE__ */ new Map();
8085
+ nodes.forEach((n, i) => idx.set(n.id, i));
8086
+ const compactNodes = nodes.map(toCompactNode);
8087
+ const edges = raw.edges;
8088
+ const compactEdges = edges ? toCompactEdges(edges, idx) : void 0;
8089
+ const out = { _schema: COMPACT_SCHEMA };
8090
+ for (const [k, v] of Object.entries(raw)) {
8091
+ if (k === "nodes" || k === "edges") continue;
8092
+ out[k] = v;
8093
+ }
8094
+ out.nodes = compactNodes;
8095
+ if (compactEdges !== void 0) out.edges = compactEdges;
8096
+ return out;
8097
+ }
8098
+ function neighborhood(graph, centerId, hops, layer, minimal) {
7566
8099
  const center = graph.nodes.find((n) => n.id === centerId);
7567
8100
  if (!center) return null;
7568
8101
  const visited = /* @__PURE__ */ new Set([centerId]);
7569
8102
  let frontier = /* @__PURE__ */ new Set([centerId]);
8103
+ let budgetExceeded = false;
8104
+ let stoppedAtHop = 0;
7570
8105
  for (let h = 0; h < hops; h++) {
7571
8106
  const next = /* @__PURE__ */ new Set();
7572
8107
  for (const edge of graph.edges) {
7573
8108
  if (frontier.has(edge.source) && !visited.has(edge.target)) next.add(edge.target);
7574
8109
  if (frontier.has(edge.target) && !visited.has(edge.source)) next.add(edge.source);
7575
8110
  }
8111
+ const projectedVisited = visited.size + next.size;
8112
+ let projectedEdges = 0;
8113
+ for (const e of graph.edges) {
8114
+ const srcIn = visited.has(e.source) || next.has(e.source);
8115
+ const dstIn = visited.has(e.target) || next.has(e.target);
8116
+ if (srcIn && dstIn) projectedEdges++;
8117
+ }
8118
+ const perNode = minimal ? EST_CHARS_PER_NODE_MIN[layer] : EST_CHARS_PER_NODE_FULL[layer];
8119
+ const projectedChars = projectedVisited * perNode + projectedEdges * EST_CHARS_PER_EDGE[layer];
8120
+ if (projectedChars > NEIGHBORHOOD_BUDGET_CHARS) {
8121
+ budgetExceeded = true;
8122
+ break;
8123
+ }
7576
8124
  for (const id of next) visited.add(id);
7577
8125
  frontier = next;
8126
+ stoppedAtHop = h + 1;
7578
8127
  if (frontier.size === 0) break;
7579
8128
  }
7580
8129
  const nodes = graph.nodes.filter((n) => visited.has(n.id));
7581
8130
  const edges = graph.edges.filter((e) => visited.has(e.source) && visited.has(e.target));
7582
- return { nodes, edges };
8131
+ return { nodes, edges, budgetExceeded, stoppedAtHop };
7583
8132
  }
7584
8133
  function layerSummary(graph) {
7585
8134
  const typeCounts = {};
@@ -7638,14 +8187,16 @@ Output: .launchsecure/graphs/
7638
8187
  Use read_graph with filters (search/type/module/node_id) to query.`
7639
8188
  );
7640
8189
  }
7641
- function runReadGraphQuery(rootDir, args) {
8190
+ function runReadGraphQueryRaw(rootDir, args) {
7642
8191
  const layer = args.layer;
7643
8192
  const search = args.search;
7644
8193
  const type = args.type;
7645
8194
  const module_ = args.module;
7646
8195
  const nodeId = args.node_id;
7647
8196
  const hops = args.hops ?? 1;
7648
- const minimal = args.minimal ?? false;
8197
+ const layerIsDb = args.layer === "db";
8198
+ const minimal = args.minimal ?? layerIsDb;
8199
+ const includeEdges = args.include_edges;
7649
8200
  const hasFilter = !!(search || type || module_ || nodeId);
7650
8201
  if (layer && !["ui", "api", "db"].includes(layer)) {
7651
8202
  return { error: `Invalid layer "${layer}". Must be one of: ui, api, db` };
@@ -7671,19 +8222,26 @@ function runReadGraphQuery(rootDir, args) {
7671
8222
  return { error: `No ${layer} graph found at .launchsecure/graphs/${layer}.json. Run generate_graph first.` };
7672
8223
  }
7673
8224
  if (nodeId) {
7674
- const nb = neighborhood(graph, nodeId, hops);
8225
+ const nb = neighborhood(graph, nodeId, hops, layer, minimal);
7675
8226
  if (!nb) {
7676
8227
  return { error: `Node "${nodeId}" not found in ${layer} graph. Try read_graph with search="${nodeId}" to find similar nodes.` };
7677
8228
  }
7678
- return {
8229
+ const wantEdges2 = includeEdges ?? true;
8230
+ const result2 = {
7679
8231
  layer,
7680
8232
  center: nodeId,
7681
- hops,
8233
+ hops_requested: hops,
8234
+ hops_traversed: nb.stoppedAtHop,
7682
8235
  node_count: nb.nodes.length,
7683
8236
  edge_count: nb.edges.length,
7684
- nodes: minimal ? toMinimal(nb.nodes) : nb.nodes,
7685
- edges: nb.edges
8237
+ nodes: minimal ? toMinimal(nb.nodes) : nb.nodes
7686
8238
  };
8239
+ if (wantEdges2) result2.edges = nb.edges;
8240
+ if (nb.budgetExceeded) {
8241
+ result2.budget_exceeded = true;
8242
+ result2.hint = `Neighborhood truncated at hop ${nb.stoppedAtHop} (projected size exceeded budget). To explore further, call read_graph with node_id set to a specific neighbor from the returned nodes, or rerun with hops=${Math.max(1, nb.stoppedAtHop)} to confirm the partial view is what you wanted.`;
8243
+ }
8244
+ return result2;
7687
8245
  }
7688
8246
  if (!hasFilter) {
7689
8247
  return {
@@ -7708,14 +8266,24 @@ function runReadGraphQuery(rootDir, args) {
7708
8266
  hint: "No nodes matched. Check spelling, or call read_graph without filter to see the summary and available types/modules."
7709
8267
  };
7710
8268
  }
7711
- return {
8269
+ const wantEdges = includeEdges ?? false;
8270
+ const result = {
7712
8271
  layer,
7713
8272
  filter: { search, type, module: module_ },
7714
8273
  matched: matched.length,
7715
8274
  edge_count: matchedEdges.length,
7716
- nodes: minimal ? toMinimal(matched) : matched,
7717
- edges: matchedEdges
8275
+ nodes: minimal ? toMinimal(matched) : matched
7718
8276
  };
8277
+ if (wantEdges) {
8278
+ result.edges = matchedEdges;
8279
+ } else {
8280
+ result.edges_hint = `${matchedEdges.length} edges between matched nodes omitted. Pass include_edges:true to retrieve them (only do this when you actually need edge data).`;
8281
+ }
8282
+ return result;
8283
+ }
8284
+ function runReadGraphQuery(rootDir, args) {
8285
+ const raw = runReadGraphQueryRaw(rootDir, args);
8286
+ return compactResult(raw);
7719
8287
  }
7720
8288
  function handleReadGraph(args) {
7721
8289
  const rootDir = process.cwd();
@@ -7724,16 +8292,57 @@ function handleReadGraph(args) {
7724
8292
  if (queries.length === 0) {
7725
8293
  return err("queries array is empty. Provide at least one query object.");
7726
8294
  }
7727
- const results = queries.map((q, i) => ({ index: i, query: q, result: runReadGraphQuery(rootDir, q) }));
7728
- return okJson({ batch: true, count: results.length, results });
8295
+ const results = [];
8296
+ let cumulativeChars = 0;
8297
+ let budgetHit = false;
8298
+ for (let i = 0; i < queries.length; i++) {
8299
+ const q = queries[i];
8300
+ if (budgetHit) {
8301
+ results.push({
8302
+ index: i,
8303
+ query: q,
8304
+ result: {
8305
+ skipped: true,
8306
+ reason: "batch_budget_exhausted",
8307
+ hint: "Previous sub-results filled the batch budget. Re-run this query on its own."
8308
+ }
8309
+ });
8310
+ continue;
8311
+ }
8312
+ const r = runReadGraphQuery(rootDir, q);
8313
+ const entry = { index: i, query: q, result: r };
8314
+ const entrySize = JSON.stringify(entry, null, 2).length;
8315
+ if (cumulativeChars + entrySize > BATCH_BUDGET_CHARS && results.length > 0) {
8316
+ budgetHit = true;
8317
+ results.push({
8318
+ index: i,
8319
+ query: q,
8320
+ result: {
8321
+ skipped: true,
8322
+ reason: "batch_budget_exhausted",
8323
+ hint: "This sub-query would push the batch over its size budget. Re-run it on its own."
8324
+ }
8325
+ });
8326
+ } else {
8327
+ results.push(entry);
8328
+ cumulativeChars += entrySize;
8329
+ }
8330
+ }
8331
+ return okJson({
8332
+ batch: true,
8333
+ count: results.length,
8334
+ cumulative_chars: cumulativeChars,
8335
+ budget_hit: budgetHit,
8336
+ results
8337
+ });
7729
8338
  }
7730
8339
  const result = runReadGraphQuery(rootDir, args);
7731
8340
  return okJson(result);
7732
8341
  }
7733
8342
  function nodeToFilePath(rootDir, layer, nodeId) {
7734
- if (layer === "ui") return (0, import_node_path6.join)(rootDir, "src", nodeId);
7735
- if (layer === "api") return (0, import_node_path6.join)(rootDir, nodeId);
7736
- if (layer === "db") return (0, import_node_path6.join)(rootDir, "prisma", "schema.prisma");
8343
+ if (layer === "ui") return (0, import_node_path7.join)(rootDir, "src", nodeId);
8344
+ if (layer === "api") return (0, import_node_path7.join)(rootDir, nodeId);
8345
+ if (layer === "db") return (0, import_node_path7.join)(rootDir, "prisma", "schema.prisma");
7737
8346
  return null;
7738
8347
  }
7739
8348
  function handleGrepNodes(args) {
@@ -7755,7 +8364,7 @@ function handleGrepNodes(args) {
7755
8364
  } catch (e) {
7756
8365
  return err(`Invalid regex: ${e.message}`);
7757
8366
  }
7758
- const queryResult = runReadGraphQuery(rootDir, {
8367
+ const queryResult = runReadGraphQueryRaw(rootDir, {
7759
8368
  layer,
7760
8369
  search: args.search,
7761
8370
  type: args.type,
@@ -7793,11 +8402,11 @@ function handleGrepNodes(args) {
7793
8402
  let filesSearched = 0;
7794
8403
  let truncated = false;
7795
8404
  for (const [filePath, nodeId] of filePaths) {
7796
- if (!(0, import_node_fs6.existsSync)(filePath)) continue;
8405
+ if (!(0, import_node_fs7.existsSync)(filePath)) continue;
7797
8406
  filesSearched++;
7798
8407
  let content;
7799
8408
  try {
7800
- content = (0, import_node_fs6.readFileSync)(filePath, "utf-8");
8409
+ content = (0, import_node_fs7.readFileSync)(filePath, "utf-8");
7801
8410
  } catch {
7802
8411
  continue;
7803
8412
  }
@@ -7834,6 +8443,23 @@ function handleGrepNodes(args) {
7834
8443
  truncated
7835
8444
  });
7836
8445
  }
8446
+ function handleGetGraphUiUrl() {
8447
+ const lock = getLiveLock();
8448
+ if (!lock) {
8449
+ return okJson({
8450
+ running: false,
8451
+ hint: "No launch-chart UI server is currently running. Start one with `launch-chart serve`, or set LAUNCH_CHART_AUTOSERVE=1 in your MCP config to auto-start it alongside the MCP server."
8452
+ });
8453
+ }
8454
+ return okJson({
8455
+ running: true,
8456
+ url: lock.url,
8457
+ port: lock.port,
8458
+ pid: lock.pid,
8459
+ cwd: lock.cwd,
8460
+ startedAt: lock.startedAt
8461
+ });
8462
+ }
7837
8463
  function send(msg) {
7838
8464
  process.stdout.write(JSON.stringify(msg) + "\n");
7839
8465
  }
@@ -7877,6 +8503,10 @@ function handleMessage(msg) {
7877
8503
  respond(id ?? null, handleGrepNodes(args));
7878
8504
  return;
7879
8505
  }
8506
+ if (toolName === "get_graph_ui_url") {
8507
+ respond(id ?? null, handleGetGraphUiUrl());
8508
+ return;
8509
+ }
7880
8510
  respondError(id ?? null, -32601, `Unknown tool: ${toolName}`);
7881
8511
  return;
7882
8512
  }