@mandujs/mcp 0.12.2 → 0.13.0

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 (141) hide show
  1. package/README.md +367 -367
  2. package/package.json +2 -2
  3. package/src/activity-monitor.ts +847 -847
  4. package/src/adapters/index.ts +20 -20
  5. package/src/adapters/monitor-adapter.ts +100 -100
  6. package/src/adapters/tool-adapter.ts +88 -88
  7. package/src/executor/error-handler.ts +250 -250
  8. package/src/executor/index.ts +22 -22
  9. package/src/executor/tool-executor.ts +148 -148
  10. package/src/hooks/config-watcher.ts +174 -174
  11. package/src/hooks/index.ts +23 -23
  12. package/src/hooks/mcp-hooks.ts +227 -227
  13. package/src/index.ts +106 -106
  14. package/src/logging/index.ts +15 -15
  15. package/src/logging/mcp-transport.ts +134 -134
  16. package/src/registry/index.ts +13 -13
  17. package/src/registry/mcp-tool-registry.ts +298 -298
  18. package/src/resources/skills/guides.ts +1136 -1136
  19. package/src/resources/skills/index.ts +12 -12
  20. package/src/resources/skills/loader.ts +218 -218
  21. package/src/resources/skills/mandu-composition/SKILL.md +91 -91
  22. package/src/resources/skills/mandu-composition/metadata.json +13 -13
  23. package/src/resources/skills/mandu-composition/rules/_sections.md +26 -26
  24. package/src/resources/skills/mandu-composition/rules/_template.md +77 -77
  25. package/src/resources/skills/mandu-composition/rules/comp-arch-avoid-boolean-props.md +146 -146
  26. package/src/resources/skills/mandu-composition/rules/comp-arch-compound-components.md +164 -164
  27. package/src/resources/skills/mandu-composition/rules/comp-island-event.md +161 -161
  28. package/src/resources/skills/mandu-composition/rules/comp-island-slot-split.md +167 -167
  29. package/src/resources/skills/mandu-composition/rules/comp-pattern-children.md +149 -149
  30. package/src/resources/skills/mandu-composition/rules/comp-state-context-interface.md +148 -148
  31. package/src/resources/skills/mandu-composition/rules/comp-state-lift-state.md +150 -150
  32. package/src/resources/skills/mandu-deployment/SKILL.md +92 -92
  33. package/src/resources/skills/mandu-deployment/_sections.md +41 -41
  34. package/src/resources/skills/mandu-deployment/_template.md +38 -38
  35. package/src/resources/skills/mandu-deployment/metadata.json +13 -13
  36. package/src/resources/skills/mandu-deployment/rules/deploy-build-bun.md +109 -109
  37. package/src/resources/skills/mandu-deployment/rules/deploy-build-output.md +115 -115
  38. package/src/resources/skills/mandu-deployment/rules/deploy-cicd-github.md +219 -219
  39. package/src/resources/skills/mandu-deployment/rules/deploy-docker-bun.md +150 -150
  40. package/src/resources/skills/mandu-deployment/rules/deploy-docker-compose.md +223 -223
  41. package/src/resources/skills/mandu-deployment/rules/deploy-platform-fly.md +152 -152
  42. package/src/resources/skills/mandu-deployment/rules/deploy-platform-render.md +179 -179
  43. package/src/resources/skills/mandu-deployment/rules/deploy-platform-supabase.md +323 -323
  44. package/src/resources/skills/mandu-deployment/rules/deploy-platform-vercel.md +140 -140
  45. package/src/resources/skills/mandu-fs-routes/SKILL.md +82 -82
  46. package/src/resources/skills/mandu-fs-routes/metadata.json +12 -12
  47. package/src/resources/skills/mandu-fs-routes/rules/_sections.md +36 -36
  48. package/src/resources/skills/mandu-fs-routes/rules/_template.md +69 -69
  49. package/src/resources/skills/mandu-fs-routes/rules/routes-api-methods.md +65 -65
  50. package/src/resources/skills/mandu-fs-routes/rules/routes-dynamic-param.md +93 -93
  51. package/src/resources/skills/mandu-fs-routes/rules/routes-naming-page.md +55 -55
  52. package/src/resources/skills/mandu-guard/SKILL.md +129 -129
  53. package/src/resources/skills/mandu-guard/metadata.json +12 -12
  54. package/src/resources/skills/mandu-guard/rules/_sections.md +36 -36
  55. package/src/resources/skills/mandu-guard/rules/_template.md +82 -82
  56. package/src/resources/skills/mandu-guard/rules/guard-config-rules.md +100 -100
  57. package/src/resources/skills/mandu-guard/rules/guard-layer-direction.md +76 -76
  58. package/src/resources/skills/mandu-guard/rules/guard-preset-mandu.md +81 -81
  59. package/src/resources/skills/mandu-guard/rules/guard-validate-import.md +80 -80
  60. package/src/resources/skills/mandu-hydration/SKILL.md +91 -91
  61. package/src/resources/skills/mandu-hydration/metadata.json +12 -12
  62. package/src/resources/skills/mandu-hydration/rules/_sections.md +31 -31
  63. package/src/resources/skills/mandu-hydration/rules/_template.md +72 -72
  64. package/src/resources/skills/mandu-hydration/rules/hydration-data-event.md +109 -109
  65. package/src/resources/skills/mandu-hydration/rules/hydration-directive-use-client.md +55 -55
  66. package/src/resources/skills/mandu-hydration/rules/hydration-island-setup.md +113 -113
  67. package/src/resources/skills/mandu-hydration/rules/hydration-priority-visible.md +68 -68
  68. package/src/resources/skills/mandu-performance/SKILL.md +85 -85
  69. package/src/resources/skills/mandu-performance/metadata.json +14 -14
  70. package/src/resources/skills/mandu-performance/rules/_sections.md +31 -31
  71. package/src/resources/skills/mandu-performance/rules/_template.md +64 -64
  72. package/src/resources/skills/mandu-performance/rules/perf-async-defer-await.md +103 -103
  73. package/src/resources/skills/mandu-performance/rules/perf-async-parallel.md +95 -95
  74. package/src/resources/skills/mandu-performance/rules/perf-bun-file.md +124 -124
  75. package/src/resources/skills/mandu-performance/rules/perf-bun-serve.md +125 -125
  76. package/src/resources/skills/mandu-performance/rules/perf-bundle-imports.md +80 -80
  77. package/src/resources/skills/mandu-performance/rules/perf-bundle-island-lazy.md +145 -145
  78. package/src/resources/skills/mandu-performance/rules/perf-cache-react.md +98 -98
  79. package/src/resources/skills/mandu-performance/rules/perf-render-transitions.md +154 -154
  80. package/src/resources/skills/mandu-security/SKILL.md +87 -87
  81. package/src/resources/skills/mandu-security/metadata.json +13 -13
  82. package/src/resources/skills/mandu-security/rules/_sections.md +31 -31
  83. package/src/resources/skills/mandu-security/rules/_template.md +74 -74
  84. package/src/resources/skills/mandu-security/rules/sec-auth-guard.md +127 -127
  85. package/src/resources/skills/mandu-security/rules/sec-env-management.md +133 -133
  86. package/src/resources/skills/mandu-security/rules/sec-input-validate.md +148 -148
  87. package/src/resources/skills/mandu-security/rules/sec-protect-csrf.md +146 -146
  88. package/src/resources/skills/mandu-security/rules/sec-protect-headers.md +138 -138
  89. package/src/resources/skills/mandu-slot/SKILL.md +85 -85
  90. package/src/resources/skills/mandu-slot/metadata.json +12 -12
  91. package/src/resources/skills/mandu-slot/rules/_sections.md +36 -36
  92. package/src/resources/skills/mandu-slot/rules/_template.md +63 -63
  93. package/src/resources/skills/mandu-slot/rules/slot-basic-structure.md +38 -38
  94. package/src/resources/skills/mandu-slot/rules/slot-ctx-response.md +56 -56
  95. package/src/resources/skills/mandu-slot/rules/slot-guard-auth.md +59 -59
  96. package/src/resources/skills/mandu-slot/rules/slot-http-methods.md +64 -64
  97. package/src/resources/skills/mandu-styling/SKILL.md +154 -154
  98. package/src/resources/skills/mandu-styling/_sections.md +43 -43
  99. package/src/resources/skills/mandu-styling/_template.md +32 -32
  100. package/src/resources/skills/mandu-styling/metadata.json +15 -15
  101. package/src/resources/skills/mandu-styling/rules/style-component-compound.md +235 -235
  102. package/src/resources/skills/mandu-styling/rules/style-component-slots.md +255 -255
  103. package/src/resources/skills/mandu-styling/rules/style-component-tokens.md +205 -205
  104. package/src/resources/skills/mandu-styling/rules/style-island-animations.md +272 -272
  105. package/src/resources/skills/mandu-styling/rules/style-island-scoping.md +167 -167
  106. package/src/resources/skills/mandu-styling/rules/style-island-variants.md +221 -221
  107. package/src/resources/skills/mandu-styling/rules/style-perf-critical.md +209 -209
  108. package/src/resources/skills/mandu-styling/rules/style-perf-purge.md +192 -192
  109. package/src/resources/skills/mandu-styling/rules/style-setup-modules.md +162 -162
  110. package/src/resources/skills/mandu-styling/rules/style-setup-panda.md +164 -164
  111. package/src/resources/skills/mandu-styling/rules/style-setup-tailwind.md +170 -170
  112. package/src/resources/skills/mandu-styling/rules/style-tailwind-v4-gotchas.md +179 -179
  113. package/src/resources/skills/mandu-styling/rules/style-theme-darkmode.md +229 -229
  114. package/src/resources/skills/mandu-testing/SKILL.md +99 -99
  115. package/src/resources/skills/mandu-testing/metadata.json +13 -13
  116. package/src/resources/skills/mandu-testing/rules/_sections.md +26 -26
  117. package/src/resources/skills/mandu-testing/rules/_template.md +65 -65
  118. package/src/resources/skills/mandu-testing/rules/test-component-island.md +195 -195
  119. package/src/resources/skills/mandu-testing/rules/test-e2e-playwright.md +196 -196
  120. package/src/resources/skills/mandu-testing/rules/test-mock-fetch.md +219 -219
  121. package/src/resources/skills/mandu-testing/rules/test-slot-unit.md +192 -192
  122. package/src/resources/skills/mandu-ui/SKILL.md +117 -117
  123. package/src/resources/skills/mandu-ui/_sections.md +23 -23
  124. package/src/resources/skills/mandu-ui/_template.md +32 -32
  125. package/src/resources/skills/mandu-ui/metadata.json +13 -13
  126. package/src/resources/skills/mandu-ui/rules/ui-accessibility-aria.md +232 -232
  127. package/src/resources/skills/mandu-ui/rules/ui-accessibility-focus.md +238 -238
  128. package/src/resources/skills/mandu-ui/rules/ui-composition-patterns.md +259 -259
  129. package/src/resources/skills/mandu-ui/rules/ui-island-integration.md +258 -258
  130. package/src/resources/skills/mandu-ui/rules/ui-radix-patterns.md +213 -213
  131. package/src/resources/skills/mandu-ui/rules/ui-shadcn-setup.md +209 -209
  132. package/src/resources/skills/recipes.ts +932 -932
  133. package/src/tools/generate.ts +7 -4
  134. package/src/tools/guard.ts +17 -4
  135. package/src/tools/hydration.ts +10 -10
  136. package/src/tools/project.ts +334 -334
  137. package/src/tools/runtime.ts +497 -497
  138. package/src/tools/seo.ts +417 -417
  139. package/src/tools/spec.ts +80 -159
  140. package/src/utils/project.ts +22 -12
  141. package/src/utils/withWarnings.ts +83 -83
package/src/tools/spec.ts CHANGED
@@ -3,16 +3,19 @@ import {
3
3
  loadManifest,
4
4
  validateManifest,
5
5
  writeLock,
6
+ generateManifest,
7
+ GENERATED_RELATIVE_PATHS,
6
8
  type RouteSpec,
7
9
  type RoutesManifest,
8
10
  } from "@mandujs/core";
9
11
  import { getProjectPaths, readJsonFile, writeJsonFile } from "../utils/project.js";
10
12
  import path from "path";
13
+ import fs from "fs/promises";
11
14
 
12
15
  export const specToolDefinitions: Tool[] = [
13
16
  {
14
17
  name: "mandu_list_routes",
15
- description: "List all routes in the current Mandu project",
18
+ description: "List all routes in the current Mandu project (reads from .mandu/routes.manifest.json)",
16
19
  inputSchema: {
17
20
  type: "object",
18
21
  properties: {},
@@ -35,62 +38,34 @@ export const specToolDefinitions: Tool[] = [
35
38
  },
36
39
  {
37
40
  name: "mandu_add_route",
38
- description: "Add a new route to the manifest",
41
+ description: "Add a new route by scaffolding files in app/ and optionally in spec/slots/ and spec/contracts/",
39
42
  inputSchema: {
40
43
  type: "object",
41
44
  properties: {
42
- id: {
45
+ path: {
43
46
  type: "string",
44
- description: "Unique route identifier",
45
- },
46
- pattern: {
47
- type: "string",
48
- description: "URL pattern (e.g., /api/users/:id)",
47
+ description: "Route path relative to app/ (e.g., 'api/users' or 'blog/[slug]')",
49
48
  },
50
49
  kind: {
51
50
  type: "string",
52
51
  enum: ["api", "page"],
53
- description: "Route type: api or page",
54
- },
55
- slotModule: {
56
- type: "string",
57
- description: "Path to slot file (optional)",
52
+ description: "Route type: api (route.ts) or page (page.tsx)",
58
53
  },
59
- componentModule: {
60
- type: "string",
61
- description: "Path to component module (required for page kind)",
62
- },
63
- },
64
- required: ["id", "pattern", "kind"],
65
- },
66
- },
67
- {
68
- name: "mandu_update_route",
69
- description: "Update an existing route",
70
- inputSchema: {
71
- type: "object",
72
- properties: {
73
- routeId: {
74
- type: "string",
75
- description: "The route ID to update",
54
+ withSlot: {
55
+ type: "boolean",
56
+ description: "Scaffold a slot file in spec/slots/ (default: true)",
76
57
  },
77
- updates: {
78
- type: "object",
79
- description: "Partial route updates",
80
- properties: {
81
- pattern: { type: "string" },
82
- kind: { type: "string", enum: ["api", "page"] },
83
- slotModule: { type: "string" },
84
- componentModule: { type: "string" },
85
- },
58
+ withContract: {
59
+ type: "boolean",
60
+ description: "Scaffold a contract file in spec/contracts/",
86
61
  },
87
62
  },
88
- required: ["routeId", "updates"],
63
+ required: ["path", "kind"],
89
64
  },
90
65
  },
91
66
  {
92
67
  name: "mandu_delete_route",
93
- description: "Delete a route from the manifest",
68
+ description: "Delete a route's app/ files and rescan (preserves slot/contract files)",
94
69
  inputSchema: {
95
70
  type: "object",
96
71
  properties: {
@@ -103,8 +78,8 @@ export const specToolDefinitions: Tool[] = [
103
78
  },
104
79
  },
105
80
  {
106
- name: "mandu_validate_spec",
107
- description: "Validate the current spec manifest",
81
+ name: "mandu_validate_manifest",
82
+ description: "Validate the current routes manifest (.mandu/routes.manifest.json)",
108
83
  inputSchema: {
109
84
  type: "object",
110
85
  properties: {},
@@ -130,6 +105,7 @@ export function specTools(projectRoot: string) {
130
105
  pattern: r.pattern,
131
106
  kind: r.kind,
132
107
  slotModule: r.slotModule,
108
+ contractModule: r.contractModule,
133
109
  componentModule: r.componentModule,
134
110
  })),
135
111
  count: result.data.routes.length,
@@ -153,157 +129,102 @@ export function specTools(projectRoot: string) {
153
129
  },
154
130
 
155
131
  mandu_add_route: async (args: Record<string, unknown>) => {
156
- const { id, pattern, kind, slotModule, componentModule } = args as {
157
- id: string;
158
- pattern: string;
132
+ const { path: routePath, kind, withSlot = true, withContract = false } = args as {
133
+ path: string;
159
134
  kind: "api" | "page";
160
- slotModule?: string;
161
- componentModule?: string;
135
+ withSlot?: boolean;
136
+ withContract?: boolean;
162
137
  };
163
138
 
164
- // Load current manifest
165
- const result = await loadManifest(paths.manifestPath);
166
- if (!result.success || !result.data) {
167
- return { error: result.errors };
168
- }
139
+ const createdFiles: string[] = [];
169
140
 
170
- // Check for duplicate
171
- if (result.data.routes.some((r) => r.id === id)) {
172
- return { error: `Route with id '${id}' already exists` };
173
- }
174
-
175
- if (result.data.routes.some((r) => r.pattern === pattern)) {
176
- return { error: `Route with pattern '${pattern}' already exists` };
177
- }
141
+ // Scaffold app/ file
142
+ const fileName = kind === "api" ? "route.ts" : "page.tsx";
143
+ const appFilePath = path.join(paths.appDir, routePath, fileName);
144
+ const appFileDir = path.dirname(appFilePath);
178
145
 
179
- // Build new route
180
- const newRoute: RouteSpec = {
181
- id,
182
- pattern,
183
- kind,
184
- module: `apps/server/generated/routes/${id}.route.ts`,
185
- slotModule: slotModule || `spec/slots/${id}.slot.ts`,
186
- };
146
+ await fs.mkdir(appFileDir, { recursive: true });
187
147
 
188
- if (kind === "page") {
189
- newRoute.componentModule = componentModule || `apps/web/generated/routes/${id}.route.tsx`;
148
+ if (kind === "api") {
149
+ await Bun.write(appFilePath, `export function GET(req: Request) {\n return Response.json({ message: "Hello" });\n}\n`);
150
+ } else {
151
+ await Bun.write(appFilePath, `export default function Page() {\n return <div>Page</div>;\n}\n`);
190
152
  }
191
-
192
- // Validate new route
193
- const newManifest: RoutesManifest = {
194
- version: result.data.version,
195
- routes: [...result.data.routes, newRoute],
196
- };
197
-
198
- const validation = validateManifest(newManifest);
199
- if (!validation.success) {
200
- return { error: validation.errors };
153
+ createdFiles.push(`app/${routePath}/${fileName}`);
154
+
155
+ // Derive route ID from path
156
+ const routeId = routePath.replace(/\//g, "-").replace(/[\[\]\.]/g, "");
157
+
158
+ // Scaffold slot if requested
159
+ if (withSlot) {
160
+ const slotPath = path.join(paths.slotsDir, `${routeId}.slot.ts`);
161
+ await fs.mkdir(paths.slotsDir, { recursive: true });
162
+ if (!(await Bun.file(slotPath).exists())) {
163
+ await Bun.write(slotPath, `export default function slot(req: Request) {\n return {};\n}\n`);
164
+ createdFiles.push(`spec/slots/${routeId}.slot.ts`);
165
+ }
201
166
  }
202
167
 
203
- // Write updated manifest
204
- await writeJsonFile(paths.manifestPath, newManifest);
205
-
206
- // Update lock file
207
- await writeLock(paths.lockPath, newManifest);
208
-
209
- return {
210
- success: true,
211
- route: newRoute,
212
- message: `Route '${id}' added successfully`,
213
- };
214
- },
215
-
216
- mandu_update_route: async (args: Record<string, unknown>) => {
217
- const { routeId, updates } = args as {
218
- routeId: string;
219
- updates: Partial<RouteSpec>;
220
- };
221
-
222
- // Load current manifest
223
- const result = await loadManifest(paths.manifestPath);
224
- if (!result.success || !result.data) {
225
- return { error: result.errors };
168
+ // Scaffold contract if requested
169
+ if (withContract) {
170
+ const contractPath = path.join(paths.contractsDir, `${routeId}.contract.ts`);
171
+ await fs.mkdir(paths.contractsDir, { recursive: true });
172
+ if (!(await Bun.file(contractPath).exists())) {
173
+ await Bun.write(contractPath, `import { z } from "zod";\n\nexport const contract = {\n request: z.object({}),\n response: z.object({}),\n};\n`);
174
+ createdFiles.push(`spec/contracts/${routeId}.contract.ts`);
175
+ }
226
176
  }
227
177
 
228
- // Find route
229
- const routeIndex = result.data.routes.findIndex((r) => r.id === routeId);
230
- if (routeIndex === -1) {
231
- return { error: `Route not found: ${routeId}` };
232
- }
233
-
234
- // Apply updates
235
- const updatedRoute = {
236
- ...result.data.routes[routeIndex],
237
- ...updates,
238
- id: routeId, // ID cannot be changed
239
- };
240
-
241
- const newRoutes = [...result.data.routes];
242
- newRoutes[routeIndex] = updatedRoute as RouteSpec;
243
-
244
- // Validate
245
- const newManifest: RoutesManifest = {
246
- version: result.data.version,
247
- routes: newRoutes,
248
- };
249
-
250
- const validation = validateManifest(newManifest);
251
- if (!validation.success) {
252
- return { error: validation.errors };
253
- }
254
-
255
- // Write updated manifest
256
- await writeJsonFile(paths.manifestPath, newManifest);
257
-
258
- // Update lock file
259
- await writeLock(paths.lockPath, newManifest);
178
+ // Rescan to regenerate manifest with auto-linking
179
+ const genResult = await generateManifest(projectRoot);
260
180
 
261
181
  return {
262
182
  success: true,
263
- route: updatedRoute,
264
- message: `Route '${routeId}' updated successfully`,
183
+ routeId,
184
+ createdFiles,
185
+ totalRoutes: genResult.manifest.routes.length,
186
+ message: `Route '${routeId}' scaffolded successfully`,
265
187
  };
266
188
  },
267
189
 
268
190
  mandu_delete_route: async (args: Record<string, unknown>) => {
269
191
  const { routeId } = args as { routeId: string };
270
192
 
271
- // Load current manifest
193
+ // Load current manifest to find the route
272
194
  const result = await loadManifest(paths.manifestPath);
273
195
  if (!result.success || !result.data) {
274
196
  return { error: result.errors };
275
197
  }
276
198
 
277
- // Find route
278
- const routeIndex = result.data.routes.findIndex((r) => r.id === routeId);
279
- if (routeIndex === -1) {
199
+ const route = result.data.routes.find((r) => r.id === routeId);
200
+ if (!route) {
280
201
  return { error: `Route not found: ${routeId}` };
281
202
  }
282
203
 
283
- const deletedRoute = result.data.routes[routeIndex];
284
-
285
- // Remove route
286
- const newRoutes = result.data.routes.filter((r) => r.id !== routeId);
287
-
288
- const newManifest: RoutesManifest = {
289
- version: result.data.version,
290
- routes: newRoutes,
291
- };
292
-
293
- // Write updated manifest
294
- await writeJsonFile(paths.manifestPath, newManifest);
204
+ // Delete app/ source file (module path points to generated; need to find source)
205
+ const deletedFiles: string[] = [];
206
+ if (route.module && route.module.startsWith("app/")) {
207
+ const fullPath = path.join(projectRoot, route.module);
208
+ try {
209
+ await fs.unlink(fullPath);
210
+ deletedFiles.push(route.module);
211
+ } catch {}
212
+ }
295
213
 
296
- // Update lock file
297
- await writeLock(paths.lockPath, newManifest);
214
+ // Rescan manifest (slot/contract files preserved)
215
+ const genResult = await generateManifest(projectRoot);
298
216
 
299
217
  return {
300
218
  success: true,
301
- deletedRoute,
302
- message: `Route '${routeId}' deleted successfully`,
219
+ deletedRoute: route,
220
+ deletedFiles,
221
+ preservedFiles: [route.slotModule, route.contractModule].filter(Boolean),
222
+ totalRoutes: genResult.manifest.routes.length,
223
+ message: `Route '${routeId}' deleted from app/. Slot/contract files preserved.`,
303
224
  };
304
225
  },
305
226
 
306
- mandu_validate_spec: async () => {
227
+ mandu_validate_manifest: async () => {
307
228
  const result = await loadManifest(paths.manifestPath);
308
229
  if (!result.success) {
309
230
  return {
@@ -3,19 +3,27 @@ import fs from "fs/promises";
3
3
  import { pathToFileURL } from "url";
4
4
 
5
5
  /**
6
- * Find the Mandu project root by looking for routes.manifest.json
6
+ * Find the Mandu project root by looking for app/ directory or mandu.config.*
7
7
  */
8
8
  export async function findProjectRoot(startDir: string = process.cwd()): Promise<string | null> {
9
9
  let currentDir = path.resolve(startDir);
10
10
 
11
11
  while (currentDir !== path.dirname(currentDir)) {
12
- const manifestPath = path.join(currentDir, "spec", "routes.manifest.json");
12
+ // Check for app/ directory (FS Routes source)
13
13
  try {
14
- await fs.access(manifestPath);
15
- return currentDir;
16
- } catch {
17
- currentDir = path.dirname(currentDir);
14
+ const appStat = await fs.stat(path.join(currentDir, "app"));
15
+ if (appStat.isDirectory()) return currentDir;
16
+ } catch {}
17
+
18
+ // Check for mandu.config.* files
19
+ for (const configFile of ["mandu.config.ts", "mandu.config.js", "mandu.config.json"]) {
20
+ try {
21
+ await fs.access(path.join(currentDir, configFile));
22
+ return currentDir;
23
+ } catch {}
18
24
  }
25
+
26
+ currentDir = path.dirname(currentDir);
19
27
  }
20
28
 
21
29
  return null;
@@ -27,14 +35,16 @@ export async function findProjectRoot(startDir: string = process.cwd()): Promise
27
35
  export function getProjectPaths(rootDir: string) {
28
36
  return {
29
37
  root: rootDir,
38
+ appDir: path.join(rootDir, "app"),
30
39
  specDir: path.join(rootDir, "spec"),
31
- manifestPath: path.join(rootDir, "spec", "routes.manifest.json"),
32
- lockPath: path.join(rootDir, "spec", "spec.lock.json"),
40
+ manifestPath: path.join(rootDir, ".mandu", "routes.manifest.json"),
41
+ lockPath: path.join(rootDir, ".mandu", "spec.lock.json"),
33
42
  slotsDir: path.join(rootDir, "spec", "slots"),
34
- historyDir: path.join(rootDir, "spec", "history"),
35
- generatedMapPath: path.join(rootDir, "packages", "core", "map", "generated.map.json"),
36
- serverRoutesDir: path.join(rootDir, "apps", "server", "generated", "routes"),
37
- webRoutesDir: path.join(rootDir, "apps", "web", "generated", "routes"),
43
+ contractsDir: path.join(rootDir, "spec", "contracts"),
44
+ historyDir: path.join(rootDir, ".mandu", "history"),
45
+ generatedMapPath: path.join(rootDir, ".mandu", "generated", "generated.map.json"),
46
+ serverRoutesDir: path.join(rootDir, ".mandu", "generated", "server", "routes"),
47
+ webRoutesDir: path.join(rootDir, ".mandu", "generated", "web", "routes"),
38
48
  };
39
49
  }
40
50
 
@@ -1,83 +1,83 @@
1
- /**
2
- * Watcher Warning Injection for Mutation Tools
3
- *
4
- * Mutation 도구(write_slot, add_route, generate 등) 실행 후
5
- * watcher 경고를 자동으로 응답에 포함시킨다.
6
- *
7
- * MCP notification이 Claude Code에 전달되지 않는 문제를 해결.
8
- */
9
-
10
- import { getWatcher } from "../../../core/src/index.js";
11
-
12
- const MUTATION_TOOLS = new Set([
13
- "mandu_write_slot",
14
- "mandu_add_route",
15
- "mandu_update_route",
16
- "mandu_delete_route",
17
- "mandu_generate",
18
- "mandu_build",
19
- "mandu_commit",
20
- "mandu_add_client_slot",
21
- "mandu_set_hydration",
22
- "mandu_create_contract",
23
- "mandu_update_route_contract",
24
- "mandu_sync_contract_slot",
25
- ]);
26
-
27
- /** watcher debounce(300ms) + 여유분 */
28
- const WARNING_WAIT_MS = 400;
29
-
30
- type ToolHandler = (args: Record<string, unknown>) => Promise<unknown>;
31
-
32
- /**
33
- * Mutation 도구 핸들러를 감싸서, 실행 후 발생한 watcher 경고를
34
- * 응답 객체의 `_warnings` 필드에 자동 포함시킨다.
35
- */
36
- export function applyWarningInjection(
37
- handlers: Record<string, ToolHandler>
38
- ): Record<string, ToolHandler> {
39
- const wrapped: Record<string, ToolHandler> = {};
40
-
41
- for (const [name, handler] of Object.entries(handlers)) {
42
- if (MUTATION_TOOLS.has(name)) {
43
- wrapped[name] = wrapWithWarnings(handler);
44
- } else {
45
- wrapped[name] = handler;
46
- }
47
- }
48
-
49
- return wrapped;
50
- }
51
-
52
- function wrapWithWarnings(handler: ToolHandler): ToolHandler {
53
- return async (args: Record<string, unknown>) => {
54
- const watcher = getWatcher();
55
- const beforeCount = watcher?.getRecentWarnings(100).length ?? 0;
56
-
57
- const result = await handler(args);
58
-
59
- // watcher debounce 대기
60
- await new Promise((resolve) => setTimeout(resolve, WARNING_WAIT_MS));
61
-
62
- const allWarnings = watcher?.getRecentWarnings(100) ?? [];
63
- const newWarnings = allWarnings.slice(beforeCount);
64
-
65
- if (
66
- newWarnings.length > 0 &&
67
- typeof result === "object" &&
68
- result !== null
69
- ) {
70
- return {
71
- ...(result as Record<string, unknown>),
72
- _warnings: newWarnings.map((w) => ({
73
- ruleId: w.ruleId,
74
- file: w.file,
75
- message: w.message,
76
- level: w.level ?? "warn",
77
- })),
78
- };
79
- }
80
-
81
- return result;
82
- };
83
- }
1
+ /**
2
+ * Watcher Warning Injection for Mutation Tools
3
+ *
4
+ * Mutation 도구(write_slot, add_route, generate 등) 실행 후
5
+ * watcher 경고를 자동으로 응답에 포함시킨다.
6
+ *
7
+ * MCP notification이 Claude Code에 전달되지 않는 문제를 해결.
8
+ */
9
+
10
+ import { getWatcher } from "../../../core/src/index.js";
11
+
12
+ const MUTATION_TOOLS = new Set([
13
+ "mandu_write_slot",
14
+ "mandu_add_route",
15
+ "mandu_update_route",
16
+ "mandu_delete_route",
17
+ "mandu_generate",
18
+ "mandu_build",
19
+ "mandu_commit",
20
+ "mandu_add_client_slot",
21
+ "mandu_set_hydration",
22
+ "mandu_create_contract",
23
+ "mandu_update_route_contract",
24
+ "mandu_sync_contract_slot",
25
+ ]);
26
+
27
+ /** watcher debounce(300ms) + 여유분 */
28
+ const WARNING_WAIT_MS = 400;
29
+
30
+ type ToolHandler = (args: Record<string, unknown>) => Promise<unknown>;
31
+
32
+ /**
33
+ * Mutation 도구 핸들러를 감싸서, 실행 후 발생한 watcher 경고를
34
+ * 응답 객체의 `_warnings` 필드에 자동 포함시킨다.
35
+ */
36
+ export function applyWarningInjection(
37
+ handlers: Record<string, ToolHandler>
38
+ ): Record<string, ToolHandler> {
39
+ const wrapped: Record<string, ToolHandler> = {};
40
+
41
+ for (const [name, handler] of Object.entries(handlers)) {
42
+ if (MUTATION_TOOLS.has(name)) {
43
+ wrapped[name] = wrapWithWarnings(handler);
44
+ } else {
45
+ wrapped[name] = handler;
46
+ }
47
+ }
48
+
49
+ return wrapped;
50
+ }
51
+
52
+ function wrapWithWarnings(handler: ToolHandler): ToolHandler {
53
+ return async (args: Record<string, unknown>) => {
54
+ const watcher = getWatcher();
55
+ const beforeCount = watcher?.getRecentWarnings(100).length ?? 0;
56
+
57
+ const result = await handler(args);
58
+
59
+ // watcher debounce 대기
60
+ await new Promise((resolve) => setTimeout(resolve, WARNING_WAIT_MS));
61
+
62
+ const allWarnings = watcher?.getRecentWarnings(100) ?? [];
63
+ const newWarnings = allWarnings.slice(beforeCount);
64
+
65
+ if (
66
+ newWarnings.length > 0 &&
67
+ typeof result === "object" &&
68
+ result !== null
69
+ ) {
70
+ return {
71
+ ...(result as Record<string, unknown>),
72
+ _warnings: newWarnings.map((w) => ({
73
+ ruleId: w.ruleId,
74
+ file: w.file,
75
+ message: w.message,
76
+ level: w.level ?? "warn",
77
+ })),
78
+ };
79
+ }
80
+
81
+ return result;
82
+ };
83
+ }