@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.
- package/dist/chart-client/assets/index-BPR5akxH.js +323 -0
- package/dist/chart-client/assets/index-DhNl1aFF.css +1 -0
- package/dist/chart-client/index.html +21 -0
- package/dist/client/assets/{index-CcHIoRl6.js → index-D9e81rsq.js} +68 -63
- package/dist/client/assets/index-nR-HgoHH.css +32 -0
- package/dist/client/index.html +2 -2
- package/dist/server/chart-serve.js +1764 -0
- package/dist/server/cli.js +672 -42
- package/dist/server/graph-mcp-entry.js +1281 -256
- package/package.json +7 -3
- package/dist/client/assets/index-C8GAsRGO.css +0 -32
package/dist/server/cli.js
CHANGED
|
@@ -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
|
|
6639
|
-
|
|
6640
|
-
|
|
6641
|
-
|
|
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) =>
|
|
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
|
|
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 (
|
|
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: [...
|
|
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 = ["
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
7728
|
-
|
|
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,
|
|
7735
|
-
if (layer === "api") return (0,
|
|
7736
|
-
if (layer === "db") return (0,
|
|
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 =
|
|
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,
|
|
8405
|
+
if (!(0, import_node_fs7.existsSync)(filePath)) continue;
|
|
7797
8406
|
filesSearched++;
|
|
7798
8407
|
let content;
|
|
7799
8408
|
try {
|
|
7800
|
-
content = (0,
|
|
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
|
}
|