@launchsecure/launch-kit 0.0.11 → 0.0.13

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.
@@ -408,6 +408,24 @@ function extractDbCallsTS(absPath) {
408
408
  }
409
409
  return calls;
410
410
  }
411
+ function classifyFile(absPath) {
412
+ const fileName = require("path").basename(absPath);
413
+ if (fileName.includes(".test.") || fileName.includes(".spec.") || fileName.includes("__test")) return "test";
414
+ if (fileName.includes(".stories.")) return "story";
415
+ const tree = parseSource(absPath);
416
+ const root = tree.rootNode;
417
+ const classifyQuery = getQuery("classify");
418
+ const captures = classifyQuery.captures(root);
419
+ const capNames = new Set(captures.map((c) => c.name));
420
+ if (capNames.has("http_export") || capNames.has("http_export_fn")) return "endpoint";
421
+ if (fileName === "page.tsx" && capNames.has("has_jsx")) return "page";
422
+ if (fileName === "layout.tsx" && capNames.has("has_jsx")) return "layout";
423
+ if (capNames.has("has_create_context") || capNames.has("has_create_context_bare")) return "context";
424
+ if (capNames.has("has_jsx")) return "component";
425
+ if (capNames.has("hook_decl") || capNames.has("hook_const")) return "hook";
426
+ if (fileName.includes("config") || fileName.includes(".config.")) return "config";
427
+ return "lib";
428
+ }
411
429
  function extractAuthWrappersTS(absPath) {
412
430
  const tree = parseSource(absPath);
413
431
  const root = tree.rootNode;
@@ -580,7 +598,6 @@ function extractDeep(absPath) {
580
598
  }
581
599
 
582
600
  // src/server/graph/parsers/ui/react-nextjs.ts
583
- var RENDER_TYPES = /* @__PURE__ */ new Set(["component", "ui", "layout", "context"]);
584
601
  function walk(dir, exts) {
585
602
  const results = [];
586
603
  if (!(0, import_node_fs3.existsSync)(dir)) return results;
@@ -681,20 +698,10 @@ function buildAllBarrelMaps(srcDir, parsedByPath) {
681
698
  }
682
699
  return barrels;
683
700
  }
684
- function classifyType(id) {
685
- if (id.endsWith("/page.tsx")) return "page";
686
- if (id.endsWith("/layout.tsx")) return "layout";
687
- if (id.startsWith("client/components/ui/")) return "ui";
688
- if (id.startsWith("client/components/")) return "component";
689
- if (id.startsWith("client/hooks/")) return "hook";
690
- if (/client\/lib\/.*-context\./.test(id)) return "context";
691
- if (id.startsWith("client/lib/")) return id.includes("config") ? "config" : "util";
692
- if (id.startsWith("client/api/")) return "util";
693
- if (id.startsWith("server/mcp/")) return "mcp-tool";
694
- if (id.startsWith("server/lib/")) return "lib";
695
- if (id.startsWith("server/")) return "lib";
696
- if (id.startsWith("lib/") || id.startsWith("config/")) return "lib";
697
- return "component";
701
+ function classifyType(absPath, id) {
702
+ const contentType = classifyFile(absPath);
703
+ if (contentType === "lib" && id.startsWith("server/mcp/")) return "mcp-tool";
704
+ return contentType;
698
705
  }
699
706
  function extractRoute(id) {
700
707
  if (!id.endsWith("/page.tsx")) return null;
@@ -799,13 +806,10 @@ function extractEdges(srcDir, absPath, sourceId, parsed, nodeIdSet, nodeTypeMap,
799
806
  if (label) edge.label = label;
800
807
  edges.push(edge);
801
808
  }
802
- function edgeTypeFor(targetId, isTypeOnlyImport, importedNames) {
809
+ function edgeTypeFor(_targetId, isTypeOnlyImport, importedNames) {
803
810
  if (isTypeOnlyImport) return "imports";
804
- const targetType = nodeTypeMap.get(targetId);
805
- if (targetType && RENDER_TYPES.has(targetType)) {
806
- const anyRendered = importedNames.some((n) => parsed.jsxElements.has(n));
807
- if (anyRendered) return "renders";
808
- }
811
+ const anyRendered = importedNames.some((n) => parsed.jsxElements.has(n));
812
+ if (anyRendered) return "renders";
809
813
  return "imports";
810
814
  }
811
815
  for (const imp of parsed.imports) {
@@ -912,7 +916,7 @@ function generate(rootDir) {
912
916
  const routeToNodeId = /* @__PURE__ */ new Map();
913
917
  for (const absPath of fileSet) {
914
918
  const id = toNodeId(srcDir, absPath);
915
- const type = classifyType(id);
919
+ const type = classifyType(absPath, id);
916
920
  const parsed = parsedByPath.get(absPath);
917
921
  const name = parsed.name || nameFromFilename(absPath);
918
922
  const route = extractRoute(id);
@@ -6690,6 +6690,24 @@ function extractDbCallsTS(absPath) {
6690
6690
  }
6691
6691
  return calls;
6692
6692
  }
6693
+ function classifyFile(absPath) {
6694
+ const fileName = require("path").basename(absPath);
6695
+ if (fileName.includes(".test.") || fileName.includes(".spec.") || fileName.includes("__test")) return "test";
6696
+ if (fileName.includes(".stories.")) return "story";
6697
+ const tree = parseSource(absPath);
6698
+ const root = tree.rootNode;
6699
+ const classifyQuery = getQuery("classify");
6700
+ const captures = classifyQuery.captures(root);
6701
+ const capNames = new Set(captures.map((c) => c.name));
6702
+ if (capNames.has("http_export") || capNames.has("http_export_fn")) return "endpoint";
6703
+ if (fileName === "page.tsx" && capNames.has("has_jsx")) return "page";
6704
+ if (fileName === "layout.tsx" && capNames.has("has_jsx")) return "layout";
6705
+ if (capNames.has("has_create_context") || capNames.has("has_create_context_bare")) return "context";
6706
+ if (capNames.has("has_jsx")) return "component";
6707
+ if (capNames.has("hook_decl") || capNames.has("hook_const")) return "hook";
6708
+ if (fileName.includes("config") || fileName.includes(".config.")) return "config";
6709
+ return "lib";
6710
+ }
6693
6711
  function extractAuthWrappersTS(absPath) {
6694
6712
  const tree = parseSource(absPath);
6695
6713
  const root = tree.rootNode;
@@ -6862,7 +6880,6 @@ function extractDeep(absPath) {
6862
6880
  }
6863
6881
 
6864
6882
  // src/server/graph/parsers/ui/react-nextjs.ts
6865
- var RENDER_TYPES = /* @__PURE__ */ new Set(["component", "ui", "layout", "context"]);
6866
6883
  function walk(dir, exts) {
6867
6884
  const results = [];
6868
6885
  if (!(0, import_node_fs3.existsSync)(dir)) return results;
@@ -6963,20 +6980,10 @@ function buildAllBarrelMaps(srcDir, parsedByPath) {
6963
6980
  }
6964
6981
  return barrels;
6965
6982
  }
6966
- function classifyType(id) {
6967
- if (id.endsWith("/page.tsx")) return "page";
6968
- if (id.endsWith("/layout.tsx")) return "layout";
6969
- if (id.startsWith("client/components/ui/")) return "ui";
6970
- if (id.startsWith("client/components/")) return "component";
6971
- if (id.startsWith("client/hooks/")) return "hook";
6972
- if (/client\/lib\/.*-context\./.test(id)) return "context";
6973
- if (id.startsWith("client/lib/")) return id.includes("config") ? "config" : "util";
6974
- if (id.startsWith("client/api/")) return "util";
6975
- if (id.startsWith("server/mcp/")) return "mcp-tool";
6976
- if (id.startsWith("server/lib/")) return "lib";
6977
- if (id.startsWith("server/")) return "lib";
6978
- if (id.startsWith("lib/") || id.startsWith("config/")) return "lib";
6979
- return "component";
6983
+ function classifyType(absPath, id) {
6984
+ const contentType = classifyFile(absPath);
6985
+ if (contentType === "lib" && id.startsWith("server/mcp/")) return "mcp-tool";
6986
+ return contentType;
6980
6987
  }
6981
6988
  function extractRoute(id) {
6982
6989
  if (!id.endsWith("/page.tsx")) return null;
@@ -7081,13 +7088,10 @@ function extractEdges(srcDir, absPath, sourceId, parsed, nodeIdSet, nodeTypeMap,
7081
7088
  if (label) edge.label = label;
7082
7089
  edges.push(edge);
7083
7090
  }
7084
- function edgeTypeFor(targetId, isTypeOnlyImport, importedNames) {
7091
+ function edgeTypeFor(_targetId, isTypeOnlyImport, importedNames) {
7085
7092
  if (isTypeOnlyImport) return "imports";
7086
- const targetType = nodeTypeMap.get(targetId);
7087
- if (targetType && RENDER_TYPES.has(targetType)) {
7088
- const anyRendered = importedNames.some((n) => parsed.jsxElements.has(n));
7089
- if (anyRendered) return "renders";
7090
- }
7093
+ const anyRendered = importedNames.some((n) => parsed.jsxElements.has(n));
7094
+ if (anyRendered) return "renders";
7091
7095
  return "imports";
7092
7096
  }
7093
7097
  for (const imp of parsed.imports) {
@@ -7194,7 +7198,7 @@ function generate(rootDir) {
7194
7198
  const routeToNodeId = /* @__PURE__ */ new Map();
7195
7199
  for (const absPath of fileSet) {
7196
7200
  const id = toNodeId(srcDir, absPath);
7197
- const type = classifyType(id);
7201
+ const type = classifyType(absPath, id);
7198
7202
  const parsed = parsedByPath.get(absPath);
7199
7203
  const name = parsed.name || nameFromFilename(absPath);
7200
7204
  const route = extractRoute(id);
@@ -9159,7 +9163,7 @@ Returns: { pattern, filter, files_searched, total_matches, matches: [{file, line
9159
9163
  name: "inspect_node",
9160
9164
  description: `Get deep AST data for specific graph nodes \u2014 what's INSIDE a component or endpoint. Returns elements (JSX), state hooks, conditions, variables, responses, and request params. Use INSTEAD of Grep/Read when you need to understand component internals without reading source.
9161
9165
 
9162
- USE THIS FOR: "what elements does LoginPage have?", "what conditions does the login endpoint check?", "what state does SettingsPage manage?", "what responses can this endpoint return?", "what validation does this API do?", "what props does this component accept?"
9166
+ USE THIS FOR: "what elements does LoginPage have?", "what conditions does the login endpoint check?", "what state does SettingsPage manage?", "what responses can this endpoint return?", "what validation does this API do?", "what props does this component accept?", "which endpoints check for isAdmin?", "find all conditions mentioning rateLimit"
9163
9167
 
9164
9168
  DO NOT USE FOR: structural queries (use read_graph), content search (use grep_nodes).
9165
9169
 
@@ -9184,6 +9188,14 @@ Returns deep fields only \u2014 not structural metadata (use read_graph for that
9184
9188
  type: "array",
9185
9189
  items: { type: "string" },
9186
9190
  description: "Specific deep fields to return. Options: elements, stateVars, conditions, variables, responses, params. Omit for all."
9191
+ },
9192
+ filter: {
9193
+ type: "string",
9194
+ description: "Regex pattern to search WITHIN deep field values. Only returns nodes where at least one deep field matches. Searches across all string values in the requested fields (condition tests, variable inits, element tags/props, response bodies, etc.). When set, search becomes optional and node limit is raised to 50."
9195
+ },
9196
+ case_insensitive: {
9197
+ type: "boolean",
9198
+ description: "Case-insensitive filter matching. Default true."
9187
9199
  }
9188
9200
  },
9189
9201
  required: ["layer"]
@@ -9660,8 +9672,10 @@ function handleInspectNode(args) {
9660
9672
  const nodeId = args.node_id;
9661
9673
  const search = args.search;
9662
9674
  const fields = args.fields;
9675
+ const filter = args.filter;
9676
+ const caseInsensitive = args.case_insensitive ?? true;
9663
9677
  if (!layer) return err("layer is required.");
9664
- if (!nodeId && !search) return err("Either node_id or search is required.");
9678
+ if (!nodeId && !search && !filter) return err("Either node_id, search, or filter is required.");
9665
9679
  const graph = readGraph(rootDir, layer);
9666
9680
  if (!graph) return err(`No graph found for layer "${layer}". Run generate_graph first.`);
9667
9681
  let matched;
@@ -9669,30 +9683,66 @@ function handleInspectNode(args) {
9669
9683
  const node = graph.nodes.find((n) => n.id === nodeId);
9670
9684
  if (!node) return err(`Node "${nodeId}" not found in ${layer} layer.`);
9671
9685
  matched = [node];
9672
- } else {
9686
+ } else if (search) {
9673
9687
  const searchLower = search.toLowerCase();
9674
9688
  matched = graph.nodes.filter(
9675
9689
  (n) => n.id.toLowerCase().includes(searchLower) || n.name.toLowerCase().includes(searchLower) || n.route?.toLowerCase().includes(searchLower)
9676
9690
  );
9677
- }
9678
- if (matched.length === 0) return err(`No nodes matching "${search}" in ${layer} layer.`);
9679
- if (matched.length > 5) {
9680
- return err(`${matched.length} nodes match "${search}". Narrow your search (max 5 for inspect_node).`);
9691
+ } else {
9692
+ matched = graph.nodes;
9681
9693
  }
9682
9694
  const allDeepFields = ["elements", "stateVars", "conditions", "variables", "responses", "params"];
9683
9695
  const requestedFields = fields ?? allDeepFields;
9684
- const results = matched.map((node) => {
9696
+ let filterRegex = null;
9697
+ if (filter) {
9698
+ try {
9699
+ filterRegex = new RegExp(filter, caseInsensitive ? "i" : "");
9700
+ } catch {
9701
+ return err(`Invalid regex pattern: "${filter}"`);
9702
+ }
9703
+ }
9704
+ function deepMatch(obj, regex) {
9705
+ if (typeof obj === "string") return regex.test(obj);
9706
+ if (Array.isArray(obj)) return obj.some((item) => deepMatch(item, regex));
9707
+ if (obj && typeof obj === "object") {
9708
+ return Object.values(obj).some((val) => deepMatch(val, regex));
9709
+ }
9710
+ return false;
9711
+ }
9712
+ const results = [];
9713
+ const maxResults = filter ? 50 : 5;
9714
+ for (const node of matched) {
9685
9715
  const deep = { id: node.id, name: node.name, type: node.type };
9716
+ let hasData = false;
9686
9717
  for (const field of requestedFields) {
9687
9718
  if (allDeepFields.includes(field) && node[field] != null) {
9688
9719
  deep[field] = node[field];
9720
+ hasData = true;
9689
9721
  }
9690
9722
  }
9691
- return deep;
9692
- });
9723
+ if (filterRegex) {
9724
+ let fieldMatches = false;
9725
+ for (const field of requestedFields) {
9726
+ if (node[field] != null && deepMatch(node[field], filterRegex)) {
9727
+ fieldMatches = true;
9728
+ break;
9729
+ }
9730
+ }
9731
+ if (!fieldMatches) continue;
9732
+ }
9733
+ if (hasData || !filter) {
9734
+ results.push(deep);
9735
+ }
9736
+ if (results.length >= maxResults) break;
9737
+ }
9738
+ if (results.length === 0) {
9739
+ const hint = filter ? `No nodes with deep fields matching /${filter}/${caseInsensitive ? "i" : ""} in ${layer} layer.` : `No nodes matching "${search}" in ${layer} layer.`;
9740
+ return err(hint);
9741
+ }
9693
9742
  return okJson({
9694
9743
  layer,
9695
9744
  matched: results.length,
9745
+ ...results.length >= maxResults ? { truncated: true, hint: `Showing first ${maxResults} matches. Narrow with search param.` } : {},
9696
9746
  nodes: results
9697
9747
  });
9698
9748
  }
@@ -0,0 +1,41 @@
1
+ ; Detect JSX usage (component/page/layout)
2
+ [(jsx_element) (jsx_self_closing_element)] @has_jsx
3
+
4
+ ; Detect React hooks (function name starts with use)
5
+ (function_declaration
6
+ name: (identifier) @hook_decl
7
+ (#match? @hook_decl "^use[A-Z]"))
8
+
9
+ (lexical_declaration
10
+ (variable_declarator
11
+ name: (identifier) @hook_const
12
+ (#match? @hook_const "^use[A-Z]")))
13
+
14
+ ; Detect useState/useEffect usage (consumer of hooks)
15
+ (call_expression
16
+ function: (identifier) @hook_call
17
+ (#match? @hook_call "^use[A-Z]"))
18
+
19
+ ; Detect React.createContext
20
+ (call_expression
21
+ function: (member_expression
22
+ object: (identifier) @_react
23
+ property: (property_identifier) @_createCtx)
24
+ (#eq? @_react "React")
25
+ (#eq? @_createCtx "createContext")) @has_create_context
26
+
27
+ (call_expression
28
+ function: (identifier) @_createCtx2
29
+ (#eq? @_createCtx2 "createContext")) @has_create_context_bare
30
+
31
+ ; Detect HTTP method exports (API endpoint)
32
+ (export_statement
33
+ declaration: (lexical_declaration
34
+ (variable_declarator
35
+ name: (identifier) @http_export
36
+ (#match? @http_export "^(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)$"))))
37
+
38
+ (export_statement
39
+ declaration: (function_declaration
40
+ name: (identifier) @http_export_fn
41
+ (#match? @http_export_fn "^(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)$")))
@@ -456,6 +456,24 @@ function extractDbCallsTS(absPath) {
456
456
  }
457
457
  return calls;
458
458
  }
459
+ function classifyFile(absPath) {
460
+ const fileName = require("path").basename(absPath);
461
+ if (fileName.includes(".test.") || fileName.includes(".spec.") || fileName.includes("__test")) return "test";
462
+ if (fileName.includes(".stories.")) return "story";
463
+ const tree = parseSource(absPath);
464
+ const root = tree.rootNode;
465
+ const classifyQuery = getQuery("classify");
466
+ const captures = classifyQuery.captures(root);
467
+ const capNames = new Set(captures.map((c) => c.name));
468
+ if (capNames.has("http_export") || capNames.has("http_export_fn")) return "endpoint";
469
+ if (fileName === "page.tsx" && capNames.has("has_jsx")) return "page";
470
+ if (fileName === "layout.tsx" && capNames.has("has_jsx")) return "layout";
471
+ if (capNames.has("has_create_context") || capNames.has("has_create_context_bare")) return "context";
472
+ if (capNames.has("has_jsx")) return "component";
473
+ if (capNames.has("hook_decl") || capNames.has("hook_const")) return "hook";
474
+ if (fileName.includes("config") || fileName.includes(".config.")) return "config";
475
+ return "lib";
476
+ }
459
477
  function extractAuthWrappersTS(absPath) {
460
478
  const tree = parseSource(absPath);
461
479
  const root = tree.rootNode;
@@ -757,20 +775,10 @@ function buildAllBarrelMaps(srcDir, parsedByPath) {
757
775
  }
758
776
  return barrels;
759
777
  }
760
- function classifyType(id) {
761
- if (id.endsWith("/page.tsx")) return "page";
762
- if (id.endsWith("/layout.tsx")) return "layout";
763
- if (id.startsWith("client/components/ui/")) return "ui";
764
- if (id.startsWith("client/components/")) return "component";
765
- if (id.startsWith("client/hooks/")) return "hook";
766
- if (/client\/lib\/.*-context\./.test(id)) return "context";
767
- if (id.startsWith("client/lib/")) return id.includes("config") ? "config" : "util";
768
- if (id.startsWith("client/api/")) return "util";
769
- if (id.startsWith("server/mcp/")) return "mcp-tool";
770
- if (id.startsWith("server/lib/")) return "lib";
771
- if (id.startsWith("server/")) return "lib";
772
- if (id.startsWith("lib/") || id.startsWith("config/")) return "lib";
773
- return "component";
778
+ function classifyType(absPath, id) {
779
+ const contentType = classifyFile(absPath);
780
+ if (contentType === "lib" && id.startsWith("server/mcp/")) return "mcp-tool";
781
+ return contentType;
774
782
  }
775
783
  function extractRoute(id) {
776
784
  if (!id.endsWith("/page.tsx")) return null;
@@ -875,13 +883,10 @@ function extractEdges(srcDir, absPath, sourceId, parsed, nodeIdSet, nodeTypeMap,
875
883
  if (label) edge.label = label;
876
884
  edges.push(edge);
877
885
  }
878
- function edgeTypeFor(targetId, isTypeOnlyImport, importedNames) {
886
+ function edgeTypeFor(_targetId, isTypeOnlyImport, importedNames) {
879
887
  if (isTypeOnlyImport) return "imports";
880
- const targetType = nodeTypeMap.get(targetId);
881
- if (targetType && RENDER_TYPES.has(targetType)) {
882
- const anyRendered = importedNames.some((n) => parsed.jsxElements.has(n));
883
- if (anyRendered) return "renders";
884
- }
888
+ const anyRendered = importedNames.some((n) => parsed.jsxElements.has(n));
889
+ if (anyRendered) return "renders";
885
890
  return "imports";
886
891
  }
887
892
  for (const imp of parsed.imports) {
@@ -988,7 +993,7 @@ function generate(rootDir) {
988
993
  const routeToNodeId = /* @__PURE__ */ new Map();
989
994
  for (const absPath of fileSet) {
990
995
  const id = toNodeId(srcDir, absPath);
991
- const type = classifyType(id);
996
+ const type = classifyType(absPath, id);
992
997
  const parsed = parsedByPath.get(absPath);
993
998
  const name = parsed.name || nameFromFilename(absPath);
994
999
  const route = extractRoute(id);
@@ -1176,14 +1181,13 @@ function generate(rootDir) {
1176
1181
  }
1177
1182
  };
1178
1183
  }
1179
- var import_node_fs4, import_node_path4, RENDER_TYPES, reactNextjsParser;
1184
+ var import_node_fs4, import_node_path4, reactNextjsParser;
1180
1185
  var init_react_nextjs = __esm({
1181
1186
  "src/server/graph/parsers/ui/react-nextjs.ts"() {
1182
1187
  "use strict";
1183
1188
  import_node_fs4 = require("node:fs");
1184
1189
  import_node_path4 = require("node:path");
1185
1190
  init_ts_extractor();
1186
- RENDER_TYPES = /* @__PURE__ */ new Set(["component", "ui", "layout", "context"]);
1187
1191
  reactNextjsParser = {
1188
1192
  id: "react-nextjs",
1189
1193
  layer: "ui",
@@ -3519,8 +3523,10 @@ function handleInspectNode(args) {
3519
3523
  const nodeId = args.node_id;
3520
3524
  const search = args.search;
3521
3525
  const fields = args.fields;
3526
+ const filter = args.filter;
3527
+ const caseInsensitive = args.case_insensitive ?? true;
3522
3528
  if (!layer) return err("layer is required.");
3523
- if (!nodeId && !search) return err("Either node_id or search is required.");
3529
+ if (!nodeId && !search && !filter) return err("Either node_id, search, or filter is required.");
3524
3530
  const graph = readGraph(rootDir, layer);
3525
3531
  if (!graph) return err(`No graph found for layer "${layer}". Run generate_graph first.`);
3526
3532
  let matched;
@@ -3528,30 +3534,66 @@ function handleInspectNode(args) {
3528
3534
  const node = graph.nodes.find((n) => n.id === nodeId);
3529
3535
  if (!node) return err(`Node "${nodeId}" not found in ${layer} layer.`);
3530
3536
  matched = [node];
3531
- } else {
3537
+ } else if (search) {
3532
3538
  const searchLower = search.toLowerCase();
3533
3539
  matched = graph.nodes.filter(
3534
3540
  (n) => n.id.toLowerCase().includes(searchLower) || n.name.toLowerCase().includes(searchLower) || n.route?.toLowerCase().includes(searchLower)
3535
3541
  );
3536
- }
3537
- if (matched.length === 0) return err(`No nodes matching "${search}" in ${layer} layer.`);
3538
- if (matched.length > 5) {
3539
- return err(`${matched.length} nodes match "${search}". Narrow your search (max 5 for inspect_node).`);
3542
+ } else {
3543
+ matched = graph.nodes;
3540
3544
  }
3541
3545
  const allDeepFields = ["elements", "stateVars", "conditions", "variables", "responses", "params"];
3542
3546
  const requestedFields = fields ?? allDeepFields;
3543
- const results = matched.map((node) => {
3547
+ let filterRegex = null;
3548
+ if (filter) {
3549
+ try {
3550
+ filterRegex = new RegExp(filter, caseInsensitive ? "i" : "");
3551
+ } catch {
3552
+ return err(`Invalid regex pattern: "${filter}"`);
3553
+ }
3554
+ }
3555
+ function deepMatch(obj, regex) {
3556
+ if (typeof obj === "string") return regex.test(obj);
3557
+ if (Array.isArray(obj)) return obj.some((item) => deepMatch(item, regex));
3558
+ if (obj && typeof obj === "object") {
3559
+ return Object.values(obj).some((val) => deepMatch(val, regex));
3560
+ }
3561
+ return false;
3562
+ }
3563
+ const results = [];
3564
+ const maxResults = filter ? 50 : 5;
3565
+ for (const node of matched) {
3544
3566
  const deep = { id: node.id, name: node.name, type: node.type };
3567
+ let hasData = false;
3545
3568
  for (const field of requestedFields) {
3546
3569
  if (allDeepFields.includes(field) && node[field] != null) {
3547
3570
  deep[field] = node[field];
3571
+ hasData = true;
3548
3572
  }
3549
3573
  }
3550
- return deep;
3551
- });
3574
+ if (filterRegex) {
3575
+ let fieldMatches = false;
3576
+ for (const field of requestedFields) {
3577
+ if (node[field] != null && deepMatch(node[field], filterRegex)) {
3578
+ fieldMatches = true;
3579
+ break;
3580
+ }
3581
+ }
3582
+ if (!fieldMatches) continue;
3583
+ }
3584
+ if (hasData || !filter) {
3585
+ results.push(deep);
3586
+ }
3587
+ if (results.length >= maxResults) break;
3588
+ }
3589
+ if (results.length === 0) {
3590
+ const hint = filter ? `No nodes with deep fields matching /${filter}/${caseInsensitive ? "i" : ""} in ${layer} layer.` : `No nodes matching "${search}" in ${layer} layer.`;
3591
+ return err(hint);
3592
+ }
3552
3593
  return okJson({
3553
3594
  layer,
3554
3595
  matched: results.length,
3596
+ ...results.length >= maxResults ? { truncated: true, hint: `Showing first ${maxResults} matches. Narrow with search param.` } : {},
3555
3597
  nodes: results
3556
3598
  });
3557
3599
  }
@@ -4074,7 +4116,7 @@ Returns: { pattern, filter, files_searched, total_matches, matches: [{file, line
4074
4116
  name: "inspect_node",
4075
4117
  description: `Get deep AST data for specific graph nodes \u2014 what's INSIDE a component or endpoint. Returns elements (JSX), state hooks, conditions, variables, responses, and request params. Use INSTEAD of Grep/Read when you need to understand component internals without reading source.
4076
4118
 
4077
- USE THIS FOR: "what elements does LoginPage have?", "what conditions does the login endpoint check?", "what state does SettingsPage manage?", "what responses can this endpoint return?", "what validation does this API do?", "what props does this component accept?"
4119
+ USE THIS FOR: "what elements does LoginPage have?", "what conditions does the login endpoint check?", "what state does SettingsPage manage?", "what responses can this endpoint return?", "what validation does this API do?", "what props does this component accept?", "which endpoints check for isAdmin?", "find all conditions mentioning rateLimit"
4078
4120
 
4079
4121
  DO NOT USE FOR: structural queries (use read_graph), content search (use grep_nodes).
4080
4122
 
@@ -4099,6 +4141,14 @@ Returns deep fields only \u2014 not structural metadata (use read_graph for that
4099
4141
  type: "array",
4100
4142
  items: { type: "string" },
4101
4143
  description: "Specific deep fields to return. Options: elements, stateVars, conditions, variables, responses, params. Omit for all."
4144
+ },
4145
+ filter: {
4146
+ type: "string",
4147
+ description: "Regex pattern to search WITHIN deep field values. Only returns nodes where at least one deep field matches. Searches across all string values in the requested fields (condition tests, variable inits, element tags/props, response bodies, etc.). When set, search becomes optional and node limit is raised to 50."
4148
+ },
4149
+ case_insensitive: {
4150
+ type: "boolean",
4151
+ description: "Case-insensitive filter matching. Default true."
4102
4152
  }
4103
4153
  },
4104
4154
  required: ["layer"]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@launchsecure/launch-kit",
3
- "version": "0.0.11",
3
+ "version": "0.0.13",
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",