@launchsecure/launch-kit 0.0.2 → 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.
@@ -7444,7 +7444,7 @@ var TOOLS = [
7444
7444
  },
7445
7445
  {
7446
7446
  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.',
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.',
7448
7448
  inputSchema: {
7449
7449
  type: "object",
7450
7450
  properties: {
@@ -7475,11 +7475,15 @@ var TOOLS = [
7475
7475
  },
7476
7476
  minimal: {
7477
7477
  type: "boolean",
7478
- 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."
7479
7483
  },
7480
7484
  queries: {
7481
7485
  type: "array",
7482
- description: "Batch mode \u2014 array of query objects to run in a single call. Each uses the same param schema (layer/search/type/module/node_id/hops/minimal). When set, top-level params are ignored.",
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.",
7483
7487
  items: {
7484
7488
  type: "object",
7485
7489
  properties: {
@@ -7489,7 +7493,8 @@ var TOOLS = [
7489
7493
  module: { type: "string" },
7490
7494
  node_id: { type: "string" },
7491
7495
  hops: { type: "number" },
7492
- minimal: { type: "boolean" }
7496
+ minimal: { type: "boolean" },
7497
+ include_edges: { type: "boolean" }
7493
7498
  }
7494
7499
  }
7495
7500
  }
@@ -7556,30 +7561,134 @@ function matchesSearch(node, query) {
7556
7561
  function toMinimal(nodes) {
7557
7562
  return nodes.map((n) => {
7558
7563
  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;
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;
7562
7567
  return out;
7563
7568
  });
7564
7569
  }
7565
- 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) {
7566
7659
  const center = graph.nodes.find((n) => n.id === centerId);
7567
7660
  if (!center) return null;
7568
7661
  const visited = /* @__PURE__ */ new Set([centerId]);
7569
7662
  let frontier = /* @__PURE__ */ new Set([centerId]);
7663
+ let budgetExceeded = false;
7664
+ let stoppedAtHop = 0;
7570
7665
  for (let h = 0; h < hops; h++) {
7571
7666
  const next = /* @__PURE__ */ new Set();
7572
7667
  for (const edge of graph.edges) {
7573
7668
  if (frontier.has(edge.source) && !visited.has(edge.target)) next.add(edge.target);
7574
7669
  if (frontier.has(edge.target) && !visited.has(edge.source)) next.add(edge.source);
7575
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
+ }
7576
7684
  for (const id of next) visited.add(id);
7577
7685
  frontier = next;
7686
+ stoppedAtHop = h + 1;
7578
7687
  if (frontier.size === 0) break;
7579
7688
  }
7580
7689
  const nodes = graph.nodes.filter((n) => visited.has(n.id));
7581
7690
  const edges = graph.edges.filter((e) => visited.has(e.source) && visited.has(e.target));
7582
- return { nodes, edges };
7691
+ return { nodes, edges, budgetExceeded, stoppedAtHop };
7583
7692
  }
7584
7693
  function layerSummary(graph) {
7585
7694
  const typeCounts = {};
@@ -7638,14 +7747,16 @@ Output: .launchsecure/graphs/
7638
7747
  Use read_graph with filters (search/type/module/node_id) to query.`
7639
7748
  );
7640
7749
  }
7641
- function runReadGraphQuery(rootDir, args) {
7750
+ function runReadGraphQueryRaw(rootDir, args) {
7642
7751
  const layer = args.layer;
7643
7752
  const search = args.search;
7644
7753
  const type = args.type;
7645
7754
  const module_ = args.module;
7646
7755
  const nodeId = args.node_id;
7647
7756
  const hops = args.hops ?? 1;
7648
- const minimal = args.minimal ?? false;
7757
+ const layerIsDb = args.layer === "db";
7758
+ const minimal = args.minimal ?? layerIsDb;
7759
+ const includeEdges = args.include_edges;
7649
7760
  const hasFilter = !!(search || type || module_ || nodeId);
7650
7761
  if (layer && !["ui", "api", "db"].includes(layer)) {
7651
7762
  return { error: `Invalid layer "${layer}". Must be one of: ui, api, db` };
@@ -7671,19 +7782,26 @@ function runReadGraphQuery(rootDir, args) {
7671
7782
  return { error: `No ${layer} graph found at .launchsecure/graphs/${layer}.json. Run generate_graph first.` };
7672
7783
  }
7673
7784
  if (nodeId) {
7674
- const nb = neighborhood(graph, nodeId, hops);
7785
+ const nb = neighborhood(graph, nodeId, hops, layer, minimal);
7675
7786
  if (!nb) {
7676
7787
  return { error: `Node "${nodeId}" not found in ${layer} graph. Try read_graph with search="${nodeId}" to find similar nodes.` };
7677
7788
  }
7678
- return {
7789
+ const wantEdges2 = includeEdges ?? true;
7790
+ const result2 = {
7679
7791
  layer,
7680
7792
  center: nodeId,
7681
- hops,
7793
+ hops_requested: hops,
7794
+ hops_traversed: nb.stoppedAtHop,
7682
7795
  node_count: nb.nodes.length,
7683
7796
  edge_count: nb.edges.length,
7684
- nodes: minimal ? toMinimal(nb.nodes) : nb.nodes,
7685
- edges: nb.edges
7797
+ nodes: minimal ? toMinimal(nb.nodes) : nb.nodes
7686
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;
7687
7805
  }
7688
7806
  if (!hasFilter) {
7689
7807
  return {
@@ -7708,14 +7826,24 @@ function runReadGraphQuery(rootDir, args) {
7708
7826
  hint: "No nodes matched. Check spelling, or call read_graph without filter to see the summary and available types/modules."
7709
7827
  };
7710
7828
  }
7711
- return {
7829
+ const wantEdges = includeEdges ?? false;
7830
+ const result = {
7712
7831
  layer,
7713
7832
  filter: { search, type, module: module_ },
7714
7833
  matched: matched.length,
7715
7834
  edge_count: matchedEdges.length,
7716
- nodes: minimal ? toMinimal(matched) : matched,
7717
- edges: matchedEdges
7835
+ nodes: minimal ? toMinimal(matched) : matched
7718
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);
7719
7847
  }
7720
7848
  function handleReadGraph(args) {
7721
7849
  const rootDir = process.cwd();
@@ -7724,8 +7852,49 @@ function handleReadGraph(args) {
7724
7852
  if (queries.length === 0) {
7725
7853
  return err("queries array is empty. Provide at least one query object.");
7726
7854
  }
7727
- const results = queries.map((q, i) => ({ index: i, query: q, result: runReadGraphQuery(rootDir, q) }));
7728
- 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
+ });
7729
7898
  }
7730
7899
  const result = runReadGraphQuery(rootDir, args);
7731
7900
  return okJson(result);
@@ -7755,7 +7924,7 @@ function handleGrepNodes(args) {
7755
7924
  } catch (e) {
7756
7925
  return err(`Invalid regex: ${e.message}`);
7757
7926
  }
7758
- const queryResult = runReadGraphQuery(rootDir, {
7927
+ const queryResult = runReadGraphQueryRaw(rootDir, {
7759
7928
  layer,
7760
7929
  search: args.search,
7761
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.2",
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",