@launchsecure/launch-kit 0.0.1 → 0.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/server/cli.js +199 -22
- package/dist/server/graph-mcp-entry.js +191 -22
- package/package.json +1 -1
package/dist/server/cli.js
CHANGED
|
@@ -3828,6 +3828,10 @@ function writeClaudeConfig(projectDir, mcpUrl, token) {
|
|
|
3828
3828
|
headers: {
|
|
3829
3829
|
Authorization: `Bearer ${token}`
|
|
3830
3830
|
}
|
|
3831
|
+
},
|
|
3832
|
+
"launch-chart": {
|
|
3833
|
+
command: "launch-chart",
|
|
3834
|
+
args: []
|
|
3831
3835
|
}
|
|
3832
3836
|
}
|
|
3833
3837
|
};
|
|
@@ -3845,6 +3849,10 @@ function writeCodexConfig(projectDir, mcpUrl, token) {
|
|
|
3845
3849
|
`[mcp_servers.launch-pod]`,
|
|
3846
3850
|
`url = "${mcpUrl}"`,
|
|
3847
3851
|
`http_headers = { "Authorization" = "Bearer ${token}" }`,
|
|
3852
|
+
``,
|
|
3853
|
+
`[mcp_servers.launch-chart]`,
|
|
3854
|
+
`command = "launch-chart"`,
|
|
3855
|
+
`args = []`,
|
|
3848
3856
|
``
|
|
3849
3857
|
].join("\n");
|
|
3850
3858
|
const filePath = import_path.default.join(codexDir, "config.toml");
|
|
@@ -7436,7 +7444,7 @@ var TOOLS = [
|
|
|
7436
7444
|
},
|
|
7437
7445
|
{
|
|
7438
7446
|
name: "read_graph",
|
|
7439
|
-
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.',
|
|
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- 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.',
|
|
7440
7448
|
inputSchema: {
|
|
7441
7449
|
type: "object",
|
|
7442
7450
|
properties: {
|
|
@@ -7467,11 +7475,15 @@ var TOOLS = [
|
|
|
7467
7475
|
},
|
|
7468
7476
|
minimal: {
|
|
7469
7477
|
type: "boolean",
|
|
7470
|
-
description: "Return minimal node fields only (id, type, name, module, route). Default false."
|
|
7478
|
+
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."
|
|
7479
|
+
},
|
|
7480
|
+
include_edges: {
|
|
7481
|
+
type: "boolean",
|
|
7482
|
+
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."
|
|
7471
7483
|
},
|
|
7472
7484
|
queries: {
|
|
7473
7485
|
type: "array",
|
|
7474
|
-
description: "Batch mode \u2014 array of query objects to run in a single call. Each uses the same param schema
|
|
7486
|
+
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.",
|
|
7475
7487
|
items: {
|
|
7476
7488
|
type: "object",
|
|
7477
7489
|
properties: {
|
|
@@ -7481,7 +7493,8 @@ var TOOLS = [
|
|
|
7481
7493
|
module: { type: "string" },
|
|
7482
7494
|
node_id: { type: "string" },
|
|
7483
7495
|
hops: { type: "number" },
|
|
7484
|
-
minimal: { type: "boolean" }
|
|
7496
|
+
minimal: { type: "boolean" },
|
|
7497
|
+
include_edges: { type: "boolean" }
|
|
7485
7498
|
}
|
|
7486
7499
|
}
|
|
7487
7500
|
}
|
|
@@ -7548,30 +7561,134 @@ function matchesSearch(node, query) {
|
|
|
7548
7561
|
function toMinimal(nodes) {
|
|
7549
7562
|
return nodes.map((n) => {
|
|
7550
7563
|
const out = { id: n.id, type: n.type, name: n.name };
|
|
7551
|
-
if (n.module) out.module = n.module;
|
|
7552
|
-
if (n.route) out.route = n.route;
|
|
7553
|
-
if (n.methods) out.methods = n.methods;
|
|
7564
|
+
if (n.module != null) out.module = n.module;
|
|
7565
|
+
if (n.route != null) out.route = n.route;
|
|
7566
|
+
if (n.methods != null) out.methods = n.methods;
|
|
7554
7567
|
return out;
|
|
7555
7568
|
});
|
|
7556
7569
|
}
|
|
7557
|
-
|
|
7570
|
+
var COMPACT_SCHEMA = {
|
|
7571
|
+
nodes: {
|
|
7572
|
+
i: "id",
|
|
7573
|
+
t: "type",
|
|
7574
|
+
n: "name",
|
|
7575
|
+
m: "module",
|
|
7576
|
+
r: "route",
|
|
7577
|
+
mt: "methods",
|
|
7578
|
+
x: "exports",
|
|
7579
|
+
c: "columns"
|
|
7580
|
+
},
|
|
7581
|
+
edges: {
|
|
7582
|
+
s: "source_node_index",
|
|
7583
|
+
d: "target_node_index",
|
|
7584
|
+
t: "type",
|
|
7585
|
+
l: "label"
|
|
7586
|
+
},
|
|
7587
|
+
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."
|
|
7588
|
+
};
|
|
7589
|
+
var COMPACT_NODE_KNOWN_KEYS = /* @__PURE__ */ new Set([
|
|
7590
|
+
"id",
|
|
7591
|
+
"type",
|
|
7592
|
+
"name",
|
|
7593
|
+
"module",
|
|
7594
|
+
"route",
|
|
7595
|
+
"methods",
|
|
7596
|
+
"exports",
|
|
7597
|
+
"columns"
|
|
7598
|
+
]);
|
|
7599
|
+
var EST_CHARS_PER_NODE_FULL = {
|
|
7600
|
+
ui: 300,
|
|
7601
|
+
api: 300,
|
|
7602
|
+
db: 3500
|
|
7603
|
+
};
|
|
7604
|
+
var EST_CHARS_PER_NODE_MIN = {
|
|
7605
|
+
ui: 150,
|
|
7606
|
+
api: 200,
|
|
7607
|
+
db: 120
|
|
7608
|
+
};
|
|
7609
|
+
var EST_CHARS_PER_EDGE = {
|
|
7610
|
+
ui: 65,
|
|
7611
|
+
api: 65,
|
|
7612
|
+
db: 65
|
|
7613
|
+
};
|
|
7614
|
+
var NEIGHBORHOOD_BUDGET_CHARS = 55e3;
|
|
7615
|
+
var BATCH_BUDGET_CHARS = 6e4;
|
|
7616
|
+
function toCompactNode(n) {
|
|
7617
|
+
const out = { i: n.id, t: n.type, n: n.name };
|
|
7618
|
+
if (n.module != null) out.m = n.module;
|
|
7619
|
+
if (n.route != null) out.r = n.route;
|
|
7620
|
+
if (n.methods != null) out.mt = n.methods;
|
|
7621
|
+
if (n.exports != null) out.x = n.exports;
|
|
7622
|
+
if (n.columns != null) out.c = n.columns;
|
|
7623
|
+
for (const k of Object.keys(n)) {
|
|
7624
|
+
if (!COMPACT_NODE_KNOWN_KEYS.has(k) && n[k] != null) out[k] = n[k];
|
|
7625
|
+
}
|
|
7626
|
+
return out;
|
|
7627
|
+
}
|
|
7628
|
+
function toCompactEdges(edges, idx) {
|
|
7629
|
+
return edges.map((e) => {
|
|
7630
|
+
const s = idx.get(e.source);
|
|
7631
|
+
const d = idx.get(e.target);
|
|
7632
|
+
const o = {
|
|
7633
|
+
s: s ?? e.source,
|
|
7634
|
+
d: d ?? e.target,
|
|
7635
|
+
t: e.type
|
|
7636
|
+
};
|
|
7637
|
+
if (e.label != null) o.l = e.label;
|
|
7638
|
+
return o;
|
|
7639
|
+
});
|
|
7640
|
+
}
|
|
7641
|
+
function compactResult(raw) {
|
|
7642
|
+
const nodes = raw.nodes;
|
|
7643
|
+
if (!nodes) return raw;
|
|
7644
|
+
const idx = /* @__PURE__ */ new Map();
|
|
7645
|
+
nodes.forEach((n, i) => idx.set(n.id, i));
|
|
7646
|
+
const compactNodes = nodes.map(toCompactNode);
|
|
7647
|
+
const edges = raw.edges;
|
|
7648
|
+
const compactEdges = edges ? toCompactEdges(edges, idx) : void 0;
|
|
7649
|
+
const out = { _schema: COMPACT_SCHEMA };
|
|
7650
|
+
for (const [k, v] of Object.entries(raw)) {
|
|
7651
|
+
if (k === "nodes" || k === "edges") continue;
|
|
7652
|
+
out[k] = v;
|
|
7653
|
+
}
|
|
7654
|
+
out.nodes = compactNodes;
|
|
7655
|
+
if (compactEdges !== void 0) out.edges = compactEdges;
|
|
7656
|
+
return out;
|
|
7657
|
+
}
|
|
7658
|
+
function neighborhood(graph, centerId, hops, layer, minimal) {
|
|
7558
7659
|
const center = graph.nodes.find((n) => n.id === centerId);
|
|
7559
7660
|
if (!center) return null;
|
|
7560
7661
|
const visited = /* @__PURE__ */ new Set([centerId]);
|
|
7561
7662
|
let frontier = /* @__PURE__ */ new Set([centerId]);
|
|
7663
|
+
let budgetExceeded = false;
|
|
7664
|
+
let stoppedAtHop = 0;
|
|
7562
7665
|
for (let h = 0; h < hops; h++) {
|
|
7563
7666
|
const next = /* @__PURE__ */ new Set();
|
|
7564
7667
|
for (const edge of graph.edges) {
|
|
7565
7668
|
if (frontier.has(edge.source) && !visited.has(edge.target)) next.add(edge.target);
|
|
7566
7669
|
if (frontier.has(edge.target) && !visited.has(edge.source)) next.add(edge.source);
|
|
7567
7670
|
}
|
|
7671
|
+
const projectedVisited = visited.size + next.size;
|
|
7672
|
+
let projectedEdges = 0;
|
|
7673
|
+
for (const e of graph.edges) {
|
|
7674
|
+
const srcIn = visited.has(e.source) || next.has(e.source);
|
|
7675
|
+
const dstIn = visited.has(e.target) || next.has(e.target);
|
|
7676
|
+
if (srcIn && dstIn) projectedEdges++;
|
|
7677
|
+
}
|
|
7678
|
+
const perNode = minimal ? EST_CHARS_PER_NODE_MIN[layer] : EST_CHARS_PER_NODE_FULL[layer];
|
|
7679
|
+
const projectedChars = projectedVisited * perNode + projectedEdges * EST_CHARS_PER_EDGE[layer];
|
|
7680
|
+
if (projectedChars > NEIGHBORHOOD_BUDGET_CHARS) {
|
|
7681
|
+
budgetExceeded = true;
|
|
7682
|
+
break;
|
|
7683
|
+
}
|
|
7568
7684
|
for (const id of next) visited.add(id);
|
|
7569
7685
|
frontier = next;
|
|
7686
|
+
stoppedAtHop = h + 1;
|
|
7570
7687
|
if (frontier.size === 0) break;
|
|
7571
7688
|
}
|
|
7572
7689
|
const nodes = graph.nodes.filter((n) => visited.has(n.id));
|
|
7573
7690
|
const edges = graph.edges.filter((e) => visited.has(e.source) && visited.has(e.target));
|
|
7574
|
-
return { nodes, edges };
|
|
7691
|
+
return { nodes, edges, budgetExceeded, stoppedAtHop };
|
|
7575
7692
|
}
|
|
7576
7693
|
function layerSummary(graph) {
|
|
7577
7694
|
const typeCounts = {};
|
|
@@ -7630,14 +7747,16 @@ Output: .launchsecure/graphs/
|
|
|
7630
7747
|
Use read_graph with filters (search/type/module/node_id) to query.`
|
|
7631
7748
|
);
|
|
7632
7749
|
}
|
|
7633
|
-
function
|
|
7750
|
+
function runReadGraphQueryRaw(rootDir, args) {
|
|
7634
7751
|
const layer = args.layer;
|
|
7635
7752
|
const search = args.search;
|
|
7636
7753
|
const type = args.type;
|
|
7637
7754
|
const module_ = args.module;
|
|
7638
7755
|
const nodeId = args.node_id;
|
|
7639
7756
|
const hops = args.hops ?? 1;
|
|
7640
|
-
const
|
|
7757
|
+
const layerIsDb = args.layer === "db";
|
|
7758
|
+
const minimal = args.minimal ?? layerIsDb;
|
|
7759
|
+
const includeEdges = args.include_edges;
|
|
7641
7760
|
const hasFilter = !!(search || type || module_ || nodeId);
|
|
7642
7761
|
if (layer && !["ui", "api", "db"].includes(layer)) {
|
|
7643
7762
|
return { error: `Invalid layer "${layer}". Must be one of: ui, api, db` };
|
|
@@ -7663,19 +7782,26 @@ function runReadGraphQuery(rootDir, args) {
|
|
|
7663
7782
|
return { error: `No ${layer} graph found at .launchsecure/graphs/${layer}.json. Run generate_graph first.` };
|
|
7664
7783
|
}
|
|
7665
7784
|
if (nodeId) {
|
|
7666
|
-
const nb = neighborhood(graph, nodeId, hops);
|
|
7785
|
+
const nb = neighborhood(graph, nodeId, hops, layer, minimal);
|
|
7667
7786
|
if (!nb) {
|
|
7668
7787
|
return { error: `Node "${nodeId}" not found in ${layer} graph. Try read_graph with search="${nodeId}" to find similar nodes.` };
|
|
7669
7788
|
}
|
|
7670
|
-
|
|
7789
|
+
const wantEdges2 = includeEdges ?? true;
|
|
7790
|
+
const result2 = {
|
|
7671
7791
|
layer,
|
|
7672
7792
|
center: nodeId,
|
|
7673
|
-
hops,
|
|
7793
|
+
hops_requested: hops,
|
|
7794
|
+
hops_traversed: nb.stoppedAtHop,
|
|
7674
7795
|
node_count: nb.nodes.length,
|
|
7675
7796
|
edge_count: nb.edges.length,
|
|
7676
|
-
nodes: minimal ? toMinimal(nb.nodes) : nb.nodes
|
|
7677
|
-
edges: nb.edges
|
|
7797
|
+
nodes: minimal ? toMinimal(nb.nodes) : nb.nodes
|
|
7678
7798
|
};
|
|
7799
|
+
if (wantEdges2) result2.edges = nb.edges;
|
|
7800
|
+
if (nb.budgetExceeded) {
|
|
7801
|
+
result2.budget_exceeded = true;
|
|
7802
|
+
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.`;
|
|
7803
|
+
}
|
|
7804
|
+
return result2;
|
|
7679
7805
|
}
|
|
7680
7806
|
if (!hasFilter) {
|
|
7681
7807
|
return {
|
|
@@ -7700,14 +7826,24 @@ function runReadGraphQuery(rootDir, args) {
|
|
|
7700
7826
|
hint: "No nodes matched. Check spelling, or call read_graph without filter to see the summary and available types/modules."
|
|
7701
7827
|
};
|
|
7702
7828
|
}
|
|
7703
|
-
|
|
7829
|
+
const wantEdges = includeEdges ?? false;
|
|
7830
|
+
const result = {
|
|
7704
7831
|
layer,
|
|
7705
7832
|
filter: { search, type, module: module_ },
|
|
7706
7833
|
matched: matched.length,
|
|
7707
7834
|
edge_count: matchedEdges.length,
|
|
7708
|
-
nodes: minimal ? toMinimal(matched) : matched
|
|
7709
|
-
edges: matchedEdges
|
|
7835
|
+
nodes: minimal ? toMinimal(matched) : matched
|
|
7710
7836
|
};
|
|
7837
|
+
if (wantEdges) {
|
|
7838
|
+
result.edges = matchedEdges;
|
|
7839
|
+
} else {
|
|
7840
|
+
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).`;
|
|
7841
|
+
}
|
|
7842
|
+
return result;
|
|
7843
|
+
}
|
|
7844
|
+
function runReadGraphQuery(rootDir, args) {
|
|
7845
|
+
const raw = runReadGraphQueryRaw(rootDir, args);
|
|
7846
|
+
return compactResult(raw);
|
|
7711
7847
|
}
|
|
7712
7848
|
function handleReadGraph(args) {
|
|
7713
7849
|
const rootDir = process.cwd();
|
|
@@ -7716,8 +7852,49 @@ function handleReadGraph(args) {
|
|
|
7716
7852
|
if (queries.length === 0) {
|
|
7717
7853
|
return err("queries array is empty. Provide at least one query object.");
|
|
7718
7854
|
}
|
|
7719
|
-
const results =
|
|
7720
|
-
|
|
7855
|
+
const results = [];
|
|
7856
|
+
let cumulativeChars = 0;
|
|
7857
|
+
let budgetHit = false;
|
|
7858
|
+
for (let i = 0; i < queries.length; i++) {
|
|
7859
|
+
const q = queries[i];
|
|
7860
|
+
if (budgetHit) {
|
|
7861
|
+
results.push({
|
|
7862
|
+
index: i,
|
|
7863
|
+
query: q,
|
|
7864
|
+
result: {
|
|
7865
|
+
skipped: true,
|
|
7866
|
+
reason: "batch_budget_exhausted",
|
|
7867
|
+
hint: "Previous sub-results filled the batch budget. Re-run this query on its own."
|
|
7868
|
+
}
|
|
7869
|
+
});
|
|
7870
|
+
continue;
|
|
7871
|
+
}
|
|
7872
|
+
const r = runReadGraphQuery(rootDir, q);
|
|
7873
|
+
const entry = { index: i, query: q, result: r };
|
|
7874
|
+
const entrySize = JSON.stringify(entry, null, 2).length;
|
|
7875
|
+
if (cumulativeChars + entrySize > BATCH_BUDGET_CHARS && results.length > 0) {
|
|
7876
|
+
budgetHit = true;
|
|
7877
|
+
results.push({
|
|
7878
|
+
index: i,
|
|
7879
|
+
query: q,
|
|
7880
|
+
result: {
|
|
7881
|
+
skipped: true,
|
|
7882
|
+
reason: "batch_budget_exhausted",
|
|
7883
|
+
hint: "This sub-query would push the batch over its size budget. Re-run it on its own."
|
|
7884
|
+
}
|
|
7885
|
+
});
|
|
7886
|
+
} else {
|
|
7887
|
+
results.push(entry);
|
|
7888
|
+
cumulativeChars += entrySize;
|
|
7889
|
+
}
|
|
7890
|
+
}
|
|
7891
|
+
return okJson({
|
|
7892
|
+
batch: true,
|
|
7893
|
+
count: results.length,
|
|
7894
|
+
cumulative_chars: cumulativeChars,
|
|
7895
|
+
budget_hit: budgetHit,
|
|
7896
|
+
results
|
|
7897
|
+
});
|
|
7721
7898
|
}
|
|
7722
7899
|
const result = runReadGraphQuery(rootDir, args);
|
|
7723
7900
|
return okJson(result);
|
|
@@ -7747,7 +7924,7 @@ function handleGrepNodes(args) {
|
|
|
7747
7924
|
} catch (e) {
|
|
7748
7925
|
return err(`Invalid regex: ${e.message}`);
|
|
7749
7926
|
}
|
|
7750
|
-
const queryResult =
|
|
7927
|
+
const queryResult = runReadGraphQueryRaw(rootDir, {
|
|
7751
7928
|
layer,
|
|
7752
7929
|
search: args.search,
|
|
7753
7930
|
type: args.type,
|
|
@@ -1069,7 +1069,7 @@ var TOOLS = [
|
|
|
1069
1069
|
},
|
|
1070
1070
|
{
|
|
1071
1071
|
name: "read_graph",
|
|
1072
|
-
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.',
|
|
1072
|
+
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.',
|
|
1073
1073
|
inputSchema: {
|
|
1074
1074
|
type: "object",
|
|
1075
1075
|
properties: {
|
|
@@ -1100,11 +1100,15 @@ var TOOLS = [
|
|
|
1100
1100
|
},
|
|
1101
1101
|
minimal: {
|
|
1102
1102
|
type: "boolean",
|
|
1103
|
-
description: "Return minimal node fields only (id, type, name, module, route). Default false."
|
|
1103
|
+
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."
|
|
1104
|
+
},
|
|
1105
|
+
include_edges: {
|
|
1106
|
+
type: "boolean",
|
|
1107
|
+
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."
|
|
1104
1108
|
},
|
|
1105
1109
|
queries: {
|
|
1106
1110
|
type: "array",
|
|
1107
|
-
description: "Batch mode \u2014 array of query objects to run in a single call. Each uses the same param schema
|
|
1111
|
+
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.",
|
|
1108
1112
|
items: {
|
|
1109
1113
|
type: "object",
|
|
1110
1114
|
properties: {
|
|
@@ -1114,7 +1118,8 @@ var TOOLS = [
|
|
|
1114
1118
|
module: { type: "string" },
|
|
1115
1119
|
node_id: { type: "string" },
|
|
1116
1120
|
hops: { type: "number" },
|
|
1117
|
-
minimal: { type: "boolean" }
|
|
1121
|
+
minimal: { type: "boolean" },
|
|
1122
|
+
include_edges: { type: "boolean" }
|
|
1118
1123
|
}
|
|
1119
1124
|
}
|
|
1120
1125
|
}
|
|
@@ -1181,30 +1186,134 @@ function matchesSearch(node, query) {
|
|
|
1181
1186
|
function toMinimal(nodes) {
|
|
1182
1187
|
return nodes.map((n) => {
|
|
1183
1188
|
const out = { id: n.id, type: n.type, name: n.name };
|
|
1184
|
-
if (n.module) out.module = n.module;
|
|
1185
|
-
if (n.route) out.route = n.route;
|
|
1186
|
-
if (n.methods) out.methods = n.methods;
|
|
1189
|
+
if (n.module != null) out.module = n.module;
|
|
1190
|
+
if (n.route != null) out.route = n.route;
|
|
1191
|
+
if (n.methods != null) out.methods = n.methods;
|
|
1187
1192
|
return out;
|
|
1188
1193
|
});
|
|
1189
1194
|
}
|
|
1190
|
-
|
|
1195
|
+
var COMPACT_SCHEMA = {
|
|
1196
|
+
nodes: {
|
|
1197
|
+
i: "id",
|
|
1198
|
+
t: "type",
|
|
1199
|
+
n: "name",
|
|
1200
|
+
m: "module",
|
|
1201
|
+
r: "route",
|
|
1202
|
+
mt: "methods",
|
|
1203
|
+
x: "exports",
|
|
1204
|
+
c: "columns"
|
|
1205
|
+
},
|
|
1206
|
+
edges: {
|
|
1207
|
+
s: "source_node_index",
|
|
1208
|
+
d: "target_node_index",
|
|
1209
|
+
t: "type",
|
|
1210
|
+
l: "label"
|
|
1211
|
+
},
|
|
1212
|
+
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."
|
|
1213
|
+
};
|
|
1214
|
+
var COMPACT_NODE_KNOWN_KEYS = /* @__PURE__ */ new Set([
|
|
1215
|
+
"id",
|
|
1216
|
+
"type",
|
|
1217
|
+
"name",
|
|
1218
|
+
"module",
|
|
1219
|
+
"route",
|
|
1220
|
+
"methods",
|
|
1221
|
+
"exports",
|
|
1222
|
+
"columns"
|
|
1223
|
+
]);
|
|
1224
|
+
var EST_CHARS_PER_NODE_FULL = {
|
|
1225
|
+
ui: 300,
|
|
1226
|
+
api: 300,
|
|
1227
|
+
db: 3500
|
|
1228
|
+
};
|
|
1229
|
+
var EST_CHARS_PER_NODE_MIN = {
|
|
1230
|
+
ui: 150,
|
|
1231
|
+
api: 200,
|
|
1232
|
+
db: 120
|
|
1233
|
+
};
|
|
1234
|
+
var EST_CHARS_PER_EDGE = {
|
|
1235
|
+
ui: 65,
|
|
1236
|
+
api: 65,
|
|
1237
|
+
db: 65
|
|
1238
|
+
};
|
|
1239
|
+
var NEIGHBORHOOD_BUDGET_CHARS = 55e3;
|
|
1240
|
+
var BATCH_BUDGET_CHARS = 6e4;
|
|
1241
|
+
function toCompactNode(n) {
|
|
1242
|
+
const out = { i: n.id, t: n.type, n: n.name };
|
|
1243
|
+
if (n.module != null) out.m = n.module;
|
|
1244
|
+
if (n.route != null) out.r = n.route;
|
|
1245
|
+
if (n.methods != null) out.mt = n.methods;
|
|
1246
|
+
if (n.exports != null) out.x = n.exports;
|
|
1247
|
+
if (n.columns != null) out.c = n.columns;
|
|
1248
|
+
for (const k of Object.keys(n)) {
|
|
1249
|
+
if (!COMPACT_NODE_KNOWN_KEYS.has(k) && n[k] != null) out[k] = n[k];
|
|
1250
|
+
}
|
|
1251
|
+
return out;
|
|
1252
|
+
}
|
|
1253
|
+
function toCompactEdges(edges, idx) {
|
|
1254
|
+
return edges.map((e) => {
|
|
1255
|
+
const s = idx.get(e.source);
|
|
1256
|
+
const d = idx.get(e.target);
|
|
1257
|
+
const o = {
|
|
1258
|
+
s: s ?? e.source,
|
|
1259
|
+
d: d ?? e.target,
|
|
1260
|
+
t: e.type
|
|
1261
|
+
};
|
|
1262
|
+
if (e.label != null) o.l = e.label;
|
|
1263
|
+
return o;
|
|
1264
|
+
});
|
|
1265
|
+
}
|
|
1266
|
+
function compactResult(raw) {
|
|
1267
|
+
const nodes = raw.nodes;
|
|
1268
|
+
if (!nodes) return raw;
|
|
1269
|
+
const idx = /* @__PURE__ */ new Map();
|
|
1270
|
+
nodes.forEach((n, i) => idx.set(n.id, i));
|
|
1271
|
+
const compactNodes = nodes.map(toCompactNode);
|
|
1272
|
+
const edges = raw.edges;
|
|
1273
|
+
const compactEdges = edges ? toCompactEdges(edges, idx) : void 0;
|
|
1274
|
+
const out = { _schema: COMPACT_SCHEMA };
|
|
1275
|
+
for (const [k, v] of Object.entries(raw)) {
|
|
1276
|
+
if (k === "nodes" || k === "edges") continue;
|
|
1277
|
+
out[k] = v;
|
|
1278
|
+
}
|
|
1279
|
+
out.nodes = compactNodes;
|
|
1280
|
+
if (compactEdges !== void 0) out.edges = compactEdges;
|
|
1281
|
+
return out;
|
|
1282
|
+
}
|
|
1283
|
+
function neighborhood(graph, centerId, hops, layer, minimal) {
|
|
1191
1284
|
const center = graph.nodes.find((n) => n.id === centerId);
|
|
1192
1285
|
if (!center) return null;
|
|
1193
1286
|
const visited = /* @__PURE__ */ new Set([centerId]);
|
|
1194
1287
|
let frontier = /* @__PURE__ */ new Set([centerId]);
|
|
1288
|
+
let budgetExceeded = false;
|
|
1289
|
+
let stoppedAtHop = 0;
|
|
1195
1290
|
for (let h = 0; h < hops; h++) {
|
|
1196
1291
|
const next = /* @__PURE__ */ new Set();
|
|
1197
1292
|
for (const edge of graph.edges) {
|
|
1198
1293
|
if (frontier.has(edge.source) && !visited.has(edge.target)) next.add(edge.target);
|
|
1199
1294
|
if (frontier.has(edge.target) && !visited.has(edge.source)) next.add(edge.source);
|
|
1200
1295
|
}
|
|
1296
|
+
const projectedVisited = visited.size + next.size;
|
|
1297
|
+
let projectedEdges = 0;
|
|
1298
|
+
for (const e of graph.edges) {
|
|
1299
|
+
const srcIn = visited.has(e.source) || next.has(e.source);
|
|
1300
|
+
const dstIn = visited.has(e.target) || next.has(e.target);
|
|
1301
|
+
if (srcIn && dstIn) projectedEdges++;
|
|
1302
|
+
}
|
|
1303
|
+
const perNode = minimal ? EST_CHARS_PER_NODE_MIN[layer] : EST_CHARS_PER_NODE_FULL[layer];
|
|
1304
|
+
const projectedChars = projectedVisited * perNode + projectedEdges * EST_CHARS_PER_EDGE[layer];
|
|
1305
|
+
if (projectedChars > NEIGHBORHOOD_BUDGET_CHARS) {
|
|
1306
|
+
budgetExceeded = true;
|
|
1307
|
+
break;
|
|
1308
|
+
}
|
|
1201
1309
|
for (const id of next) visited.add(id);
|
|
1202
1310
|
frontier = next;
|
|
1311
|
+
stoppedAtHop = h + 1;
|
|
1203
1312
|
if (frontier.size === 0) break;
|
|
1204
1313
|
}
|
|
1205
1314
|
const nodes = graph.nodes.filter((n) => visited.has(n.id));
|
|
1206
1315
|
const edges = graph.edges.filter((e) => visited.has(e.source) && visited.has(e.target));
|
|
1207
|
-
return { nodes, edges };
|
|
1316
|
+
return { nodes, edges, budgetExceeded, stoppedAtHop };
|
|
1208
1317
|
}
|
|
1209
1318
|
function layerSummary(graph) {
|
|
1210
1319
|
const typeCounts = {};
|
|
@@ -1263,14 +1372,16 @@ Output: .launchsecure/graphs/
|
|
|
1263
1372
|
Use read_graph with filters (search/type/module/node_id) to query.`
|
|
1264
1373
|
);
|
|
1265
1374
|
}
|
|
1266
|
-
function
|
|
1375
|
+
function runReadGraphQueryRaw(rootDir, args) {
|
|
1267
1376
|
const layer = args.layer;
|
|
1268
1377
|
const search = args.search;
|
|
1269
1378
|
const type = args.type;
|
|
1270
1379
|
const module_ = args.module;
|
|
1271
1380
|
const nodeId = args.node_id;
|
|
1272
1381
|
const hops = args.hops ?? 1;
|
|
1273
|
-
const
|
|
1382
|
+
const layerIsDb = args.layer === "db";
|
|
1383
|
+
const minimal = args.minimal ?? layerIsDb;
|
|
1384
|
+
const includeEdges = args.include_edges;
|
|
1274
1385
|
const hasFilter = !!(search || type || module_ || nodeId);
|
|
1275
1386
|
if (layer && !["ui", "api", "db"].includes(layer)) {
|
|
1276
1387
|
return { error: `Invalid layer "${layer}". Must be one of: ui, api, db` };
|
|
@@ -1296,19 +1407,26 @@ function runReadGraphQuery(rootDir, args) {
|
|
|
1296
1407
|
return { error: `No ${layer} graph found at .launchsecure/graphs/${layer}.json. Run generate_graph first.` };
|
|
1297
1408
|
}
|
|
1298
1409
|
if (nodeId) {
|
|
1299
|
-
const nb = neighborhood(graph, nodeId, hops);
|
|
1410
|
+
const nb = neighborhood(graph, nodeId, hops, layer, minimal);
|
|
1300
1411
|
if (!nb) {
|
|
1301
1412
|
return { error: `Node "${nodeId}" not found in ${layer} graph. Try read_graph with search="${nodeId}" to find similar nodes.` };
|
|
1302
1413
|
}
|
|
1303
|
-
|
|
1414
|
+
const wantEdges2 = includeEdges ?? true;
|
|
1415
|
+
const result2 = {
|
|
1304
1416
|
layer,
|
|
1305
1417
|
center: nodeId,
|
|
1306
|
-
hops,
|
|
1418
|
+
hops_requested: hops,
|
|
1419
|
+
hops_traversed: nb.stoppedAtHop,
|
|
1307
1420
|
node_count: nb.nodes.length,
|
|
1308
1421
|
edge_count: nb.edges.length,
|
|
1309
|
-
nodes: minimal ? toMinimal(nb.nodes) : nb.nodes
|
|
1310
|
-
edges: nb.edges
|
|
1422
|
+
nodes: minimal ? toMinimal(nb.nodes) : nb.nodes
|
|
1311
1423
|
};
|
|
1424
|
+
if (wantEdges2) result2.edges = nb.edges;
|
|
1425
|
+
if (nb.budgetExceeded) {
|
|
1426
|
+
result2.budget_exceeded = true;
|
|
1427
|
+
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.`;
|
|
1428
|
+
}
|
|
1429
|
+
return result2;
|
|
1312
1430
|
}
|
|
1313
1431
|
if (!hasFilter) {
|
|
1314
1432
|
return {
|
|
@@ -1333,14 +1451,24 @@ function runReadGraphQuery(rootDir, args) {
|
|
|
1333
1451
|
hint: "No nodes matched. Check spelling, or call read_graph without filter to see the summary and available types/modules."
|
|
1334
1452
|
};
|
|
1335
1453
|
}
|
|
1336
|
-
|
|
1454
|
+
const wantEdges = includeEdges ?? false;
|
|
1455
|
+
const result = {
|
|
1337
1456
|
layer,
|
|
1338
1457
|
filter: { search, type, module: module_ },
|
|
1339
1458
|
matched: matched.length,
|
|
1340
1459
|
edge_count: matchedEdges.length,
|
|
1341
|
-
nodes: minimal ? toMinimal(matched) : matched
|
|
1342
|
-
edges: matchedEdges
|
|
1460
|
+
nodes: minimal ? toMinimal(matched) : matched
|
|
1343
1461
|
};
|
|
1462
|
+
if (wantEdges) {
|
|
1463
|
+
result.edges = matchedEdges;
|
|
1464
|
+
} else {
|
|
1465
|
+
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).`;
|
|
1466
|
+
}
|
|
1467
|
+
return result;
|
|
1468
|
+
}
|
|
1469
|
+
function runReadGraphQuery(rootDir, args) {
|
|
1470
|
+
const raw = runReadGraphQueryRaw(rootDir, args);
|
|
1471
|
+
return compactResult(raw);
|
|
1344
1472
|
}
|
|
1345
1473
|
function handleReadGraph(args) {
|
|
1346
1474
|
const rootDir = process.cwd();
|
|
@@ -1349,8 +1477,49 @@ function handleReadGraph(args) {
|
|
|
1349
1477
|
if (queries.length === 0) {
|
|
1350
1478
|
return err("queries array is empty. Provide at least one query object.");
|
|
1351
1479
|
}
|
|
1352
|
-
const results =
|
|
1353
|
-
|
|
1480
|
+
const results = [];
|
|
1481
|
+
let cumulativeChars = 0;
|
|
1482
|
+
let budgetHit = false;
|
|
1483
|
+
for (let i = 0; i < queries.length; i++) {
|
|
1484
|
+
const q = queries[i];
|
|
1485
|
+
if (budgetHit) {
|
|
1486
|
+
results.push({
|
|
1487
|
+
index: i,
|
|
1488
|
+
query: q,
|
|
1489
|
+
result: {
|
|
1490
|
+
skipped: true,
|
|
1491
|
+
reason: "batch_budget_exhausted",
|
|
1492
|
+
hint: "Previous sub-results filled the batch budget. Re-run this query on its own."
|
|
1493
|
+
}
|
|
1494
|
+
});
|
|
1495
|
+
continue;
|
|
1496
|
+
}
|
|
1497
|
+
const r = runReadGraphQuery(rootDir, q);
|
|
1498
|
+
const entry = { index: i, query: q, result: r };
|
|
1499
|
+
const entrySize = JSON.stringify(entry, null, 2).length;
|
|
1500
|
+
if (cumulativeChars + entrySize > BATCH_BUDGET_CHARS && results.length > 0) {
|
|
1501
|
+
budgetHit = true;
|
|
1502
|
+
results.push({
|
|
1503
|
+
index: i,
|
|
1504
|
+
query: q,
|
|
1505
|
+
result: {
|
|
1506
|
+
skipped: true,
|
|
1507
|
+
reason: "batch_budget_exhausted",
|
|
1508
|
+
hint: "This sub-query would push the batch over its size budget. Re-run it on its own."
|
|
1509
|
+
}
|
|
1510
|
+
});
|
|
1511
|
+
} else {
|
|
1512
|
+
results.push(entry);
|
|
1513
|
+
cumulativeChars += entrySize;
|
|
1514
|
+
}
|
|
1515
|
+
}
|
|
1516
|
+
return okJson({
|
|
1517
|
+
batch: true,
|
|
1518
|
+
count: results.length,
|
|
1519
|
+
cumulative_chars: cumulativeChars,
|
|
1520
|
+
budget_hit: budgetHit,
|
|
1521
|
+
results
|
|
1522
|
+
});
|
|
1354
1523
|
}
|
|
1355
1524
|
const result = runReadGraphQuery(rootDir, args);
|
|
1356
1525
|
return okJson(result);
|
|
@@ -1380,7 +1549,7 @@ function handleGrepNodes(args) {
|
|
|
1380
1549
|
} catch (e) {
|
|
1381
1550
|
return err(`Invalid regex: ${e.message}`);
|
|
1382
1551
|
}
|
|
1383
|
-
const queryResult =
|
|
1552
|
+
const queryResult = runReadGraphQueryRaw(rootDir, {
|
|
1384
1553
|
layer,
|
|
1385
1554
|
search: args.search,
|
|
1386
1555
|
type: args.type,
|
package/package.json
CHANGED