@launchsecure/launch-kit 0.0.1

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.
Files changed (64) hide show
  1. package/README.md +37 -0
  2. package/dist/client/assets/index-C8GAsRGO.css +32 -0
  3. package/dist/client/assets/index-CcHIoRl6.js +286 -0
  4. package/dist/client/index.html +22 -0
  5. package/dist/server/cli.js +8853 -0
  6. package/dist/server/fb-wizard.js +136 -0
  7. package/dist/server/graph-mcp-entry.js +1542 -0
  8. package/dist/server/public/app.js +1312 -0
  9. package/dist/server/public/icons.js +36 -0
  10. package/dist/server/public/index.html +159 -0
  11. package/dist/server/public/plan-detector.js +186 -0
  12. package/dist/server/public/session-manager.js +1129 -0
  13. package/dist/server/public/splits.js +569 -0
  14. package/dist/server/public/style.css +1620 -0
  15. package/package.json +73 -0
  16. package/prompts/analysis.md +992 -0
  17. package/prompts/architect-reconcile.md +931 -0
  18. package/prompts/architecture-sync.md +902 -0
  19. package/prompts/be-contract.md +709 -0
  20. package/prompts/be-impl.md +565 -0
  21. package/prompts/be-policy.md +551 -0
  22. package/prompts/be-test.md +591 -0
  23. package/prompts/bug-diagnosis.md +653 -0
  24. package/prompts/bug-intake.md +563 -0
  25. package/prompts/change-request-intake.md +593 -0
  26. package/prompts/db-contract.md +644 -0
  27. package/prompts/db-impl.md +522 -0
  28. package/prompts/db-interaction.md +569 -0
  29. package/prompts/db-test.md +630 -0
  30. package/prompts/decision-pack.md +654 -0
  31. package/prompts/fe-contract.md +992 -0
  32. package/prompts/fe-flow.md +537 -0
  33. package/prompts/fe-impl.md +597 -0
  34. package/prompts/fe-reconcile.md +506 -0
  35. package/prompts/fe-review.md +550 -0
  36. package/prompts/fe-test.md +705 -0
  37. package/prompts/fix-planner.md +1219 -0
  38. package/prompts/global-db-patterns.md +588 -0
  39. package/prompts/global-env-config.md +460 -0
  40. package/prompts/global-integrations.md +504 -0
  41. package/prompts/global-middleware.md +442 -0
  42. package/prompts/global-navigation.md +502 -0
  43. package/prompts/global-security.md +603 -0
  44. package/prompts/global-services.md +427 -0
  45. package/prompts/greenfield-classifier.md +590 -0
  46. package/prompts/llm-council.md +597 -0
  47. package/prompts/module-sequencer.md +529 -0
  48. package/prompts/normalize.md +611 -0
  49. package/prompts/optimization.md +633 -0
  50. package/prompts/prd-generation.md +544 -0
  51. package/prompts/prd-reconcile.md +584 -0
  52. package/prompts/prd-review.md +504 -0
  53. package/prompts/pre-code-analysis.md +565 -0
  54. package/prompts/pre-code-global-analysis.md +169 -0
  55. package/prompts/production-bootstrap.md +577 -0
  56. package/prompts/research.md +702 -0
  57. package/prompts/retrofit-analysis.md +845 -0
  58. package/prompts/spike.md +850 -0
  59. package/prompts/theming.md +835 -0
  60. package/prompts/triage.md +599 -0
  61. package/prompts/unified-reconcile.md +628 -0
  62. package/prompts/unified-review.md +592 -0
  63. package/prompts/user-stories.md +486 -0
  64. package/prompts/wireframe.md +576 -0
@@ -0,0 +1,1542 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ // src/server/graph-mcp.ts
5
+ var import_node_fs6 = require("node:fs");
6
+ var import_node_path6 = require("node:path");
7
+
8
+ // src/server/graph/index.ts
9
+ var import_node_fs5 = require("node:fs");
10
+ var import_node_path5 = require("node:path");
11
+
12
+ // src/server/graph/parsers/ui/react-nextjs.ts
13
+ var import_node_fs2 = require("node:fs");
14
+ var import_node_path2 = require("node:path");
15
+
16
+ // src/server/graph/core/ast-helpers.ts
17
+ var import_node_fs = require("node:fs");
18
+ var import_node_path = require("node:path");
19
+ var tsModule;
20
+ function getTs() {
21
+ if (!tsModule) {
22
+ tsModule = require("typescript");
23
+ }
24
+ return tsModule;
25
+ }
26
+ function parseFile(absPath) {
27
+ const ts = getTs();
28
+ const content = (0, import_node_fs.readFileSync)(absPath, "utf-8");
29
+ const ext = (0, import_node_path.extname)(absPath);
30
+ const scriptKind = ext === ".tsx" ? ts.ScriptKind.TSX : ext === ".ts" ? ts.ScriptKind.TS : ext === ".jsx" ? ts.ScriptKind.JSX : ts.ScriptKind.JS;
31
+ const sourceFile = ts.createSourceFile(
32
+ absPath,
33
+ content,
34
+ ts.ScriptTarget.Latest,
35
+ /* setParentNodes */
36
+ true,
37
+ scriptKind
38
+ );
39
+ const exportsSet = /* @__PURE__ */ new Set();
40
+ const exportsOrdered = [];
41
+ let defaultName = null;
42
+ let firstValueExport = null;
43
+ let firstTypeExport = null;
44
+ const imports = [];
45
+ const reExports = [];
46
+ const jsxElements = /* @__PURE__ */ new Set();
47
+ const navigations = [];
48
+ function addExport(name2, kind) {
49
+ if (!exportsSet.has(name2)) {
50
+ exportsSet.add(name2);
51
+ exportsOrdered.push(name2);
52
+ }
53
+ if (kind === "default") defaultName = name2;
54
+ else if (kind === "value" && !firstValueExport) firstValueExport = name2;
55
+ else if (kind === "type" && !firstTypeExport) firstTypeExport = name2;
56
+ }
57
+ function hasModifier(node, kind) {
58
+ const modifiers = ts.canHaveModifiers(node) ? ts.getModifiers(node) : void 0;
59
+ return modifiers?.some((m) => m.kind === kind) ?? false;
60
+ }
61
+ function extractTargetFromExpression(expr) {
62
+ if (ts.isStringLiteral(expr) || ts.isNoSubstitutionTemplateLiteral(expr)) {
63
+ return { target: expr.text, isTemplate: false };
64
+ }
65
+ if (ts.isTemplateExpression(expr)) {
66
+ return { target: expr.getText(sourceFile), isTemplate: true };
67
+ }
68
+ return null;
69
+ }
70
+ function visit(node) {
71
+ if (ts.isImportDeclaration(node)) {
72
+ const moduleSpec = node.moduleSpecifier;
73
+ if (ts.isStringLiteral(moduleSpec)) {
74
+ const specifier = moduleSpec.text;
75
+ const clause = node.importClause;
76
+ const isTypeOnly = !!clause?.isTypeOnly;
77
+ const names = [];
78
+ const typeNames = /* @__PURE__ */ new Set();
79
+ if (clause) {
80
+ if (clause.name) names.push(clause.name.text);
81
+ const nb = clause.namedBindings;
82
+ if (nb && ts.isNamedImports(nb)) {
83
+ for (const el of nb.elements) {
84
+ names.push(el.name.text);
85
+ if (el.isTypeOnly) typeNames.add(el.name.text);
86
+ }
87
+ } else if (nb && ts.isNamespaceImport(nb)) {
88
+ names.push(nb.name.text);
89
+ }
90
+ }
91
+ if (names.length > 0 || isTypeOnly) {
92
+ imports.push({ names, specifier, isTypeOnly, typeNames });
93
+ }
94
+ }
95
+ }
96
+ if (ts.isExportDeclaration(node)) {
97
+ const fromSpec = node.moduleSpecifier && ts.isStringLiteral(node.moduleSpecifier) ? node.moduleSpecifier.text : null;
98
+ if (node.exportClause && ts.isNamedExports(node.exportClause)) {
99
+ for (const el of node.exportClause.elements) {
100
+ const exportedName = el.name.text;
101
+ addExport(exportedName, "value");
102
+ if (fromSpec) {
103
+ reExports.push({ name: exportedName, from: fromSpec });
104
+ }
105
+ }
106
+ }
107
+ }
108
+ if (ts.isExportAssignment(node) && !node.isExportEquals) {
109
+ if (ts.isIdentifier(node.expression)) {
110
+ addExport(node.expression.text, "default");
111
+ } else {
112
+ if (!defaultName) defaultName = "default";
113
+ }
114
+ }
115
+ if (ts.isFunctionDeclaration(node) && hasModifier(node, ts.SyntaxKind.ExportKeyword)) {
116
+ const isDefault = hasModifier(node, ts.SyntaxKind.DefaultKeyword);
117
+ if (node.name) addExport(node.name.text, isDefault ? "default" : "value");
118
+ }
119
+ if (ts.isVariableStatement(node) && hasModifier(node, ts.SyntaxKind.ExportKeyword)) {
120
+ for (const decl of node.declarationList.declarations) {
121
+ if (ts.isIdentifier(decl.name)) {
122
+ addExport(decl.name.text, "value");
123
+ }
124
+ }
125
+ }
126
+ if (ts.isClassDeclaration(node) && hasModifier(node, ts.SyntaxKind.ExportKeyword)) {
127
+ const isDefault = hasModifier(node, ts.SyntaxKind.DefaultKeyword);
128
+ if (node.name) addExport(node.name.text, isDefault ? "default" : "value");
129
+ }
130
+ if (ts.isTypeAliasDeclaration(node) && hasModifier(node, ts.SyntaxKind.ExportKeyword)) {
131
+ addExport(node.name.text, "type");
132
+ }
133
+ if (ts.isInterfaceDeclaration(node) && hasModifier(node, ts.SyntaxKind.ExportKeyword)) {
134
+ addExport(node.name.text, "type");
135
+ }
136
+ if (ts.isEnumDeclaration(node) && hasModifier(node, ts.SyntaxKind.ExportKeyword)) {
137
+ addExport(node.name.text, "value");
138
+ }
139
+ if (ts.isJsxOpeningElement(node) || ts.isJsxSelfClosingElement(node)) {
140
+ const tagName = node.tagName;
141
+ if (ts.isIdentifier(tagName) && /^[A-Z]/.test(tagName.text)) {
142
+ jsxElements.add(tagName.text);
143
+ } else if (ts.isPropertyAccessExpression(tagName)) {
144
+ let root = tagName;
145
+ while (ts.isPropertyAccessExpression(root)) root = root.expression;
146
+ if (ts.isIdentifier(root) && /^[A-Z]/.test(root.text)) {
147
+ jsxElements.add(root.text);
148
+ }
149
+ }
150
+ }
151
+ if (ts.isCallExpression(node)) {
152
+ const expr = node.expression;
153
+ if (ts.isPropertyAccessExpression(expr) && ts.isIdentifier(expr.expression) && expr.expression.text === "router" && (expr.name.text === "push" || expr.name.text === "replace")) {
154
+ const arg = node.arguments[0];
155
+ if (arg) {
156
+ const parsed = extractTargetFromExpression(arg);
157
+ if (parsed) {
158
+ navigations.push({
159
+ kind: expr.name.text === "push" ? "router-push" : "router-replace",
160
+ target: parsed.target,
161
+ isTemplate: parsed.isTemplate
162
+ });
163
+ }
164
+ }
165
+ }
166
+ }
167
+ if (ts.isJsxOpeningElement(node) || ts.isJsxSelfClosingElement(node)) {
168
+ const tagName = node.tagName;
169
+ if (ts.isIdentifier(tagName) && tagName.text === "Link") {
170
+ for (const attr of node.attributes.properties) {
171
+ if (ts.isJsxAttribute(attr) && attr.name.getText(sourceFile) === "href" && attr.initializer) {
172
+ const init = attr.initializer;
173
+ if (ts.isStringLiteral(init)) {
174
+ navigations.push({ kind: "link-href", target: init.text, isTemplate: false });
175
+ } else if (ts.isJsxExpression(init) && init.expression) {
176
+ const parsed = extractTargetFromExpression(init.expression);
177
+ if (parsed) {
178
+ navigations.push({ kind: "link-href", target: parsed.target, isTemplate: parsed.isTemplate });
179
+ }
180
+ }
181
+ }
182
+ }
183
+ }
184
+ }
185
+ if (ts.isBinaryExpression(node) && node.operatorToken.kind === ts.SyntaxKind.EqualsToken) {
186
+ const left = node.left;
187
+ if (ts.isPropertyAccessExpression(left) && ts.isPropertyAccessExpression(left.expression) && ts.isIdentifier(left.expression.expression) && left.expression.expression.text === "window" && left.expression.name.text === "location" && left.name.text === "href") {
188
+ const parsed = extractTargetFromExpression(node.right);
189
+ if (parsed) {
190
+ navigations.push({ kind: "window-location", target: parsed.target, isTemplate: parsed.isTemplate });
191
+ }
192
+ }
193
+ }
194
+ if (ts.isCallExpression(node)) {
195
+ const expr = node.expression;
196
+ if (ts.isPropertyAccessExpression(expr) && ts.isPropertyAccessExpression(expr.expression) && ts.isIdentifier(expr.expression.expression) && expr.expression.expression.text === "window" && expr.expression.name.text === "location" && (expr.name.text === "assign" || expr.name.text === "replace")) {
197
+ const arg = node.arguments[0];
198
+ if (arg) {
199
+ const parsed = extractTargetFromExpression(arg);
200
+ if (parsed) {
201
+ navigations.push({ kind: "window-location", target: parsed.target, isTemplate: parsed.isTemplate });
202
+ }
203
+ }
204
+ }
205
+ }
206
+ ts.forEachChild(node, visit);
207
+ }
208
+ visit(sourceFile);
209
+ const name = defaultName ?? firstValueExport ?? firstTypeExport ?? "";
210
+ return {
211
+ name,
212
+ exports: exportsOrdered,
213
+ imports,
214
+ reExports,
215
+ jsxElements,
216
+ navigations
217
+ };
218
+ }
219
+ var MUTATION_METHODS = /* @__PURE__ */ new Set([
220
+ "create",
221
+ "createMany",
222
+ "createManyAndReturn",
223
+ "update",
224
+ "updateMany",
225
+ "updateManyAndReturn",
226
+ "upsert",
227
+ "delete",
228
+ "deleteMany"
229
+ ]);
230
+ function extractDbCalls(absPath) {
231
+ const ts = getTs();
232
+ const content = (0, import_node_fs.readFileSync)(absPath, "utf-8");
233
+ const ext = (0, import_node_path.extname)(absPath);
234
+ const scriptKind = ext === ".tsx" ? ts.ScriptKind.TSX : ts.ScriptKind.TS;
235
+ const sourceFile = ts.createSourceFile(absPath, content, ts.ScriptTarget.Latest, true, scriptKind);
236
+ const calls = [];
237
+ const seen = /* @__PURE__ */ new Set();
238
+ function visit(node) {
239
+ if (ts.isCallExpression(node)) {
240
+ const expr = node.expression;
241
+ if (ts.isPropertyAccessExpression(expr) && ts.isPropertyAccessExpression(expr.expression) && ts.isIdentifier(expr.expression.expression) && expr.expression.expression.text === "db") {
242
+ const model = expr.expression.name.text;
243
+ const method = expr.name.text;
244
+ const key = `${model}.${method}`;
245
+ if (!seen.has(key)) {
246
+ seen.add(key);
247
+ calls.push({
248
+ model,
249
+ method,
250
+ isMutation: MUTATION_METHODS.has(method)
251
+ });
252
+ }
253
+ }
254
+ }
255
+ ts.forEachChild(node, visit);
256
+ }
257
+ visit(sourceFile);
258
+ return calls;
259
+ }
260
+ function extractAuthWrappers(absPath) {
261
+ const ts = getTs();
262
+ const content = (0, import_node_fs.readFileSync)(absPath, "utf-8");
263
+ const ext = (0, import_node_path.extname)(absPath);
264
+ const scriptKind = ext === ".tsx" ? ts.ScriptKind.TSX : ts.ScriptKind.TS;
265
+ const sourceFile = ts.createSourceFile(absPath, content, ts.ScriptTarget.Latest, true, scriptKind);
266
+ const wrappers = /* @__PURE__ */ new Set();
267
+ const AUTH_WRAPPERS = /* @__PURE__ */ new Set(["withAuth", "withPermission", "withRole", "requireAuth"]);
268
+ function visit(node) {
269
+ if (ts.isCallExpression(node) && ts.isIdentifier(node.expression)) {
270
+ if (AUTH_WRAPPERS.has(node.expression.text)) {
271
+ wrappers.add(node.expression.text);
272
+ }
273
+ }
274
+ ts.forEachChild(node, visit);
275
+ }
276
+ visit(sourceFile);
277
+ return wrappers;
278
+ }
279
+
280
+ // src/server/graph/parsers/ui/react-nextjs.ts
281
+ var RENDER_TYPES = /* @__PURE__ */ new Set(["component", "ui", "layout", "context"]);
282
+ function walk(dir, exts) {
283
+ const results = [];
284
+ if (!(0, import_node_fs2.existsSync)(dir)) return results;
285
+ for (const entry of (0, import_node_fs2.readdirSync)(dir, { withFileTypes: true })) {
286
+ const full = (0, import_node_path2.join)(dir, entry.name);
287
+ if (entry.isDirectory()) {
288
+ results.push(...walk(full, exts));
289
+ } else if (exts.includes((0, import_node_path2.extname)(entry.name))) {
290
+ results.push(full);
291
+ }
292
+ }
293
+ return results;
294
+ }
295
+ function toNodeId(srcDir, absPath) {
296
+ return (0, import_node_path2.relative)(srcDir, absPath).replace(/\\/g, "/");
297
+ }
298
+ function resolveImport(srcDir, specifier) {
299
+ if (!specifier.startsWith("@/")) return null;
300
+ const rel = specifier.slice(2);
301
+ const base = (0, import_node_path2.join)(srcDir, rel);
302
+ for (const c of [base, base + ".ts", base + ".tsx", (0, import_node_path2.join)(base, "index.ts"), (0, import_node_path2.join)(base, "index.tsx")]) {
303
+ if ((0, import_node_fs2.existsSync)(c) && (0, import_node_fs2.statSync)(c).isFile()) return c;
304
+ }
305
+ return null;
306
+ }
307
+ function resolveRelativeImport(fromFile, specifier) {
308
+ const base = (0, import_node_path2.join)((0, import_node_path2.dirname)(fromFile), specifier);
309
+ for (const c of [base, base + ".ts", base + ".tsx", (0, import_node_path2.join)(base, "index.ts"), (0, import_node_path2.join)(base, "index.tsx")]) {
310
+ if ((0, import_node_fs2.existsSync)(c) && (0, import_node_fs2.statSync)(c).isFile()) return c;
311
+ }
312
+ return null;
313
+ }
314
+ function buildAllBarrelMaps(srcDir, parsedByPath) {
315
+ const barrels = /* @__PURE__ */ new Map();
316
+ for (const [absPath, parsed] of parsedByPath) {
317
+ const bn = (0, import_node_path2.basename)(absPath);
318
+ if (bn !== "index.ts" && bn !== "index.tsx") continue;
319
+ if (parsed.reExports.length === 0) continue;
320
+ const barrelId = (0, import_node_path2.relative)(srcDir, (0, import_node_path2.dirname)(absPath)).replace(/\\/g, "/");
321
+ const map = /* @__PURE__ */ new Map();
322
+ for (const re of parsed.reExports) {
323
+ if (!re.from.startsWith(".")) continue;
324
+ const resolved = resolveRelativeImport(absPath, re.from);
325
+ if (resolved) map.set(re.name, resolved);
326
+ }
327
+ if (map.size > 0) barrels.set(barrelId, map);
328
+ }
329
+ return barrels;
330
+ }
331
+ function classifyType(id) {
332
+ if (id.endsWith("/page.tsx")) return "page";
333
+ if (id.endsWith("/layout.tsx")) return "layout";
334
+ if (id.startsWith("client/components/ui/")) return "ui";
335
+ if (id.startsWith("client/components/")) return "component";
336
+ if (id.startsWith("client/hooks/")) return "hook";
337
+ if (/client\/lib\/.*-context\./.test(id)) return "context";
338
+ if (id.startsWith("client/lib/")) return id.includes("config") ? "config" : "util";
339
+ if (id.startsWith("client/api/")) return "util";
340
+ if (id.startsWith("server/mcp/")) return "mcp-tool";
341
+ if (id.startsWith("server/lib/")) return "lib";
342
+ if (id.startsWith("server/")) return "lib";
343
+ if (id.startsWith("lib/") || id.startsWith("config/")) return "lib";
344
+ return "component";
345
+ }
346
+ function classifyModule(id) {
347
+ if (/app\/\(auth\)\//.test(id)) return "auth";
348
+ if (/app\/\(admin\)\//.test(id)) return "admin";
349
+ if (/app\/\(settings\)\//.test(id)) return "settings";
350
+ if (/app\/\(app\)\/\[orgSlug\]\/\(project-pages\)\//.test(id)) return "project";
351
+ if (/app\/\(app\)\/\[orgSlug\]\/\(org-pages\)\//.test(id)) return "org";
352
+ if (/app\/\(app\)\/\[orgSlug\]\//.test(id)) return "org";
353
+ if (id.startsWith("app/integrations/")) return "integrations";
354
+ if (id.startsWith("app/docs/")) return "admin";
355
+ if (id.startsWith("client/components/ui/")) return "shared-ui";
356
+ if (id.startsWith("client/components/layout/") || /client\/lib\/navigation/.test(id)) return "layout";
357
+ if (/client\/components\/auth\//.test(id) || /client\/lib\/auth-/.test(id) || /client\/lib\/github-oauth/.test(id) || /client\/lib\/permission-service/.test(id) || /client\/hooks\/use-permissions/.test(id)) return "auth";
358
+ if (/client\/components\/prd-/.test(id) || /client\/hooks\/use-admin/.test(id)) return "admin";
359
+ if (/client\/components\/org-/.test(id) || /client\/hooks\/use-org-/.test(id) || /client\/hooks\/use-provider-def/.test(id)) return "org";
360
+ if (/client\/components\/project/.test(id) || /client\/hooks\/use-project-/.test(id) || /client\/hooks\/use-pipeline/.test(id) || /client\/hooks\/use-databases/.test(id) || /client\/hooks\/use-provider-env/.test(id) || /client\/hooks\/use-role-assign/.test(id) || /client\/components\/pipeline/.test(id) || /client\/components\/deployments/.test(id)) return "project";
361
+ if (/client\/hooks\/use-(profile|sessions|organizations|notification)/.test(id)) return "settings";
362
+ if (id.startsWith("server/auth/")) return "auth";
363
+ if (id.startsWith("server/mcp/")) return "mcp";
364
+ if (id.startsWith("server/lib/")) return "server-lib";
365
+ if (id.startsWith("server/middleware")) return "middleware";
366
+ if (id.startsWith("server/services/")) return "services";
367
+ if (id.startsWith("server/db")) return "db";
368
+ if (id.startsWith("server/errors")) return "errors";
369
+ if (id.startsWith("server/")) return "server-lib";
370
+ if (id.startsWith("config/")) return "config";
371
+ if (id.startsWith("lib/")) return "lib";
372
+ return "root";
373
+ }
374
+ function extractRoute(id) {
375
+ if (!id.endsWith("/page.tsx")) return null;
376
+ let route = id.replace(/^app\//, "/").replace(/\/page\.tsx$/, "");
377
+ route = route.replace(/\/\([^)]+\)/g, "");
378
+ route = route.replace(/\[([^\]]+)\]/g, ":$1");
379
+ route = route.replace(/\/+/g, "/");
380
+ if (!route.startsWith("/")) route = "/" + route;
381
+ return route || "/";
382
+ }
383
+ function nameFromFilename(absPath) {
384
+ return (0, import_node_path2.basename)(absPath, (0, import_node_path2.extname)(absPath)).replace(/[-_](\w)/g, (_, c) => c.toUpperCase()).replace(/^(\w)/, (_, c) => c.toUpperCase());
385
+ }
386
+ function resolveTemplateLiteralRoute(template, routeToNodeId) {
387
+ const parameterized = template.replace(/\$\{([^}]+)\}/g, (_, expr) => {
388
+ const cleaned = expr.trim();
389
+ if (cleaned.includes(".")) {
390
+ const parts = cleaned.split(".");
391
+ const last = parts[parts.length - 1];
392
+ const secondLast = parts.length > 1 ? parts[parts.length - 2] : "";
393
+ if (last === "slug" && secondLast === "project") return ":projectSlug";
394
+ if (last === "slug") return ":projectSlug";
395
+ if (last === "id" && /cred/i.test(secondLast)) return ":credentialId";
396
+ if (last === "id" && /run/i.test(secondLast)) return ":runId";
397
+ if (last === "sha") return ":commitSha";
398
+ if (last === "id") return ":id";
399
+ return `:${last}`;
400
+ }
401
+ if (/orgSlug/i.test(cleaned)) return ":orgSlug";
402
+ if (/projectSlug/i.test(cleaned)) return ":projectSlug";
403
+ if (/runId/i.test(cleaned)) return ":runId";
404
+ if (/credentialId/i.test(cleaned)) return ":credentialId";
405
+ if (/commitSha/i.test(cleaned)) return ":commitSha";
406
+ if (/token/i.test(cleaned)) return ":token";
407
+ return `:${cleaned}`;
408
+ });
409
+ const normalized = parameterized.replace(/\/+/g, "/").replace(/\/$/, "") || "/";
410
+ if (routeToNodeId.has(normalized)) return routeToNodeId.get(normalized);
411
+ let bestScore = -1;
412
+ let bestId = null;
413
+ for (const [route, nodeId] of routeToNodeId) {
414
+ const score = routeMatchScore(normalized, route);
415
+ if (score > bestScore) {
416
+ bestScore = score;
417
+ bestId = nodeId;
418
+ }
419
+ }
420
+ return bestScore > 0 ? bestId : null;
421
+ }
422
+ function routeMatchScore(candidate, known) {
423
+ const segsA = candidate.split("/");
424
+ const segsB = known.split("/");
425
+ if (segsA.length !== segsB.length) return -1;
426
+ let score = 0;
427
+ for (let i = 0; i < segsA.length; i++) {
428
+ const a = segsA[i], b = segsB[i];
429
+ if (a === b) {
430
+ score += 3;
431
+ continue;
432
+ }
433
+ if (a.startsWith(":") && b.startsWith(":")) {
434
+ score += 2;
435
+ continue;
436
+ }
437
+ if (a.startsWith(":") || b.startsWith(":")) {
438
+ score += 0;
439
+ continue;
440
+ }
441
+ return -1;
442
+ }
443
+ return score;
444
+ }
445
+ function templateToRoute(template) {
446
+ return template.replace(/\$\{([^}]+)\}/g, (_, expr) => {
447
+ const cleaned = expr.trim();
448
+ if (cleaned.includes(".")) {
449
+ const parts = cleaned.split(".");
450
+ const last = parts[parts.length - 1];
451
+ const secondLast = parts.length > 1 ? parts[parts.length - 2] : "";
452
+ if (last === "slug" && /project/i.test(secondLast)) return ":projectSlug";
453
+ if (last === "slug") return ":slug";
454
+ if (last === "sha") return ":commitSha";
455
+ return `:${last}`;
456
+ }
457
+ return `:${cleaned}`;
458
+ });
459
+ }
460
+ function matchRouteToPage(route, routeToNodeId) {
461
+ const normalized = route.replace(/\/$/, "") || "/";
462
+ if (routeToNodeId.has(normalized)) return routeToNodeId.get(normalized);
463
+ return null;
464
+ }
465
+ function extractEdges(srcDir, absPath, sourceId, parsed, nodeIdSet, nodeTypeMap, barrelMaps, routeToNodeId) {
466
+ const edges = [];
467
+ const flagged = [];
468
+ const seen = /* @__PURE__ */ new Set();
469
+ function addEdge(target, type, label) {
470
+ const key = `${sourceId}\u2192${target}\u2192${type}`;
471
+ if (seen.has(key)) return;
472
+ seen.add(key);
473
+ const edge = { source: sourceId, target, type };
474
+ if (label) edge.label = label;
475
+ edges.push(edge);
476
+ }
477
+ function edgeTypeFor(targetId, isTypeOnlyImport, importedNames) {
478
+ if (isTypeOnlyImport) return "imports";
479
+ const targetType = nodeTypeMap.get(targetId);
480
+ if (targetType && RENDER_TYPES.has(targetType)) {
481
+ const anyRendered = importedNames.some((n) => parsed.jsxElements.has(n));
482
+ if (anyRendered) return "renders";
483
+ }
484
+ return "imports";
485
+ }
486
+ for (const imp of parsed.imports) {
487
+ const { names, specifier, isTypeOnly, typeNames } = imp;
488
+ if (specifier.startsWith("@/")) {
489
+ const relToSrc = specifier.slice(2);
490
+ const barrelMap = barrelMaps.get(relToSrc);
491
+ if (barrelMap && names.length > 0) {
492
+ const byTarget = /* @__PURE__ */ new Map();
493
+ for (const name of names) {
494
+ const targetAbs = barrelMap.get(name);
495
+ if (targetAbs) {
496
+ const targetId = toNodeId(srcDir, targetAbs);
497
+ if (nodeIdSet.has(targetId)) {
498
+ if (!byTarget.has(targetId)) byTarget.set(targetId, []);
499
+ byTarget.get(targetId).push(name);
500
+ }
501
+ }
502
+ }
503
+ for (const [targetId, targetNames] of byTarget) {
504
+ const allType = isTypeOnly || targetNames.every((n) => typeNames.has(n));
505
+ addEdge(targetId, edgeTypeFor(targetId, allType, targetNames));
506
+ }
507
+ } else {
508
+ const resolved = resolveImport(srcDir, specifier);
509
+ if (resolved) {
510
+ const targetId = toNodeId(srcDir, resolved);
511
+ if (nodeIdSet.has(targetId) && !targetId.endsWith("/index.ts") && !targetId.endsWith("/index.tsx")) {
512
+ addEdge(targetId, edgeTypeFor(targetId, isTypeOnly, names));
513
+ }
514
+ }
515
+ }
516
+ } else if (specifier.startsWith(".")) {
517
+ const resolved = resolveRelativeImport(absPath, specifier);
518
+ if (resolved) {
519
+ const targetId = toNodeId(srcDir, resolved);
520
+ if (nodeIdSet.has(targetId) && !targetId.endsWith("/index.ts") && !targetId.endsWith("/index.tsx")) {
521
+ addEdge(targetId, edgeTypeFor(targetId, isTypeOnly, names));
522
+ }
523
+ }
524
+ }
525
+ }
526
+ for (const nav of parsed.navigations) {
527
+ if (nav.kind === "window-location") {
528
+ flagged.push({
529
+ source: sourceId,
530
+ target: "EXTERNAL",
531
+ type: "navigates",
532
+ label: `window.location to ${nav.target}`,
533
+ confidence: "high"
534
+ });
535
+ continue;
536
+ }
537
+ if (!nav.isTemplate) {
538
+ const targetId = matchRouteToPage(nav.target, routeToNodeId);
539
+ if (targetId && targetId !== sourceId) {
540
+ const label = nav.kind === "link-href" ? `Link to ${nav.target}` : `router.${nav.kind === "router-push" ? "push" : "replace"}('${nav.target}')`;
541
+ addEdge(targetId, "navigates", label);
542
+ }
543
+ } else {
544
+ const template = nav.target.replace(/^`|`$/g, "");
545
+ if (!template.includes("${")) continue;
546
+ const targetId = resolveTemplateLiteralRoute(template, routeToNodeId);
547
+ if (targetId && targetId !== sourceId) {
548
+ const label = nav.kind === "link-href" ? `Link to ${templateToRoute(template)}` : `router.${nav.kind === "router-push" ? "push" : "replace"}('${templateToRoute(template)}')`;
549
+ addEdge(targetId, "navigates", label);
550
+ } else {
551
+ flagged.push({
552
+ source: sourceId,
553
+ target: "DYNAMIC",
554
+ type: "navigates",
555
+ label: nav.kind === "link-href" ? `Link with template: \`${template}\`` : `router.${nav.kind === "router-push" ? "push" : "replace"} with template: \`${template}\``,
556
+ confidence: "medium"
557
+ });
558
+ }
559
+ }
560
+ }
561
+ return { edges, flagged };
562
+ }
563
+ function detect(rootDir) {
564
+ return (0, import_node_fs2.existsSync)((0, import_node_path2.join)(rootDir, "src", "app")) && (0, import_node_fs2.existsSync)((0, import_node_path2.join)(rootDir, "next.config.ts")) || (0, import_node_fs2.existsSync)((0, import_node_path2.join)(rootDir, "next.config.js")) || (0, import_node_fs2.existsSync)((0, import_node_path2.join)(rootDir, "next.config.mjs"));
565
+ }
566
+ function generate(rootDir) {
567
+ const srcDir = (0, import_node_path2.join)(rootDir, "src");
568
+ const appFiles = walk((0, import_node_path2.join)(srcDir, "app"), [".tsx", ".ts"]).filter(
569
+ (f) => f.endsWith("/page.tsx") || f.endsWith("/layout.tsx")
570
+ );
571
+ const clientFiles = walk((0, import_node_path2.join)(srcDir, "client"), [".tsx", ".ts"]);
572
+ const serverFiles = walk((0, import_node_path2.join)(srcDir, "server"), [".ts", ".tsx"]).filter(
573
+ (f) => (0, import_node_path2.basename)(f) !== "route.ts" && (0, import_node_path2.basename)(f) !== "route.tsx"
574
+ );
575
+ const libFiles = walk((0, import_node_path2.join)(srcDir, "lib"), [".ts", ".tsx"]);
576
+ const configFiles = walk((0, import_node_path2.join)(srcDir, "config"), [".ts", ".tsx"]);
577
+ const allDiscovered = [...appFiles, ...clientFiles, ...serverFiles, ...libFiles, ...configFiles];
578
+ const parsedByPath = /* @__PURE__ */ new Map();
579
+ for (const absPath of allDiscovered) {
580
+ parsedByPath.set(absPath, parseFile(absPath));
581
+ }
582
+ const barrelMaps = buildAllBarrelMaps(srcDir, parsedByPath);
583
+ const fileSet = allDiscovered.filter((f) => !(0, import_node_path2.basename)(f).startsWith("index."));
584
+ const nodes = [];
585
+ const nodeIdSet = /* @__PURE__ */ new Set();
586
+ const nodeTypeMap = /* @__PURE__ */ new Map();
587
+ const routeToNodeId = /* @__PURE__ */ new Map();
588
+ for (const absPath of fileSet) {
589
+ const id = toNodeId(srcDir, absPath);
590
+ const type = classifyType(id);
591
+ const parsed = parsedByPath.get(absPath);
592
+ const name = parsed.name || nameFromFilename(absPath);
593
+ const route = extractRoute(id);
594
+ const module_ = classifyModule(id);
595
+ nodes.push({ id, type, name, route, module: module_, exports: parsed.exports });
596
+ nodeIdSet.add(id);
597
+ nodeTypeMap.set(id, type);
598
+ if (route) routeToNodeId.set(route, id);
599
+ }
600
+ const allEdges = [];
601
+ const allFlagged = [];
602
+ for (const absPath of fileSet) {
603
+ const sourceId = toNodeId(srcDir, absPath);
604
+ const parsed = parsedByPath.get(absPath);
605
+ const { edges, flagged } = extractEdges(
606
+ srcDir,
607
+ absPath,
608
+ sourceId,
609
+ parsed,
610
+ nodeIdSet,
611
+ nodeTypeMap,
612
+ barrelMaps,
613
+ routeToNodeId
614
+ );
615
+ allEdges.push(...edges);
616
+ allFlagged.push(...flagged);
617
+ }
618
+ const flaggedSet = /* @__PURE__ */ new Set();
619
+ const dedupedFlagged = allFlagged.filter((f) => {
620
+ const key = `${f.source}\u2192${f.target}\u2192${f.label}`;
621
+ if (flaggedSet.has(key)) return false;
622
+ flaggedSet.add(key);
623
+ return true;
624
+ });
625
+ const typePriority = {
626
+ layout: 0,
627
+ page: 1,
628
+ component: 2,
629
+ ui: 3,
630
+ context: 4,
631
+ config: 5,
632
+ util: 6,
633
+ hook: 7,
634
+ lib: 8
635
+ };
636
+ nodes.sort((a, b) => (typePriority[a.type] ?? 99) - (typePriority[b.type] ?? 99) || a.id.localeCompare(b.id));
637
+ allEdges.sort((a, b) => a.source.localeCompare(b.source) || a.target.localeCompare(b.target));
638
+ const byType = (t) => nodes.filter((n) => n.type === t).length;
639
+ const stats = {
640
+ total_pages: byType("page"),
641
+ total_layouts: byType("layout"),
642
+ total_components: byType("component"),
643
+ total_ui: byType("ui"),
644
+ total_hooks: byType("hook"),
645
+ total_contexts: byType("context"),
646
+ total_configs: byType("config"),
647
+ total_utils: byType("util"),
648
+ total_libs: byType("lib"),
649
+ total_edges: allEdges.length,
650
+ total_flagged: dedupedFlagged.length
651
+ };
652
+ return {
653
+ metadata: {
654
+ generated: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
655
+ scope: "main-app-only",
656
+ app_root: "src/",
657
+ layer: "ui",
658
+ parser: "react-nextjs-ast",
659
+ ...stats,
660
+ notes: "Auto-generated via TypeScript AST \u2014 edges derived from actual imports, renders from JSX usage, navigations from router/Link calls."
661
+ },
662
+ nodes,
663
+ edges: allEdges,
664
+ cross_refs: [],
665
+ contradictions: [],
666
+ warnings: [],
667
+ flagged_edges: dedupedFlagged,
668
+ patterns: {
669
+ total_nodes: nodes.length,
670
+ by_type: stats,
671
+ by_edge_type: {
672
+ renders: allEdges.filter((e) => e.type === "renders").length,
673
+ imports: allEdges.filter((e) => e.type === "imports").length,
674
+ navigates: allEdges.filter((e) => e.type === "navigates").length
675
+ }
676
+ }
677
+ };
678
+ }
679
+ var reactNextjsParser = {
680
+ id: "react-nextjs",
681
+ layer: "ui",
682
+ detect,
683
+ generate
684
+ };
685
+
686
+ // src/server/graph/parsers/api/nextjs-routes.ts
687
+ var import_node_fs3 = require("node:fs");
688
+ var import_node_path3 = require("node:path");
689
+ var HTTP_METHODS = /* @__PURE__ */ new Set(["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"]);
690
+ function walk2(dir) {
691
+ const results = [];
692
+ if (!(0, import_node_fs3.existsSync)(dir)) return results;
693
+ for (const entry of (0, import_node_fs3.readdirSync)(dir, { withFileTypes: true })) {
694
+ const full = (0, import_node_path3.join)(dir, entry.name);
695
+ if (entry.isDirectory()) {
696
+ results.push(...walk2(full));
697
+ } else if (entry.name === "route.ts" || entry.name === "route.tsx") {
698
+ results.push(full);
699
+ }
700
+ }
701
+ return results;
702
+ }
703
+ function filePathToRoute(apiDir, absPath) {
704
+ let route = "/" + (0, import_node_path3.relative)(apiDir, absPath).replace(/\\/g, "/").replace(/\/route\.tsx?$/, "");
705
+ route = route.replace(/\[([^\]]+)\]/g, ":$1");
706
+ route = route.replace(/\/+/g, "/");
707
+ if (route === "/") return "/api";
708
+ return "/api" + route;
709
+ }
710
+ function camelToPascal(s) {
711
+ if (!s) return s;
712
+ return s.charAt(0).toUpperCase() + s.slice(1);
713
+ }
714
+ function detect2(rootDir) {
715
+ return (0, import_node_fs3.existsSync)((0, import_node_path3.join)(rootDir, "src", "app", "api"));
716
+ }
717
+ function generate2(rootDir) {
718
+ const apiDir = (0, import_node_path3.join)(rootDir, "src", "app", "api");
719
+ const routeFiles = walk2(apiDir);
720
+ const nodes = [];
721
+ const edges = [];
722
+ const crossRefs = [];
723
+ const mutatorCount = {};
724
+ const authUsage = {};
725
+ let endpointsWithAuth = 0;
726
+ let endpointsWithDbAccess = 0;
727
+ for (const absPath of routeFiles) {
728
+ const parsed = parseFile(absPath);
729
+ const dbCalls = extractDbCalls(absPath);
730
+ const authWrappers = extractAuthWrappers(absPath);
731
+ const methods = [];
732
+ for (const exp of parsed.exports) {
733
+ if (HTTP_METHODS.has(exp)) methods.push(exp);
734
+ }
735
+ const routePath = filePathToRoute(apiDir, absPath);
736
+ const relPath = (0, import_node_path3.relative)(rootDir, absPath).replace(/\\/g, "/");
737
+ const mutations = dbCalls.filter((c) => c.isMutation);
738
+ const reads = dbCalls.filter((c) => !c.isMutation);
739
+ const mutates = mutations.length > 0;
740
+ if (mutates) {
741
+ for (const m of mutations) {
742
+ mutatorCount[m.method] = (mutatorCount[m.method] ?? 0) + 1;
743
+ }
744
+ }
745
+ if (dbCalls.length > 0) endpointsWithDbAccess++;
746
+ const authStrategy = [];
747
+ for (const w of authWrappers) {
748
+ authStrategy.push(w);
749
+ authUsage[w] = (authUsage[w] ?? 0) + 1;
750
+ }
751
+ if (authStrategy.length > 0) endpointsWithAuth++;
752
+ nodes.push({
753
+ id: relPath,
754
+ type: "endpoint",
755
+ name: routePath,
756
+ path: routePath,
757
+ methods,
758
+ handler: relPath,
759
+ // Behavioral classification from handler body (AST-derived).
760
+ mutates,
761
+ auth: authStrategy.length > 0 ? authStrategy : ["public"],
762
+ db_models: [...new Set(dbCalls.map((c) => c.model))],
763
+ db_operations: [...new Set(dbCalls.map((c) => `${c.model}.${c.method}`))]
764
+ });
765
+ const seenModels = /* @__PURE__ */ new Set();
766
+ for (const call of dbCalls) {
767
+ if (seenModels.has(call.model)) continue;
768
+ seenModels.add(call.model);
769
+ crossRefs.push({
770
+ source: relPath,
771
+ target: camelToPascal(call.model),
772
+ type: call.isMutation ? "mutates" : "reads",
773
+ layer: "db"
774
+ });
775
+ }
776
+ }
777
+ nodes.sort((a, b) => a.path.localeCompare(b.path));
778
+ crossRefs.sort((a, b) => a.source.localeCompare(b.source) || a.target.localeCompare(b.target));
779
+ const mutatorNodes = nodes.filter((n) => n.mutates).length;
780
+ const readOnlyNodes = nodes.filter((n) => !n.mutates).length;
781
+ return {
782
+ metadata: {
783
+ generated: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
784
+ scope: "main-app-only",
785
+ stack: "nextjs-app-router",
786
+ layer: "api",
787
+ parser: "nextjs-routes-ast",
788
+ total_endpoints: nodes.length,
789
+ total_methods: nodes.reduce((sum, n) => sum + n.methods.length, 0),
790
+ endpoints_with_auth: endpointsWithAuth,
791
+ endpoints_with_db_access: endpointsWithDbAccess,
792
+ mutator_endpoints: mutatorNodes,
793
+ read_only_endpoints: readOnlyNodes
794
+ },
795
+ nodes,
796
+ edges,
797
+ cross_refs: crossRefs,
798
+ contradictions: [],
799
+ warnings: [],
800
+ flagged_edges: [],
801
+ patterns: {
802
+ total_endpoints: nodes.length,
803
+ methods_breakdown: [...HTTP_METHODS].reduce((acc, m) => {
804
+ acc[m] = nodes.filter((n) => n.methods.includes(m)).length;
805
+ return acc;
806
+ }, {}),
807
+ auth_strategies: authUsage,
808
+ mutation_operations: mutatorCount,
809
+ mutator_vs_reader: { mutators: mutatorNodes, readers: readOnlyNodes }
810
+ }
811
+ };
812
+ }
813
+ var nextjsRoutesParser = {
814
+ id: "nextjs-routes",
815
+ layer: "api",
816
+ detect: detect2,
817
+ generate: generate2
818
+ };
819
+
820
+ // src/server/graph/parsers/db/prisma-schema.ts
821
+ var import_node_fs4 = require("node:fs");
822
+ var import_node_path4 = require("node:path");
823
+ function parseModels(content) {
824
+ const nodes = [];
825
+ const relations = [];
826
+ const modelRe = /model\s+(\w+)\s*\{([^}]+)\}/g;
827
+ let m;
828
+ while ((m = modelRe.exec(content)) !== null) {
829
+ const modelName = m[1];
830
+ const body = m[2];
831
+ const columns = [];
832
+ const lines = body.split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("//") && !l.startsWith("@@"));
833
+ for (const line of lines) {
834
+ const fieldMatch = line.match(/^(\w+)\s+(\S+)(.*)/);
835
+ if (!fieldMatch) continue;
836
+ const fieldName = fieldMatch[1];
837
+ const fieldType = fieldMatch[2];
838
+ const rest = fieldMatch[3] ?? "";
839
+ const commentMatch = rest.match(/\/\/\s*(.+)$/);
840
+ const comment = commentMatch ? commentMatch[1].trim() : null;
841
+ const restNoComment = commentMatch ? rest.slice(0, commentMatch.index).trim() : rest;
842
+ const isPrimary = restNoComment.includes("@id");
843
+ const isUnique = restNoComment.includes("@unique");
844
+ const isNullable = fieldType.endsWith("?");
845
+ const baseType = fieldType.replace(/[?\[\]]/g, "");
846
+ const defaultMatch = restNoComment.match(/@default\(([^)]+)\)/);
847
+ const defaultVal = defaultMatch ? defaultMatch[1] : null;
848
+ const relationMatch = restNoComment.match(/@relation\(([^)]*)\)/);
849
+ const isRelationField = !!relationMatch;
850
+ columns.push({
851
+ name: fieldName,
852
+ type: fieldType,
853
+ primary: isPrimary,
854
+ unique: isUnique,
855
+ nullable: isNullable,
856
+ default: defaultVal,
857
+ isRelation: isRelationField,
858
+ comment
859
+ });
860
+ if (relationMatch) {
861
+ const relArgs = relationMatch[1];
862
+ const fieldsMatch = relArgs.match(/fields:\s*\[([^\]]+)\]/);
863
+ const refsMatch = relArgs.match(/references:\s*\[([^\]]+)\]/);
864
+ const onDeleteMatch = relArgs.match(/onDelete:\s*(\w+)/);
865
+ if (fieldsMatch && refsMatch) {
866
+ const fk = fieldsMatch[1].trim();
867
+ relations.push({
868
+ source: modelName,
869
+ target: baseType,
870
+ type: "belongs_to",
871
+ fk,
872
+ onDelete: onDeleteMatch ? onDeleteMatch[1] : null
873
+ });
874
+ }
875
+ }
876
+ if (fieldType.endsWith("[]") && !relationMatch) {
877
+ relations.push({
878
+ source: modelName,
879
+ target: baseType,
880
+ type: "has_many",
881
+ fk: null,
882
+ onDelete: null
883
+ });
884
+ }
885
+ }
886
+ nodes.push({
887
+ id: modelName,
888
+ type: "table",
889
+ name: modelName,
890
+ columns: columns.filter((c) => !c.isRelation || c.primary)
891
+ });
892
+ }
893
+ return { nodes, relations };
894
+ }
895
+ function parseEnums(content) {
896
+ const nodes = [];
897
+ const enumRe = /enum\s+(\w+)\s*\{([^}]+)\}/g;
898
+ let m;
899
+ while ((m = enumRe.exec(content)) !== null) {
900
+ const enumName = m[1];
901
+ const body = m[2];
902
+ const values = body.split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("//"));
903
+ nodes.push({
904
+ id: enumName,
905
+ type: "enum",
906
+ name: enumName,
907
+ values
908
+ });
909
+ }
910
+ return nodes;
911
+ }
912
+ function detect3(rootDir) {
913
+ return (0, import_node_fs4.existsSync)((0, import_node_path4.join)(rootDir, "prisma", "schema.prisma"));
914
+ }
915
+ function generate3(rootDir) {
916
+ const schemaPath = (0, import_node_path4.join)(rootDir, "prisma", "schema.prisma");
917
+ const content = (0, import_node_fs4.readFileSync)(schemaPath, "utf-8");
918
+ const { nodes: modelNodes, relations } = parseModels(content);
919
+ const enumNodes = parseEnums(content);
920
+ const allNodes = [...modelNodes, ...enumNodes];
921
+ const edges = relations.map((r) => ({
922
+ source: r.source,
923
+ target: r.target,
924
+ type: r.type,
925
+ fk: r.fk,
926
+ onDelete: r.onDelete
927
+ }));
928
+ allNodes.sort((a, b) => {
929
+ if (a.type !== b.type) return a.type === "table" ? -1 : 1;
930
+ return a.name.localeCompare(b.name);
931
+ });
932
+ edges.sort((a, b) => a.source.localeCompare(b.source) || a.target.localeCompare(b.target));
933
+ return {
934
+ metadata: {
935
+ generated: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
936
+ scope: "prisma-schema",
937
+ source: "prisma/schema.prisma",
938
+ provider: "postgresql",
939
+ layer: "db",
940
+ total_models: modelNodes.length,
941
+ total_enums: enumNodes.length,
942
+ total_relations: edges.length
943
+ },
944
+ nodes: allNodes,
945
+ edges,
946
+ cross_refs: [],
947
+ contradictions: [],
948
+ warnings: [
949
+ {
950
+ type: "schema_file_only",
951
+ detail: "Live DB introspection not yet implemented. Graph derived from prisma/schema.prisma."
952
+ }
953
+ ],
954
+ flagged_edges: [],
955
+ patterns: {
956
+ total_tables: modelNodes.length,
957
+ total_enums: enumNodes.length,
958
+ total_relations: edges.length,
959
+ relation_types: {
960
+ belongs_to: edges.filter((e) => e.type === "belongs_to").length,
961
+ has_many: edges.filter((e) => e.type === "has_many").length
962
+ }
963
+ }
964
+ };
965
+ }
966
+ var prismaSchemaParser = {
967
+ id: "prisma-schema",
968
+ layer: "db",
969
+ detect: detect3,
970
+ generate: generate3
971
+ };
972
+
973
+ // src/server/graph/core/graph-builder.ts
974
+ var ALL_PARSERS = [
975
+ reactNextjsParser,
976
+ nextjsRoutesParser,
977
+ prismaSchemaParser
978
+ ];
979
+ function getParser(layer) {
980
+ return ALL_PARSERS.find((p) => p.layer === layer);
981
+ }
982
+ function generateLayer(rootDir, layer) {
983
+ const parser = getParser(layer);
984
+ if (!parser) return null;
985
+ if (!parser.detect(rootDir)) return null;
986
+ const output = parser.generate(rootDir);
987
+ return {
988
+ layer,
989
+ output,
990
+ nodeCount: output.nodes.length,
991
+ edgeCount: output.edges.length
992
+ };
993
+ }
994
+ function generateAll(rootDir) {
995
+ const layers = ["ui", "api", "db"];
996
+ const results = [];
997
+ for (const layer of layers) {
998
+ const result = generateLayer(rootDir, layer);
999
+ if (result) results.push(result);
1000
+ }
1001
+ return results;
1002
+ }
1003
+
1004
+ // src/server/graph/index.ts
1005
+ var GRAPHS_DIR = ".launchsecure/graphs";
1006
+ var LAYERS = ["ui", "api", "db"];
1007
+ var graphCache = /* @__PURE__ */ new Map();
1008
+ function graphsDir(rootDir) {
1009
+ return (0, import_node_path5.join)(rootDir, GRAPHS_DIR);
1010
+ }
1011
+ function graphFilePath(rootDir, layer) {
1012
+ return (0, import_node_path5.join)(graphsDir(rootDir), `${layer}.json`);
1013
+ }
1014
+ function invalidateCache(filePath) {
1015
+ graphCache.delete(filePath);
1016
+ }
1017
+ function readGraph(rootDir, layer) {
1018
+ const filePath = graphFilePath(rootDir, layer);
1019
+ if (!(0, import_node_fs5.existsSync)(filePath)) return null;
1020
+ const stat = (0, import_node_fs5.statSync)(filePath);
1021
+ const cached = graphCache.get(filePath);
1022
+ if (cached && cached.mtimeMs === stat.mtimeMs) {
1023
+ return cached.graph;
1024
+ }
1025
+ const content = (0, import_node_fs5.readFileSync)(filePath, "utf-8");
1026
+ const graph = JSON.parse(content);
1027
+ graphCache.set(filePath, { mtimeMs: stat.mtimeMs, graph });
1028
+ return graph;
1029
+ }
1030
+ function readAllGraphs(rootDir) {
1031
+ const result = {};
1032
+ for (const layer of LAYERS) {
1033
+ const graph = readGraph(rootDir, layer);
1034
+ if (graph) result[layer] = graph;
1035
+ }
1036
+ return result;
1037
+ }
1038
+ function generateGraph(rootDir, layer) {
1039
+ const dir = graphsDir(rootDir);
1040
+ (0, import_node_fs5.mkdirSync)(dir, { recursive: true });
1041
+ const results = layer ? [generateLayer(rootDir, layer)].filter((r) => r !== null) : generateAll(rootDir);
1042
+ for (const result of results) {
1043
+ const filePath = graphFilePath(rootDir, result.layer);
1044
+ (0, import_node_fs5.writeFileSync)(filePath, JSON.stringify(result.output, null, 2) + "\n", "utf-8");
1045
+ invalidateCache(filePath);
1046
+ }
1047
+ return results;
1048
+ }
1049
+
1050
+ // src/server/graph-mcp.ts
1051
+ var SERVER_INFO = {
1052
+ name: "launchsecure-graph",
1053
+ version: "0.0.1"
1054
+ };
1055
+ var TOOLS = [
1056
+ {
1057
+ name: "generate_graph",
1058
+ description: "Regenerate the structural project graph by scanning source code in the current working directory. Parses three layers: UI (React/Next.js pages, layouts, components, hooks, imports/renders/navigation edges), API (Next.js App Router endpoints with HTTP methods), DB (Prisma schema models, enums, belongs_to/has_many relations). Writes JSON to .launchsecure/graphs/{layer}.json and returns node/edge counts. Run this when project structure has changed (new files, moved components, schema updates). Fast: typically <1s. After generation, use read_graph with filters to query.",
1059
+ inputSchema: {
1060
+ type: "object",
1061
+ properties: {
1062
+ layer: {
1063
+ type: "string",
1064
+ enum: ["ui", "api", "db"],
1065
+ description: "Specific layer to regenerate. Omit to regenerate all detectable layers."
1066
+ }
1067
+ }
1068
+ }
1069
+ },
1070
+ {
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.',
1073
+ inputSchema: {
1074
+ type: "object",
1075
+ properties: {
1076
+ layer: {
1077
+ type: "string",
1078
+ enum: ["ui", "api", "db"],
1079
+ description: "Graph layer to query: ui, api, or db. Required if any filter is provided."
1080
+ },
1081
+ search: {
1082
+ type: "string",
1083
+ description: "Case-insensitive substring match against node id, name, or route."
1084
+ },
1085
+ type: {
1086
+ type: "string",
1087
+ description: 'Filter by node type (e.g. "page", "hook", "component", "endpoint", "table").'
1088
+ },
1089
+ module: {
1090
+ type: "string",
1091
+ description: 'UI layer only \u2014 filter by module (e.g. "auth", "admin", "project", "org").'
1092
+ },
1093
+ node_id: {
1094
+ type: "string",
1095
+ description: "Center node for a neighborhood query. Returns the node + all nodes reachable within `hops` edges."
1096
+ },
1097
+ hops: {
1098
+ type: "number",
1099
+ description: "Neighborhood radius for node_id queries. Default 1 (direct neighbors only)."
1100
+ },
1101
+ minimal: {
1102
+ type: "boolean",
1103
+ description: "Return minimal node fields only (id, type, name, module, route). Default false."
1104
+ },
1105
+ queries: {
1106
+ 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.",
1108
+ items: {
1109
+ type: "object",
1110
+ properties: {
1111
+ layer: { type: "string", enum: ["ui", "api", "db"] },
1112
+ search: { type: "string" },
1113
+ type: { type: "string" },
1114
+ module: { type: "string" },
1115
+ node_id: { type: "string" },
1116
+ hops: { type: "number" },
1117
+ minimal: { type: "boolean" }
1118
+ }
1119
+ }
1120
+ }
1121
+ }
1122
+ }
1123
+ },
1124
+ {
1125
+ name: "grep_nodes",
1126
+ description: `Search for text patterns WITHIN files selected by the project graph. Combines structural filtering (type/module/neighborhood) with regex content search \u2014 narrower than plain Grep because it only scans files matching the graph filter, reducing noise from tests, docs, generated code, unrelated modules.
1127
+
1128
+ USE THIS FOR: "which auth hooks use JWT decoding", "find TODO comments in pages only", "which deployment writers call Sentry", "what validation schemas exist in form components". It's grep scoped to a structurally-selected file set.
1129
+
1130
+ REQUIRED: layer + pattern. At least one filter (search/type/module/node_id) must be set to narrow the file set \u2014 otherwise you are grepping everything and should just use Grep.
1131
+
1132
+ FILTER PARAMS (same semantics as read_graph):
1133
+ - layer: ui, api, or db
1134
+ - search: substring match on node id/name/route
1135
+ - type: node type filter
1136
+ - module: ui-layer module filter
1137
+ - node_id + hops: neighborhood scope
1138
+
1139
+ CONTENT PARAMS:
1140
+ - pattern: regex to search for (required)
1141
+ - case_insensitive: default false
1142
+ - context: lines of context around each match (default 2)
1143
+ - max_matches: cap on total matches returned (default 50)
1144
+ - max_files: cap on files searched (default 50, errors if filter returns more)
1145
+
1146
+ Returns: { pattern, filter, files_searched, total_matches, matches: [{file, line, text, context}], truncated }. Note: for db layer, all nodes map to prisma/schema.prisma so matches are deduped by file.`,
1147
+ inputSchema: {
1148
+ type: "object",
1149
+ properties: {
1150
+ layer: {
1151
+ type: "string",
1152
+ enum: ["ui", "api", "db"],
1153
+ description: "Graph layer to scope files (required)."
1154
+ },
1155
+ pattern: {
1156
+ type: "string",
1157
+ description: "Regex pattern to search for (required)."
1158
+ },
1159
+ search: { type: "string", description: "Substring match on node id/name/route." },
1160
+ type: { type: "string", description: "Filter by node type." },
1161
+ module: { type: "string", description: "UI layer only \u2014 filter by module." },
1162
+ node_id: { type: "string", description: "Center node for neighborhood scope." },
1163
+ hops: { type: "number", description: "Neighborhood radius (default 1)." },
1164
+ case_insensitive: { type: "boolean", description: "Case-insensitive regex. Default false." },
1165
+ context: { type: "number", description: "Context lines around each match. Default 2." },
1166
+ max_matches: { type: "number", description: "Max matches to return total. Default 50." },
1167
+ max_files: { type: "number", description: "Max files to search. Default 50." }
1168
+ },
1169
+ required: ["layer", "pattern"]
1170
+ }
1171
+ }
1172
+ ];
1173
+ function matchesSearch(node, query) {
1174
+ const q = query.toLowerCase();
1175
+ if (node.id.toLowerCase().includes(q)) return true;
1176
+ if (node.name.toLowerCase().includes(q)) return true;
1177
+ const route = node.route;
1178
+ if (route && route.toLowerCase().includes(q)) return true;
1179
+ return false;
1180
+ }
1181
+ function toMinimal(nodes) {
1182
+ return nodes.map((n) => {
1183
+ 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;
1187
+ return out;
1188
+ });
1189
+ }
1190
+ function neighborhood(graph, centerId, hops) {
1191
+ const center = graph.nodes.find((n) => n.id === centerId);
1192
+ if (!center) return null;
1193
+ const visited = /* @__PURE__ */ new Set([centerId]);
1194
+ let frontier = /* @__PURE__ */ new Set([centerId]);
1195
+ for (let h = 0; h < hops; h++) {
1196
+ const next = /* @__PURE__ */ new Set();
1197
+ for (const edge of graph.edges) {
1198
+ if (frontier.has(edge.source) && !visited.has(edge.target)) next.add(edge.target);
1199
+ if (frontier.has(edge.target) && !visited.has(edge.source)) next.add(edge.source);
1200
+ }
1201
+ for (const id of next) visited.add(id);
1202
+ frontier = next;
1203
+ if (frontier.size === 0) break;
1204
+ }
1205
+ const nodes = graph.nodes.filter((n) => visited.has(n.id));
1206
+ const edges = graph.edges.filter((e) => visited.has(e.source) && visited.has(e.target));
1207
+ return { nodes, edges };
1208
+ }
1209
+ function layerSummary(graph) {
1210
+ const typeCounts = {};
1211
+ const moduleCounts = {};
1212
+ for (const n of graph.nodes) {
1213
+ typeCounts[n.type] = (typeCounts[n.type] ?? 0) + 1;
1214
+ const mod = n.module;
1215
+ if (mod) moduleCounts[mod] = (moduleCounts[mod] ?? 0) + 1;
1216
+ }
1217
+ const edgeTypeCounts = {};
1218
+ for (const e of graph.edges) {
1219
+ edgeTypeCounts[e.type] = (edgeTypeCounts[e.type] ?? 0) + 1;
1220
+ }
1221
+ return {
1222
+ total_nodes: graph.nodes.length,
1223
+ total_edges: graph.edges.length,
1224
+ by_type: typeCounts,
1225
+ ...Object.keys(moduleCounts).length > 0 && { by_module: moduleCounts },
1226
+ by_edge_type: edgeTypeCounts,
1227
+ warnings: graph.warnings.length
1228
+ };
1229
+ }
1230
+ function ok(text) {
1231
+ return { content: [{ type: "text", text }] };
1232
+ }
1233
+ function okJson(data) {
1234
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
1235
+ }
1236
+ function err(text) {
1237
+ return { content: [{ type: "text", text }], isError: true };
1238
+ }
1239
+ function handleGenerateGraph(args) {
1240
+ const rootDir = process.cwd();
1241
+ const layer = args.layer;
1242
+ if (layer && !["ui", "api", "db"].includes(layer)) {
1243
+ return err(`Invalid layer "${layer}". Must be one of: ui, api, db`);
1244
+ }
1245
+ const results = generateGraph(rootDir, layer);
1246
+ if (results.length === 0) {
1247
+ return err(
1248
+ layer ? `No parser detected for the "${layer}" layer in this project.` : "No parsers detected for this project. Check that the project has the expected structure."
1249
+ );
1250
+ }
1251
+ const summary = results.map((r) => {
1252
+ const warnings = r.output.warnings.length;
1253
+ return ` ${r.layer}: ${r.nodeCount} nodes, ${r.edgeCount} edges${warnings ? ` (${warnings} warnings)` : ""}`;
1254
+ }).join("\n");
1255
+ return ok(
1256
+ `Graph generated successfully.
1257
+
1258
+ Layers:
1259
+ ${summary}
1260
+
1261
+ Output: .launchsecure/graphs/
1262
+
1263
+ Use read_graph with filters (search/type/module/node_id) to query.`
1264
+ );
1265
+ }
1266
+ function runReadGraphQuery(rootDir, args) {
1267
+ const layer = args.layer;
1268
+ const search = args.search;
1269
+ const type = args.type;
1270
+ const module_ = args.module;
1271
+ const nodeId = args.node_id;
1272
+ const hops = args.hops ?? 1;
1273
+ const minimal = args.minimal ?? false;
1274
+ const hasFilter = !!(search || type || module_ || nodeId);
1275
+ if (layer && !["ui", "api", "db"].includes(layer)) {
1276
+ return { error: `Invalid layer "${layer}". Must be one of: ui, api, db` };
1277
+ }
1278
+ if (!layer) {
1279
+ const graphs = readAllGraphs(rootDir);
1280
+ const available = Object.keys(graphs);
1281
+ if (available.length === 0) {
1282
+ return { error: "No graphs found in .launchsecure/graphs/. Run generate_graph first." };
1283
+ }
1284
+ const summaries = {};
1285
+ for (const l of available) {
1286
+ summaries[l] = layerSummary(graphs[l]);
1287
+ }
1288
+ return {
1289
+ hint: "No layer specified \u2014 returning per-layer summary. Call again with layer + filter to get nodes.",
1290
+ available_layers: available,
1291
+ summaries
1292
+ };
1293
+ }
1294
+ const graph = readGraph(rootDir, layer);
1295
+ if (!graph) {
1296
+ return { error: `No ${layer} graph found at .launchsecure/graphs/${layer}.json. Run generate_graph first.` };
1297
+ }
1298
+ if (nodeId) {
1299
+ const nb = neighborhood(graph, nodeId, hops);
1300
+ if (!nb) {
1301
+ return { error: `Node "${nodeId}" not found in ${layer} graph. Try read_graph with search="${nodeId}" to find similar nodes.` };
1302
+ }
1303
+ return {
1304
+ layer,
1305
+ center: nodeId,
1306
+ hops,
1307
+ node_count: nb.nodes.length,
1308
+ edge_count: nb.edges.length,
1309
+ nodes: minimal ? toMinimal(nb.nodes) : nb.nodes,
1310
+ edges: nb.edges
1311
+ };
1312
+ }
1313
+ if (!hasFilter) {
1314
+ return {
1315
+ hint: "No filter specified \u2014 returning summary only. Use search/type/module/node_id to retrieve nodes.",
1316
+ layer,
1317
+ summary: layerSummary(graph)
1318
+ };
1319
+ }
1320
+ const matched = graph.nodes.filter((n) => {
1321
+ if (search && !matchesSearch(n, search)) return false;
1322
+ if (type && n.type !== type) return false;
1323
+ if (module_ && n.module !== module_) return false;
1324
+ return true;
1325
+ });
1326
+ const matchedIds = new Set(matched.map((n) => n.id));
1327
+ const matchedEdges = graph.edges.filter((e) => matchedIds.has(e.source) && matchedIds.has(e.target));
1328
+ if (matched.length === 0) {
1329
+ return {
1330
+ layer,
1331
+ filter: { search, type, module: module_ },
1332
+ matched: 0,
1333
+ hint: "No nodes matched. Check spelling, or call read_graph without filter to see the summary and available types/modules."
1334
+ };
1335
+ }
1336
+ return {
1337
+ layer,
1338
+ filter: { search, type, module: module_ },
1339
+ matched: matched.length,
1340
+ edge_count: matchedEdges.length,
1341
+ nodes: minimal ? toMinimal(matched) : matched,
1342
+ edges: matchedEdges
1343
+ };
1344
+ }
1345
+ function handleReadGraph(args) {
1346
+ const rootDir = process.cwd();
1347
+ if (Array.isArray(args.queries)) {
1348
+ const queries = args.queries;
1349
+ if (queries.length === 0) {
1350
+ return err("queries array is empty. Provide at least one query object.");
1351
+ }
1352
+ const results = queries.map((q, i) => ({ index: i, query: q, result: runReadGraphQuery(rootDir, q) }));
1353
+ return okJson({ batch: true, count: results.length, results });
1354
+ }
1355
+ const result = runReadGraphQuery(rootDir, args);
1356
+ return okJson(result);
1357
+ }
1358
+ function nodeToFilePath(rootDir, layer, nodeId) {
1359
+ if (layer === "ui") return (0, import_node_path6.join)(rootDir, "src", nodeId);
1360
+ if (layer === "api") return (0, import_node_path6.join)(rootDir, nodeId);
1361
+ if (layer === "db") return (0, import_node_path6.join)(rootDir, "prisma", "schema.prisma");
1362
+ return null;
1363
+ }
1364
+ function handleGrepNodes(args) {
1365
+ const rootDir = process.cwd();
1366
+ const pattern = args.pattern;
1367
+ const layer = args.layer;
1368
+ if (!pattern) return err("pattern is required");
1369
+ if (!layer) return err("layer is required (ui, api, or db)");
1370
+ if (!["ui", "api", "db"].includes(layer)) return err(`Invalid layer "${layer}". Must be one of: ui, api, db`);
1371
+ const hasFilter = !!(args.search || args.type || args.module || args.node_id);
1372
+ if (!hasFilter) {
1373
+ return err(
1374
+ "At least one filter (search/type/module/node_id) must be set. Without a filter, grep_nodes would scan the entire layer \u2014 use Grep directly for that."
1375
+ );
1376
+ }
1377
+ let re;
1378
+ try {
1379
+ re = new RegExp(pattern, args.case_insensitive ? "i" : "");
1380
+ } catch (e) {
1381
+ return err(`Invalid regex: ${e.message}`);
1382
+ }
1383
+ const queryResult = runReadGraphQuery(rootDir, {
1384
+ layer,
1385
+ search: args.search,
1386
+ type: args.type,
1387
+ module: args.module,
1388
+ node_id: args.node_id,
1389
+ hops: args.hops,
1390
+ minimal: true
1391
+ });
1392
+ if (queryResult.error) return err(queryResult.error);
1393
+ const nodes = queryResult.nodes ?? [];
1394
+ if (nodes.length === 0) {
1395
+ return okJson({
1396
+ pattern,
1397
+ filter: { layer, search: args.search, type: args.type, module: args.module, node_id: args.node_id },
1398
+ files_searched: 0,
1399
+ total_matches: 0,
1400
+ matches: []
1401
+ });
1402
+ }
1403
+ const maxFiles = args.max_files ?? 50;
1404
+ if (nodes.length > maxFiles) {
1405
+ return err(
1406
+ `Filter matched ${nodes.length} nodes (max_files: ${maxFiles}). Narrow the filter (add type/module) or pass max_files to override.`
1407
+ );
1408
+ }
1409
+ const filePaths = /* @__PURE__ */ new Map();
1410
+ for (const node of nodes) {
1411
+ const fp = nodeToFilePath(rootDir, layer, node.id);
1412
+ if (fp && !filePaths.has(fp)) filePaths.set(fp, node.id);
1413
+ }
1414
+ const contextLines = args.context ?? 2;
1415
+ const maxMatches = args.max_matches ?? 50;
1416
+ const matches = [];
1417
+ let totalMatches = 0;
1418
+ let filesSearched = 0;
1419
+ let truncated = false;
1420
+ for (const [filePath, nodeId] of filePaths) {
1421
+ if (!(0, import_node_fs6.existsSync)(filePath)) continue;
1422
+ filesSearched++;
1423
+ let content;
1424
+ try {
1425
+ content = (0, import_node_fs6.readFileSync)(filePath, "utf-8");
1426
+ } catch {
1427
+ continue;
1428
+ }
1429
+ const lines = content.split("\n");
1430
+ for (let i = 0; i < lines.length; i++) {
1431
+ if (re.test(lines[i])) {
1432
+ totalMatches++;
1433
+ const match = {
1434
+ file: nodeId,
1435
+ line: i + 1,
1436
+ text: lines[i]
1437
+ };
1438
+ if (contextLines > 0) {
1439
+ match.context = {
1440
+ before: lines.slice(Math.max(0, i - contextLines), i),
1441
+ after: lines.slice(i + 1, i + 1 + contextLines)
1442
+ };
1443
+ }
1444
+ matches.push(match);
1445
+ if (matches.length >= maxMatches) {
1446
+ truncated = true;
1447
+ break;
1448
+ }
1449
+ }
1450
+ }
1451
+ if (truncated) break;
1452
+ }
1453
+ return okJson({
1454
+ pattern,
1455
+ filter: { layer, search: args.search, type: args.type, module: args.module, node_id: args.node_id, hops: args.hops },
1456
+ files_searched: filesSearched,
1457
+ total_matches: totalMatches,
1458
+ matches,
1459
+ truncated
1460
+ });
1461
+ }
1462
+ function send(msg) {
1463
+ process.stdout.write(JSON.stringify(msg) + "\n");
1464
+ }
1465
+ function respond(id, result) {
1466
+ send({ jsonrpc: "2.0", id, result });
1467
+ }
1468
+ function respondError(id, code, message) {
1469
+ send({ jsonrpc: "2.0", id, error: { code, message } });
1470
+ }
1471
+ function handleMessage(msg) {
1472
+ const method = msg.method;
1473
+ const id = msg.id;
1474
+ if (method === "initialize") {
1475
+ respond(id ?? null, {
1476
+ protocolVersion: "2025-03-26",
1477
+ capabilities: { tools: {} },
1478
+ serverInfo: SERVER_INFO
1479
+ });
1480
+ return;
1481
+ }
1482
+ if (method === "notifications/initialized") {
1483
+ return;
1484
+ }
1485
+ if (method === "tools/list") {
1486
+ respond(id ?? null, { tools: TOOLS });
1487
+ return;
1488
+ }
1489
+ if (method === "tools/call") {
1490
+ const params = msg.params;
1491
+ const toolName = params.name;
1492
+ const args = params.arguments ?? {};
1493
+ if (toolName === "generate_graph") {
1494
+ respond(id ?? null, handleGenerateGraph(args));
1495
+ return;
1496
+ }
1497
+ if (toolName === "read_graph") {
1498
+ respond(id ?? null, handleReadGraph(args));
1499
+ return;
1500
+ }
1501
+ if (toolName === "grep_nodes") {
1502
+ respond(id ?? null, handleGrepNodes(args));
1503
+ return;
1504
+ }
1505
+ respondError(id ?? null, -32601, `Unknown tool: ${toolName}`);
1506
+ return;
1507
+ }
1508
+ if (method === "ping") {
1509
+ respond(id ?? null, {});
1510
+ return;
1511
+ }
1512
+ if (id !== void 0 && id !== null) {
1513
+ respondError(id, -32601, `Method not found: ${method}`);
1514
+ }
1515
+ }
1516
+ function startGraphMcpServer() {
1517
+ process.stdin.setEncoding("utf-8");
1518
+ let buffer = "";
1519
+ process.stdin.on("data", (chunk) => {
1520
+ buffer += chunk;
1521
+ const lines = buffer.split("\n");
1522
+ buffer = lines.pop() || "";
1523
+ for (const line of lines) {
1524
+ const trimmed = line.trim();
1525
+ if (!trimmed) continue;
1526
+ try {
1527
+ handleMessage(JSON.parse(trimmed));
1528
+ } catch (err2) {
1529
+ process.stderr.write(`[launchsecure-graph] Parse error: ${err2}
1530
+ `);
1531
+ }
1532
+ }
1533
+ });
1534
+ process.stdin.on("end", () => {
1535
+ process.exit(0);
1536
+ });
1537
+ process.stderr.write(`[launchsecure-graph] MCP server started (cwd: ${process.cwd()})
1538
+ `);
1539
+ }
1540
+
1541
+ // src/server/graph-mcp-entry.ts
1542
+ startGraphMcpServer();