@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.
- package/dist/server/chart-serve.js +26 -22
- package/dist/server/cli.js +82 -32
- package/dist/server/graph/queries/classify.scm +41 -0
- package/dist/server/graph-mcp-entry.js +83 -33
- package/package.json +1 -1
|
@@ -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
|
-
|
|
686
|
-
if (id.
|
|
687
|
-
|
|
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(
|
|
809
|
+
function edgeTypeFor(_targetId, isTypeOnlyImport, importedNames) {
|
|
803
810
|
if (isTypeOnlyImport) return "imports";
|
|
804
|
-
const
|
|
805
|
-
if (
|
|
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);
|
package/dist/server/cli.js
CHANGED
|
@@ -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
|
-
|
|
6968
|
-
if (id.
|
|
6969
|
-
|
|
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(
|
|
7091
|
+
function edgeTypeFor(_targetId, isTypeOnlyImport, importedNames) {
|
|
7085
7092
|
if (isTypeOnlyImport) return "imports";
|
|
7086
|
-
const
|
|
7087
|
-
if (
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
762
|
-
if (id.
|
|
763
|
-
|
|
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(
|
|
886
|
+
function edgeTypeFor(_targetId, isTypeOnlyImport, importedNames) {
|
|
879
887
|
if (isTypeOnlyImport) return "imports";
|
|
880
|
-
const
|
|
881
|
-
if (
|
|
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,
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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