@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.
@@ -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 (layer/search/type/module/node_id/hops/minimal). When set, top-level params are ignored.",
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
- function neighborhood(graph, centerId, hops) {
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 runReadGraphQuery(rootDir, args) {
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 minimal = args.minimal ?? false;
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
- return {
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
- return {
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 = queries.map((q, i) => ({ index: i, query: q, result: runReadGraphQuery(rootDir, q) }));
7720
- return okJson({ batch: true, count: results.length, results });
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 = runReadGraphQuery(rootDir, {
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 (layer/search/type/module/node_id/hops/minimal). When set, top-level params are ignored.",
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
- function neighborhood(graph, centerId, hops) {
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 runReadGraphQuery(rootDir, args) {
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 minimal = args.minimal ?? false;
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
- return {
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
- return {
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 = queries.map((q, i) => ({ index: i, query: q, result: runReadGraphQuery(rootDir, q) }));
1353
- return okJson({ batch: true, count: results.length, results });
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 = runReadGraphQuery(rootDir, {
1552
+ const queryResult = runReadGraphQueryRaw(rootDir, {
1384
1553
  layer,
1385
1554
  search: args.search,
1386
1555
  type: args.type,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@launchsecure/launch-kit",
3
- "version": "0.0.1",
3
+ "version": "0.0.3",
4
4
  "description": "LaunchSecure toolkit — launch-pod (pipeline pod), launch-chart (project graph MCP), and more.",
5
5
  "license": "MIT",
6
6
  "author": "LaunchSecure - AutomateWithUs",