@avocadostudio-ai/orchestrator-core 0.1.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 (196) hide show
  1. package/LICENSE +201 -0
  2. package/dist/agent/agent-context.d.ts +19 -0
  3. package/dist/agent/agent-context.js +67 -0
  4. package/dist/agent/agent-logger.d.ts +5 -0
  5. package/dist/agent/agent-logger.js +22 -0
  6. package/dist/agent/agent-loop-openai.d.ts +8 -0
  7. package/dist/agent/agent-loop-openai.js +172 -0
  8. package/dist/agent/agent-loop.d.ts +56 -0
  9. package/dist/agent/agent-loop.js +167 -0
  10. package/dist/agent/agent-provider.d.ts +28 -0
  11. package/dist/agent/agent-provider.js +63 -0
  12. package/dist/agent/agent-tools.d.ts +28 -0
  13. package/dist/agent/agent-tools.js +899 -0
  14. package/dist/agent/context/editing-guidelines.md +46 -0
  15. package/dist/agent/context/role.md +39 -0
  16. package/dist/agent/integration-prompt.d.ts +9 -0
  17. package/dist/agent/integration-prompt.js +154 -0
  18. package/dist/agent/sites-agent-context.d.ts +12 -0
  19. package/dist/agent/sites-agent-context.js +316 -0
  20. package/dist/agent/sites-agent-shared.d.ts +161 -0
  21. package/dist/agent/sites-agent-shared.js +1101 -0
  22. package/dist/agent/sites-agent-tools.d.ts +18 -0
  23. package/dist/agent/sites-agent-tools.js +1227 -0
  24. package/dist/chat/anthropic-cache.d.ts +20 -0
  25. package/dist/chat/anthropic-cache.js +54 -0
  26. package/dist/chat/anthropic-planner.d.ts +98 -0
  27. package/dist/chat/anthropic-planner.js +1012 -0
  28. package/dist/chat/changelog-coverage-validator.d.ts +37 -0
  29. package/dist/chat/changelog-coverage-validator.js +215 -0
  30. package/dist/chat/chat-pipeline-context.d.ts +211 -0
  31. package/dist/chat/chat-pipeline-context.js +249 -0
  32. package/dist/chat/chat-pipeline-deterministic.d.ts +61 -0
  33. package/dist/chat/chat-pipeline-deterministic.js +407 -0
  34. package/dist/chat/chat-pipeline-image.d.ts +86 -0
  35. package/dist/chat/chat-pipeline-image.js +897 -0
  36. package/dist/chat/chat-pipeline-shared.d.ts +69 -0
  37. package/dist/chat/chat-pipeline-shared.js +212 -0
  38. package/dist/chat/chat-pipeline-translation.d.ts +27 -0
  39. package/dist/chat/chat-pipeline-translation.js +417 -0
  40. package/dist/chat/chat-pipeline-ui.d.ts +14 -0
  41. package/dist/chat/chat-pipeline-ui.js +244 -0
  42. package/dist/chat/chat-pipeline.d.ts +99 -0
  43. package/dist/chat/chat-pipeline.js +3999 -0
  44. package/dist/chat/decomposer.d.ts +21 -0
  45. package/dist/chat/decomposer.js +65 -0
  46. package/dist/chat/gemini-planner.d.ts +70 -0
  47. package/dist/chat/gemini-planner.js +541 -0
  48. package/dist/chat/hallucination-validator.d.ts +36 -0
  49. package/dist/chat/hallucination-validator.js +110 -0
  50. package/dist/chat/locale-strings.d.ts +47 -0
  51. package/dist/chat/locale-strings.js +100 -0
  52. package/dist/chat/plan-json-schema.d.ts +133 -0
  53. package/dist/chat/plan-json-schema.js +112 -0
  54. package/dist/chat/planner-types.d.ts +120 -0
  55. package/dist/chat/planner-types.js +66 -0
  56. package/dist/chat/planner.d.ts +148 -0
  57. package/dist/chat/planner.js +1361 -0
  58. package/dist/chat/prompts.d.ts +67 -0
  59. package/dist/chat/prompts.js +356 -0
  60. package/dist/chat/provider-routing.d.ts +14 -0
  61. package/dist/chat/provider-routing.js +27 -0
  62. package/dist/chat/variation-pipeline.d.ts +135 -0
  63. package/dist/chat/variation-pipeline.js +837 -0
  64. package/dist/chat/vision-alt-generator.d.ts +35 -0
  65. package/dist/chat/vision-alt-generator.js +152 -0
  66. package/dist/cms/adapter.d.ts +62 -0
  67. package/dist/cms/adapter.js +1 -0
  68. package/dist/cms/bootstrap.d.ts +17 -0
  69. package/dist/cms/bootstrap.js +85 -0
  70. package/dist/cms/editor-api-adapter.d.ts +23 -0
  71. package/dist/cms/editor-api-adapter.js +71 -0
  72. package/dist/cms/index.d.ts +4 -0
  73. package/dist/cms/index.js +3 -0
  74. package/dist/cms/json-file-adapter.d.ts +11 -0
  75. package/dist/cms/json-file-adapter.js +62 -0
  76. package/dist/demo-mode.d.ts +59 -0
  77. package/dist/demo-mode.js +201 -0
  78. package/dist/errors.d.ts +67 -0
  79. package/dist/errors.js +129 -0
  80. package/dist/http/chat-stream-resumable.d.ts +108 -0
  81. package/dist/http/chat-stream-resumable.js +290 -0
  82. package/dist/http/chat-stream.d.ts +99 -0
  83. package/dist/http/chat-stream.js +92 -0
  84. package/dist/image/gdrive-client.d.ts +22 -0
  85. package/dist/image/gdrive-client.js +215 -0
  86. package/dist/image/image-helpers.d.ts +95 -0
  87. package/dist/image/image-helpers.js +488 -0
  88. package/dist/index.d.ts +1 -0
  89. package/dist/index.js +1 -0
  90. package/dist/jira/jira-approval.d.ts +22 -0
  91. package/dist/jira/jira-approval.js +51 -0
  92. package/dist/jira/jira-client.d.ts +44 -0
  93. package/dist/jira/jira-client.js +313 -0
  94. package/dist/jira/jira-poller.d.ts +46 -0
  95. package/dist/jira/jira-poller.js +184 -0
  96. package/dist/jira/jira-processor.d.ts +103 -0
  97. package/dist/jira/jira-processor.js +1085 -0
  98. package/dist/jira/jira-types.d.ts +117 -0
  99. package/dist/jira/jira-types.js +38 -0
  100. package/dist/logger.d.ts +12 -0
  101. package/dist/logger.js +28 -0
  102. package/dist/migration/mcp-server-stdio.d.ts +8 -0
  103. package/dist/migration/mcp-server-stdio.js +672 -0
  104. package/dist/migration/migration-prompt.d.ts +7 -0
  105. package/dist/migration/migration-prompt.js +197 -0
  106. package/dist/migration/migration-tools.d.ts +17 -0
  107. package/dist/migration/migration-tools.js +159 -0
  108. package/dist/migration/scrape-cache.d.ts +9 -0
  109. package/dist/migration/scrape-cache.js +19 -0
  110. package/dist/nlp/deterministic-planner-context.d.ts +141 -0
  111. package/dist/nlp/deterministic-planner-context.js +362 -0
  112. package/dist/nlp/deterministic-planner-pages.d.ts +26 -0
  113. package/dist/nlp/deterministic-planner-pages.js +170 -0
  114. package/dist/nlp/deterministic-planner-patches.d.ts +80 -0
  115. package/dist/nlp/deterministic-planner-patches.js +508 -0
  116. package/dist/nlp/deterministic-planner-refs.d.ts +33 -0
  117. package/dist/nlp/deterministic-planner-refs.js +164 -0
  118. package/dist/nlp/deterministic-planner-suggestions.d.ts +49 -0
  119. package/dist/nlp/deterministic-planner-suggestions.js +579 -0
  120. package/dist/nlp/deterministic-planner.d.ts +85 -0
  121. package/dist/nlp/deterministic-planner.js +1631 -0
  122. package/dist/nlp/intent-detection.d.ts +309 -0
  123. package/dist/nlp/intent-detection.js +730 -0
  124. package/dist/nlp/intent-helpers.d.ts +15 -0
  125. package/dist/nlp/intent-helpers.js +243 -0
  126. package/dist/nlp/intent-patterns.d.ts +40 -0
  127. package/dist/nlp/intent-patterns.js +223 -0
  128. package/dist/nlp/plan-normalizer.d.ts +41 -0
  129. package/dist/nlp/plan-normalizer.js +1537 -0
  130. package/dist/ops/destructive-action-gate.d.ts +44 -0
  131. package/dist/ops/destructive-action-gate.js +90 -0
  132. package/dist/ops/ops-engine.d.ts +151 -0
  133. package/dist/ops/ops-engine.js +1394 -0
  134. package/dist/publish/diff-engine.d.ts +18 -0
  135. package/dist/publish/diff-engine.js +305 -0
  136. package/dist/publish/publish-helpers.d.ts +87 -0
  137. package/dist/publish/publish-helpers.js +521 -0
  138. package/dist/publish/publish-target-registry.d.ts +7 -0
  139. package/dist/publish/publish-target-registry.js +61 -0
  140. package/dist/publish/publish-target.d.ts +81 -0
  141. package/dist/publish/publish-target.js +1 -0
  142. package/dist/publish/targets/deploy-hook.d.ts +13 -0
  143. package/dist/publish/targets/deploy-hook.js +123 -0
  144. package/dist/publish/targets/git.d.ts +13 -0
  145. package/dist/publish/targets/git.js +55 -0
  146. package/dist/publish/targets/site-contract.d.ts +19 -0
  147. package/dist/publish/targets/site-contract.js +124 -0
  148. package/dist/state/content-source.d.ts +17 -0
  149. package/dist/state/content-source.js +1 -0
  150. package/dist/state/in-memory-content-source.d.ts +27 -0
  151. package/dist/state/in-memory-content-source.js +51 -0
  152. package/dist/state/session-lock.d.ts +13 -0
  153. package/dist/state/session-lock.js +29 -0
  154. package/dist/state/session-state.d.ts +310 -0
  155. package/dist/state/session-state.js +1083 -0
  156. package/dist/state/sqlite-store-singleton.d.ts +31 -0
  157. package/dist/state/sqlite-store-singleton.js +170 -0
  158. package/dist/state/sqlite-store.d.ts +135 -0
  159. package/dist/state/sqlite-store.js +421 -0
  160. package/dist/telemetry/chat-telemetry.d.ts +105 -0
  161. package/dist/telemetry/chat-telemetry.js +247 -0
  162. package/dist/telemetry/eval-candidate-store.d.ts +50 -0
  163. package/dist/telemetry/eval-candidate-store.js +120 -0
  164. package/dist/telemetry/feedback-store.d.ts +34 -0
  165. package/dist/telemetry/feedback-store.js +76 -0
  166. package/dist/telemetry/jira-telemetry.d.ts +57 -0
  167. package/dist/telemetry/jira-telemetry.js +68 -0
  168. package/dist/telemetry/migration-telemetry.d.ts +35 -0
  169. package/dist/telemetry/migration-telemetry.js +40 -0
  170. package/dist/telemetry/usage.d.ts +24 -0
  171. package/dist/telemetry/usage.js +80 -0
  172. package/dist/tools/builtin-registrations.d.ts +12 -0
  173. package/dist/tools/builtin-registrations.js +33 -0
  174. package/dist/tools/builtins/gdrive-browse.d.ts +3 -0
  175. package/dist/tools/builtins/gdrive-browse.js +68 -0
  176. package/dist/tools/builtins/image-generate.d.ts +3 -0
  177. package/dist/tools/builtins/image-generate.js +211 -0
  178. package/dist/tools/builtins/unsplash-get-by-id.d.ts +23 -0
  179. package/dist/tools/builtins/unsplash-get-by-id.js +119 -0
  180. package/dist/tools/builtins/unsplash-search.d.ts +3 -0
  181. package/dist/tools/builtins/unsplash-search.js +74 -0
  182. package/dist/tools/executor.d.ts +23 -0
  183. package/dist/tools/executor.js +169 -0
  184. package/dist/tools/index.d.ts +5 -0
  185. package/dist/tools/index.js +5 -0
  186. package/dist/tools/registry.d.ts +21 -0
  187. package/dist/tools/registry.js +75 -0
  188. package/dist/tools/runtime.d.ts +27 -0
  189. package/dist/tools/runtime.js +48 -0
  190. package/dist/tools/schema-validator.d.ts +24 -0
  191. package/dist/tools/schema-validator.js +88 -0
  192. package/dist/tools/types.d.ts +86 -0
  193. package/dist/tools/types.js +1 -0
  194. package/dist/variation-images.d.ts +19 -0
  195. package/dist/variation-images.js +12 -0
  196. package/package.json +78 -0
@@ -0,0 +1,672 @@
1
+ #!/usr/bin/env npx tsx
2
+ /**
3
+ * Standalone MCP stdio server for site migration tools.
4
+ * Spawned by the Claude CLI via --mcp-config.
5
+ *
6
+ * Usage: MIGRATION_SESSION=dev npx tsx apps/orchestrator/src/migration/mcp-server-stdio.ts
7
+ */
8
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
9
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
10
+ import { z } from "zod";
11
+ import { scrapeFullPage, buildPageSpecs, extractDesignTokens, mapToThemeVariables, downloadImage, discoverSitePages, } from "@avocadostudio-ai/migration-sdk";
12
+ import { getAllBlockMeta } from "@avocadostudio-ai/shared";
13
+ import { mkdir, writeFile, readFile } from "node:fs/promises";
14
+ import { existsSync } from "node:fs";
15
+ import { join, dirname } from "node:path";
16
+ import { getCachedScrape, setCachedScrape } from "./scrape-cache.js";
17
+ import { saveScreenshot, saveScrapeDebug } from "./migration-tools.js";
18
+ import { getDraftModeSecret } from "../agent/sites-agent-shared.js";
19
+ import { sanitizeSiteId, monorepoRoot, findAvailablePort, patchGlobalsCssVars, normalizePageBlocks, scaffoldSiteProject, analyzeCodebase, cloneRepo, detectSitePort, startAndWaitForDevServer, pageTsx, hybridPageTsx, editorApiRoute, blocksRegisterTsx, defaultsTs, samplePagesJson, defaultLogoSvg, faviconSvg, } from "../agent/sites-agent-shared.js";
20
+ const session = process.env.MIGRATION_SESSION ?? "dev";
21
+ const server = new McpServer({ name: "sites-agent", version: "1.0.0" });
22
+ // ── discover_site_structure ──
23
+ server.tool("discover_site_structure", "Discover pages on a website via sitemap.xml, robots.txt, and link crawling", {
24
+ url: z.string().describe("Site URL to discover"),
25
+ }, async ({ url }) => {
26
+ const result = await discoverSitePages(url);
27
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
28
+ });
29
+ // ── generate_page_specs ──
30
+ server.tool("generate_page_specs", "Scrape a URL and generate section specs with computed CSS styles, content, and design notes", {
31
+ url: z.string().describe("URL to analyze"),
32
+ }, async ({ url }) => {
33
+ let scrape = getCachedScrape(url);
34
+ if (!scrape) {
35
+ scrape = await scrapeFullPage(url);
36
+ setCachedScrape(url, scrape);
37
+ }
38
+ const specs = buildPageSpecs(scrape);
39
+ const tokens = extractDesignTokens(scrape.content.css, scrape.resolvedCssVars);
40
+ const theme = mapToThemeVariables(tokens);
41
+ if (scrape.computedFonts) {
42
+ if (scrape.computedFonts.heading)
43
+ theme["--font-heading"] = scrape.computedFonts.heading + ", sans-serif";
44
+ if (scrape.computedFonts.body)
45
+ theme["--font-body"] = scrape.computedFonts.body + ", sans-serif";
46
+ }
47
+ // Save debug artifacts
48
+ if (scrape.screenshot)
49
+ saveScreenshot("desktop", scrape.screenshot.base64, url).catch(() => { });
50
+ if (scrape.mobileScreenshot)
51
+ saveScreenshot("mobile", scrape.mobileScreenshot.base64, url).catch(() => { });
52
+ saveScrapeDebug(url, { url, specs, designTokens: tokens, themeVariables: theme, nav: scrape.nav, embeds: scrape.embeds, computedFonts: scrape.computedFonts }).catch(() => { });
53
+ return { content: [{ type: "text", text: JSON.stringify({ pageTitle: scrape.content.title, sectionCount: specs.length, specs, designTokens: tokens, themeVariables: theme, nav: scrape.nav, embeds: scrape.embeds }, null, 2) }] };
54
+ });
55
+ // ── scrape_url ──
56
+ server.tool("scrape_url", "Scrape a web page with Playwright, returns sections, design tokens, and screenshot", {
57
+ url: z.string().describe("URL to scrape"),
58
+ }, async ({ url }) => {
59
+ let result = getCachedScrape(url);
60
+ if (!result) {
61
+ result = await scrapeFullPage(url);
62
+ setCachedScrape(url, result);
63
+ }
64
+ const tokens = extractDesignTokens(result.content.css, result.resolvedCssVars);
65
+ const themeVars = mapToThemeVariables(tokens);
66
+ const textData = JSON.stringify({ title: result.content.title, navigation: result.nav, sectionCount: result.sections.length, designTokens: tokens, themeVariables: themeVars });
67
+ const content = [{ type: "text", text: textData }];
68
+ if (result.screenshot)
69
+ content.push({ type: "image", data: result.screenshot.base64, mimeType: "image/jpeg" });
70
+ if (result.mobileScreenshot)
71
+ content.push({ type: "image", data: result.mobileScreenshot.base64, mimeType: "image/jpeg" });
72
+ return { content };
73
+ });
74
+ // ── extract_design_tokens ──
75
+ server.tool("extract_design_tokens", "Extract design tokens (colors, fonts, border radii) from CSS text. Maps them to theme variables.", {
76
+ css: z.string().describe("Raw CSS text to analyze"),
77
+ }, async ({ css }) => {
78
+ try {
79
+ const tokens = extractDesignTokens(css);
80
+ const themeVars = mapToThemeVariables(tokens);
81
+ return {
82
+ content: [{
83
+ type: "text",
84
+ text: JSON.stringify({
85
+ tokens: { colors: tokens.colors.slice(0, 20), fonts: tokens.fonts, radii: tokens.radii.slice(0, 5) },
86
+ themeVariables: themeVars,
87
+ }),
88
+ }],
89
+ };
90
+ }
91
+ catch (e) {
92
+ return { content: [{ type: "text", text: `Error: ${e instanceof Error ? e.message : String(e)}` }], isError: true };
93
+ }
94
+ });
95
+ // ── download_remote_images ──
96
+ server.tool("download_remote_images", "Download multiple images to a site's public/images/ directory", {
97
+ siteId: z.string().describe("Site ID"),
98
+ images: z.array(z.object({ url: z.string(), alt: z.string().optional() })).describe("Images to download"),
99
+ }, async ({ siteId, images }) => {
100
+ const outputDir = join(monorepoRoot(), "apps", siteId, "public", "images");
101
+ await mkdir(outputDir, { recursive: true });
102
+ const results = [];
103
+ for (let i = 0; i < images.length; i += 4) {
104
+ const batch = images.slice(i, i + 4);
105
+ const batchResults = await Promise.all(batch.map(async (img) => {
106
+ try {
107
+ const result = await downloadImage(img.url, img.alt, outputDir);
108
+ return { url: img.url, localUrl: `/images/${result.fileName}` };
109
+ }
110
+ catch {
111
+ return { url: img.url, localUrl: img.url, error: "Download failed" };
112
+ }
113
+ }));
114
+ results.push(...batchResults);
115
+ }
116
+ return { content: [{ type: "text", text: JSON.stringify({ results, succeeded: results.filter(r => !r.error).length }) }] };
117
+ });
118
+ // ── download_remote_image (singular — matches SDK tool name) ──
119
+ server.tool("download_remote_image", "Download a single remote image to the site's public/images/ directory", {
120
+ url: z.string().describe("Image URL to download"),
121
+ siteId: z.string().describe("Site ID"),
122
+ alt: z.string().optional().describe("Alt text"),
123
+ }, async ({ url, siteId, alt }) => {
124
+ try {
125
+ const outputDir = join(monorepoRoot(), "apps", siteId, "public", "images");
126
+ await mkdir(outputDir, { recursive: true });
127
+ const result = await downloadImage(url, alt, outputDir);
128
+ const localUrl = `/images/${result.fileName}`;
129
+ return { content: [{ type: "text", text: JSON.stringify({ localUrl, fileName: result.fileName }) }] };
130
+ }
131
+ catch {
132
+ return { content: [{ type: "text", text: JSON.stringify({ localUrl: url, error: "Download failed, using original URL" }) }] };
133
+ }
134
+ });
135
+ // ── create_site ──
136
+ server.tool("create_site", "Scaffold a Next.js site project in the monorepo", {
137
+ name: z.string().describe("Site name"),
138
+ siteId: z.string().optional().describe("Kebab-case ID"),
139
+ purpose: z.string().optional().describe("Site purpose"),
140
+ }, async ({ name, siteId: rawId, purpose }) => {
141
+ const siteId = rawId ?? sanitizeSiteId(name);
142
+ const root = monorepoRoot();
143
+ const projectDir = join(root, "apps", siteId);
144
+ try {
145
+ // If project already exists, clean it and reuse
146
+ if (existsSync(projectDir)) {
147
+ let port = 0;
148
+ try {
149
+ const pkg = JSON.parse(await readFile(join(projectDir, "package.json"), "utf-8"));
150
+ const m = pkg.scripts?.dev?.match(/-p\s*(\d+)/);
151
+ if (m)
152
+ port = Number(m[1]);
153
+ }
154
+ catch { /* use default */ }
155
+ // Validate existing port is free; fall back to auto-assign
156
+ if (port) {
157
+ const { createServer } = await import("node:net");
158
+ const free = await new Promise((res) => {
159
+ const s = createServer();
160
+ s.once("error", () => res(false));
161
+ s.once("listening", () => { s.close(() => res(true)); });
162
+ s.listen(port, "127.0.0.1");
163
+ });
164
+ if (!free)
165
+ port = 0;
166
+ }
167
+ if (!port)
168
+ port = await findAvailablePort(root);
169
+ const { rm } = await import("node:fs/promises");
170
+ for (const dir of ["content", "blocks", "public/images", ".next"]) {
171
+ const target = join(projectDir, dir);
172
+ if (existsSync(target))
173
+ await rm(target, { recursive: true, force: true });
174
+ }
175
+ await mkdir(join(projectDir, "content"), { recursive: true });
176
+ await mkdir(join(projectDir, "blocks"), { recursive: true });
177
+ await mkdir(join(projectDir, "public/images"), { recursive: true });
178
+ await writeFile(join(projectDir, "blocks/register.tsx"), `import { registerCustomRenderer } from "@avocadostudio-ai/blocks"\n`, "utf-8");
179
+ // Update package.json with validated port
180
+ const existingPkg = JSON.parse(await readFile(join(projectDir, "package.json"), "utf-8"));
181
+ if (existingPkg.scripts?.dev) {
182
+ existingPkg.scripts.dev = existingPkg.scripts.dev.replace(/-p\s*\d+/, `-p ${port}`);
183
+ await writeFile(join(projectDir, "package.json"), JSON.stringify(existingPkg, null, 2), "utf-8");
184
+ }
185
+ return { content: [{ type: "text", text: JSON.stringify({ status: "reused", siteId, port, projectPath: projectDir }) }] };
186
+ }
187
+ // New project — use shared scaffolding
188
+ const result = await scaffoldSiteProject({ siteId, name, purpose });
189
+ // Start dev server and wait for readiness
190
+ const { serverReady } = await startAndWaitForDevServer({
191
+ siteId, port: result.port, cwd: root, useFilter: true,
192
+ });
193
+ return {
194
+ content: [{
195
+ type: "text",
196
+ text: JSON.stringify({
197
+ status: "site_created",
198
+ siteId: result.siteId,
199
+ port: result.port,
200
+ projectPath: result.projectDir,
201
+ devServerStarted: serverReady,
202
+ }),
203
+ }],
204
+ };
205
+ }
206
+ catch (e) {
207
+ return { content: [{ type: "text", text: `Error: ${e instanceof Error ? e.message : String(e)}` }], isError: true };
208
+ }
209
+ });
210
+ // ── bootstrap_pages ──
211
+ server.tool("bootstrap_pages", `Create pages with blocks for a site. Built-in block types: ${Object.keys(getAllBlockMeta()).join(", ")}. Custom block types registered in blocks/register.ts are also supported — use the exact component name as the block type.`, {
212
+ siteId: z.string().describe("Site ID"),
213
+ pages: z.array(z.object({
214
+ slug: z.string(),
215
+ title: z.string(),
216
+ blocks: z.array(z.object({ type: z.string(), props: z.record(z.string(), z.unknown()) })),
217
+ meta: z.object({
218
+ title: z.string().optional(),
219
+ description: z.string().optional(),
220
+ }).optional(),
221
+ })),
222
+ themeOverrides: z.record(z.string(), z.string()).optional().describe("CSS variable overrides"),
223
+ navLabels: z.record(z.string(), z.string()).optional().describe("Custom nav labels per slug"),
224
+ navGroups: z.record(z.string(), z.array(z.string())).optional().describe("Nav dropdown groups"),
225
+ siteLogo: z.string().optional().describe("Logo URL"),
226
+ siteName: z.string().optional().describe("Site name for header"),
227
+ }, async ({ siteId, pages, themeOverrides, navLabels, navGroups, siteLogo, siteName }) => {
228
+ try {
229
+ if (!pages.some(p => p.slug === "/" || p.slug === "")) {
230
+ pages.unshift({ slug: "/", title: "Home", blocks: [] });
231
+ }
232
+ const root = monorepoRoot();
233
+ const pagesJsonPath = join(root, "apps", siteId, "content", "pages.json");
234
+ await mkdir(dirname(pagesJsonPath), { recursive: true });
235
+ const footerRef = { value: null };
236
+ // Normalize slugs and validate blocks using shared logic
237
+ const pageDocs = pages.map(page => {
238
+ const slug = page.slug === "/" ? "/" : `/${page.slug.replace(/^\/+/, "").replace(/\/+$/, "")}`;
239
+ const { contentBlocks, footerBlock } = normalizePageBlocks(page.blocks);
240
+ if (footerBlock && !footerRef.value)
241
+ footerRef.value = footerBlock;
242
+ return {
243
+ id: `p_${slug === "/" ? "home" : slug.replace(/^\//, "").replace(/\//g, "_")}`,
244
+ slug,
245
+ title: page.title,
246
+ blocks: contentBlocks,
247
+ ...(page.meta ? { meta: page.meta } : {}),
248
+ updatedAt: new Date().toISOString(),
249
+ };
250
+ });
251
+ await writeFile(pagesJsonPath, JSON.stringify(pageDocs, null, 2) + "\n", "utf-8");
252
+ // Persist site config (nav, logo, footer)
253
+ const configPath = join(root, "apps", siteId, "content", "site-config.json");
254
+ const config = {};
255
+ if (siteName)
256
+ config.name = siteName;
257
+ if (siteLogo)
258
+ config.logo = siteLogo;
259
+ if (navLabels)
260
+ config.navLabels = navLabels;
261
+ if (navGroups)
262
+ config.navGroups = navGroups;
263
+ if (footerRef.value)
264
+ config.footer = { id: "chrome_footer", type: footerRef.value.type, props: footerRef.value.props };
265
+ if (Object.keys(config).length > 0) {
266
+ await writeFile(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
267
+ }
268
+ // Apply theme overrides to globals.css
269
+ if (themeOverrides && Object.keys(themeOverrides).length > 0) {
270
+ await patchGlobalsCssVars(join(root, "apps", siteId, "app", "globals.css"), themeOverrides);
271
+ }
272
+ // Sync with orchestrator draft state
273
+ const orchestratorUrl = process.env.ORCHESTRATOR_URL ?? "http://localhost:4200";
274
+ try {
275
+ await fetch(`${orchestratorUrl}/draft/bootstrap`, {
276
+ method: "POST",
277
+ headers: { "Content-Type": "application/json" },
278
+ body: JSON.stringify({ session, siteId, pages: pageDocs, overwrite: true }),
279
+ });
280
+ }
281
+ catch { /* orchestrator may not be running */ }
282
+ const totalBlocks = pageDocs.reduce((s, p) => s + p.blocks.length, 0);
283
+ return { content: [{ type: "text", text: JSON.stringify({ status: "applied", pagesCreated: pageDocs.length, totalBlocks }) }] };
284
+ }
285
+ catch (e) {
286
+ return { content: [{ type: "text", text: `Error: ${e instanceof Error ? e.message : String(e)}` }], isError: true };
287
+ }
288
+ });
289
+ // ── apply_theme ──
290
+ server.tool("apply_theme", "Apply CSS custom property overrides to a site's theme", {
291
+ siteId: z.string().describe("Site ID"),
292
+ variables: z.record(z.string(), z.string()).describe("CSS variable overrides, e.g. { '--brand': '#2563eb' }"),
293
+ }, async ({ siteId, variables }) => {
294
+ try {
295
+ await patchGlobalsCssVars(join(monorepoRoot(), "apps", siteId, "app", "globals.css"), variables);
296
+ return {
297
+ content: [{
298
+ type: "text",
299
+ text: JSON.stringify({ status: "applied", variableCount: Object.keys(variables).length }),
300
+ }],
301
+ };
302
+ }
303
+ catch (e) {
304
+ return { content: [{ type: "text", text: `Error: ${e instanceof Error ? e.message : String(e)}` }], isError: true };
305
+ }
306
+ });
307
+ // ── clone_repo ──
308
+ server.tool("clone_repo", "Clone a GitHub repository to a local directory. Uses `gh repo clone` for auth, falls back to `git clone`. Returns the local path for use with analyze_codebase.", {
309
+ url: z.string().describe("GitHub repo URL (e.g. 'https://github.com/user/repo') or shorthand ('user/repo')"),
310
+ targetDir: z.string().optional().describe("Target directory name inside apps/. Defaults to repo name."),
311
+ }, async ({ url, targetDir }) => {
312
+ try {
313
+ const result = await cloneRepo(url, targetDir);
314
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
315
+ }
316
+ catch (e) {
317
+ return { content: [{ type: "text", text: `Error cloning repo: ${e instanceof Error ? e.message : String(e)}` }], isError: true };
318
+ }
319
+ });
320
+ // ── integrate_site (composite) ──
321
+ server.tool("integrate_site", "Add AI Site Editor SDK integration to an existing Next.js project in ONE step. Creates all integration files, adds block styles import to layout, installs deps, and starts the dev server.", {
322
+ siteId: z.string().describe("Site ID (kebab-case, matches directory name in apps/)"),
323
+ name: z.string().describe("Human-readable site name"),
324
+ purpose: z.string().optional().describe("What the site is about"),
325
+ layoutPath: z.string().optional().describe("Relative path to layout file"),
326
+ useSrcDir: z.boolean().optional().describe("Whether the project uses src/app/"),
327
+ }, async ({ siteId, name, purpose, layoutPath, useSrcDir }) => {
328
+ try {
329
+ const root = monorepoRoot();
330
+ const projectDir = join(root, "apps", siteId);
331
+ if (!existsSync(join(projectDir, "package.json"))) {
332
+ return { content: [{ type: "text", text: `Error: apps/${siteId}/package.json not found.` }], isError: true };
333
+ }
334
+ const appDir = useSrcDir ? "src/app" : "app";
335
+ const filesCreated = [];
336
+ // Read package.json and auto-detect name
337
+ const pkgPath = join(projectDir, "package.json");
338
+ const pkg = JSON.parse(await readFile(pkgPath, "utf-8"));
339
+ const genericNames = new Set(["sample site", "my site", "site", "test site", "new site", "untitled", ""]);
340
+ let siteName = name;
341
+ if (!siteName || genericNames.has(siteName.toLowerCase())) {
342
+ const lf = layoutPath ? join(projectDir, layoutPath) : join(projectDir, appDir, "layout.tsx");
343
+ if (existsSync(lf)) {
344
+ const src = await readFile(lf, "utf-8");
345
+ const m = src.match(/title:\s*(?:\{\s*default:\s*)?["']([^"']+)["']/);
346
+ if (m?.[1])
347
+ siteName = m[1];
348
+ }
349
+ if (!siteName || genericNames.has(siteName.toLowerCase())) {
350
+ siteName = (pkg.name ?? siteId).replace(/^@[^/]+\//, "").replace(/[-_]/g, " ").replace(/\b\w/g, (c) => c.toUpperCase()).trim() || "My Site";
351
+ }
352
+ }
353
+ // Add workspace deps
354
+ const deps = pkg.dependencies ?? {};
355
+ let depsAdded = false;
356
+ for (const dep of ["@avocadostudio-ai/site-sdk", "@avocadostudio-ai/blocks", "@avocadostudio-ai/shared"]) {
357
+ if (!deps[dep]) {
358
+ deps[dep] = "workspace:*";
359
+ depsAdded = true;
360
+ }
361
+ }
362
+ if (depsAdded) {
363
+ pkg.dependencies = deps;
364
+ await writeFile(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf-8");
365
+ filesCreated.push("package.json (deps added)");
366
+ }
367
+ // Create or wrap catch-all page route for editor-managed pages
368
+ const catchAllDir = join(projectDir, appDir, "[[...slug]]");
369
+ const catchAllPage = join(catchAllDir, "page.tsx");
370
+ const originalPage = join(catchAllDir, "_original-page.tsx");
371
+ const editorPage = join(catchAllDir, "_editor-page.tsx");
372
+ const blocksPrefix = useSrcDir ? "../../../" : "../../";
373
+ let isHybrid = false;
374
+ if (!existsSync(catchAllDir)) {
375
+ // Case A: No catch-all — create standard editor catch-all
376
+ await mkdir(catchAllDir, { recursive: true });
377
+ await writeFile(catchAllPage, pageTsx(siteId).replace('import "../../blocks/register"', `import "${blocksPrefix}blocks/register"`), "utf-8");
378
+ filesCreated.push(`${appDir}/[[...slug]]/page.tsx`);
379
+ }
380
+ else if (existsSync(catchAllPage) && !existsSync(originalPage)) {
381
+ // Case B: Existing catch-all, not yet wrapped — create hybrid
382
+ isHybrid = true;
383
+ const { rename } = await import("node:fs/promises");
384
+ await rename(catchAllPage, originalPage);
385
+ const editorContent = pageTsx(siteId, { chrome: false }).replace('import "../../blocks/register"', `import "${blocksPrefix}blocks/register"`);
386
+ await writeFile(editorPage, editorContent, "utf-8");
387
+ await writeFile(catchAllPage, hybridPageTsx(siteId), "utf-8");
388
+ filesCreated.push(`${appDir}/[[...slug]]/_original-page.tsx (preserved)`);
389
+ filesCreated.push(`${appDir}/[[...slug]]/_editor-page.tsx`);
390
+ filesCreated.push(`${appDir}/[[...slug]]/page.tsx (hybrid wrapper)`);
391
+ }
392
+ else if (existsSync(originalPage)) {
393
+ isHybrid = true;
394
+ }
395
+ // Create editor API route
396
+ const apiDir = join(projectDir, appDir, "api/editor/[...path]");
397
+ const apiBlocksPrefix = useSrcDir ? "../../../../../" : "../../../../";
398
+ if (!existsSync(apiDir)) {
399
+ await mkdir(apiDir, { recursive: true });
400
+ await writeFile(join(apiDir, "route.ts"), editorApiRoute().replace('import "../../../../blocks/register"', `import "${apiBlocksPrefix}blocks/register"`), "utf-8");
401
+ filesCreated.push(`${appDir}/api/editor/[...path]/route.ts`);
402
+ }
403
+ // Create content dir (empty for hybrid — existing pages fall through to original)
404
+ const contentDir = join(projectDir, "content");
405
+ if (!existsSync(join(contentDir, "pages.json"))) {
406
+ await mkdir(contentDir, { recursive: true });
407
+ await writeFile(join(contentDir, "pages.json"), isHybrid ? "[]\n" : samplePagesJson(), "utf-8");
408
+ filesCreated.push("content/pages.json");
409
+ }
410
+ // Create blocks register
411
+ const blocksDir = join(projectDir, "blocks");
412
+ if (!existsSync(join(blocksDir, "register.ts")) && !existsSync(join(blocksDir, "register.tsx"))) {
413
+ await mkdir(blocksDir, { recursive: true });
414
+ await writeFile(join(blocksDir, "register.tsx"), blocksRegisterTsx(), "utf-8");
415
+ filesCreated.push("blocks/register.tsx");
416
+ }
417
+ // Create lib/defaults.ts
418
+ const libDir = join(projectDir, "lib");
419
+ if (!existsSync(join(libDir, "defaults.ts"))) {
420
+ await mkdir(libDir, { recursive: true });
421
+ await writeFile(join(libDir, "defaults.ts"), defaultsTs(siteId, siteName), "utf-8");
422
+ filesCreated.push("lib/defaults.ts");
423
+ }
424
+ // Create/merge .env.local
425
+ const envPath = join(projectDir, ".env.local");
426
+ const envVars = {
427
+ ORCHESTRATOR_URL: "http://localhost:4200",
428
+ DRAFT_MODE_SECRET: getDraftModeSecret(),
429
+ NEXT_PUBLIC_DEFAULT_SITE_ID: siteId,
430
+ NEXT_PUBLIC_SITE_NAME: siteName,
431
+ NEXT_PUBLIC_EDITOR_ORIGIN: "http://localhost:4100",
432
+ };
433
+ if (existsSync(envPath)) {
434
+ const existing = await readFile(envPath, "utf-8");
435
+ const existingKeys = new Set(existing.split("\n").map(l => l.split("=")[0]).filter(Boolean));
436
+ const newLines = [];
437
+ for (const [k, v] of Object.entries(envVars)) {
438
+ if (!existingKeys.has(k))
439
+ newLines.push(`${k}=${v}`);
440
+ }
441
+ if (newLines.length > 0) {
442
+ await writeFile(envPath, existing.trimEnd() + "\n" + newLines.join("\n") + "\n", "utf-8");
443
+ filesCreated.push(".env.local (merged)");
444
+ }
445
+ }
446
+ else {
447
+ await writeFile(envPath, Object.entries(envVars).map(([k, v]) => `${k}=${v}`).join("\n") + "\n", "utf-8");
448
+ filesCreated.push(".env.local");
449
+ }
450
+ // Add block styles import + EditorOverlay to layout
451
+ const layoutFile = layoutPath ? join(projectDir, layoutPath) : join(projectDir, appDir, "layout.tsx");
452
+ if (existsSync(layoutFile)) {
453
+ let layoutContent = await readFile(layoutFile, "utf-8");
454
+ let layoutModified = false;
455
+ if (!layoutContent.includes("@avocadostudio-ai/blocks/styles.css")) {
456
+ const lines = layoutContent.split("\n");
457
+ let lastImportIdx = -1;
458
+ for (let i = 0; i < lines.length; i++) {
459
+ if (lines[i].startsWith("import "))
460
+ lastImportIdx = i;
461
+ }
462
+ if (lastImportIdx >= 0) {
463
+ lines.splice(lastImportIdx + 1, 0, 'import "@avocadostudio-ai/blocks/styles.css"');
464
+ }
465
+ else {
466
+ lines.unshift('import "@avocadostudio-ai/blocks/styles.css"');
467
+ }
468
+ layoutContent = lines.join("\n");
469
+ layoutModified = true;
470
+ }
471
+ if (!layoutContent.includes("EditorOverlay")) {
472
+ const lines = layoutContent.split("\n");
473
+ let lastImportIdx = -1;
474
+ for (let i = 0; i < lines.length; i++) {
475
+ if (lines[i].startsWith("import "))
476
+ lastImportIdx = i;
477
+ }
478
+ const overlayImport = 'import { EditorOverlay } from "@avocadostudio-ai/site-sdk/editor"';
479
+ if (lastImportIdx >= 0) {
480
+ lines.splice(lastImportIdx + 1, 0, overlayImport);
481
+ }
482
+ else {
483
+ lines.unshift(overlayImport);
484
+ }
485
+ layoutContent = lines.join("\n");
486
+ layoutContent = layoutContent.replace("</body>", ` <EditorOverlay slug="/" editorOrigin={process.env.NEXT_PUBLIC_EDITOR_ORIGIN ?? "http://localhost:4100"} />\n </body>`);
487
+ layoutModified = true;
488
+ }
489
+ if (layoutModified) {
490
+ await writeFile(layoutFile, layoutContent, "utf-8");
491
+ filesCreated.push(layoutPath ?? `${appDir}/layout.tsx (styles + editor overlay added)`);
492
+ }
493
+ }
494
+ // Default assets
495
+ const publicDir = join(projectDir, "public");
496
+ if (!existsSync(publicDir))
497
+ await mkdir(publicDir, { recursive: true });
498
+ if (!existsSync(join(publicDir, "logo.svg"))) {
499
+ await writeFile(join(publicDir, "logo.svg"), defaultLogoSvg(siteName), "utf-8");
500
+ filesCreated.push("public/logo.svg");
501
+ }
502
+ if (!existsSync(join(publicDir, "favicon.svg"))) {
503
+ await writeFile(join(publicDir, "favicon.svg"), faviconSvg(siteName), "utf-8");
504
+ filesCreated.push("public/favicon.svg");
505
+ }
506
+ // Install deps
507
+ if (depsAdded) {
508
+ const { execFile } = await import("node:child_process");
509
+ const { promisify } = await import("node:util");
510
+ await promisify(execFile)("pnpm", ["install", "--no-frozen-lockfile"], { cwd: root, timeout: 60_000 });
511
+ }
512
+ // Detect port for reference (dev server started separately via launch_site)
513
+ const port = await detectSitePort(projectDir);
514
+ return {
515
+ content: [{
516
+ type: "text",
517
+ text: JSON.stringify({ status: "integrated", siteId, port, name: siteName, filesCreated }),
518
+ }],
519
+ };
520
+ }
521
+ catch (e) {
522
+ return { content: [{ type: "text", text: `Error: ${e instanceof Error ? e.message : String(e)}` }], isError: true };
523
+ }
524
+ });
525
+ // ── launch_site ──
526
+ server.tool("launch_site", "Start the dev server for an integrated site, wait for it to be ready, and register it in the editor. Call this AFTER integrate_site completes. Returns the confirmed preview URL once the server is responding.", {
527
+ siteId: z.string().describe("Site ID (matches directory name in apps/)"),
528
+ name: z.string().optional().describe("Human-readable site name"),
529
+ purpose: z.string().optional().describe("What the site is about"),
530
+ }, async ({ siteId, name, purpose }) => {
531
+ try {
532
+ const root = monorepoRoot();
533
+ const projectDir = join(root, "apps", siteId);
534
+ if (!existsSync(join(projectDir, "package.json"))) {
535
+ return { content: [{ type: "text", text: `Error: apps/${siteId}/package.json not found.` }], isError: true };
536
+ }
537
+ let port = await detectSitePort(projectDir);
538
+ // Check if port is in use — if so, find a free one and update package.json
539
+ try {
540
+ const { execSync } = await import("node:child_process");
541
+ const pids = execSync(`lsof -ti:${port} 2>/dev/null`, { encoding: "utf-8" }).trim();
542
+ if (pids) {
543
+ port = await findAvailablePort(root);
544
+ const pkg2 = JSON.parse(await readFile(join(projectDir, "package.json"), "utf-8"));
545
+ if (pkg2.scripts?.dev) {
546
+ pkg2.scripts.dev = pkg2.scripts.dev.replace(/-p\s*\d+/, `-p ${port}`);
547
+ await writeFile(join(projectDir, "package.json"), JSON.stringify(pkg2, null, 2) + "\n", "utf-8");
548
+ }
549
+ }
550
+ }
551
+ catch { }
552
+ // Sync with orchestrator
553
+ const orchestratorUrl = process.env.ORCHESTRATOR_URL ?? "http://localhost:4200";
554
+ try {
555
+ await fetch(`${orchestratorUrl}/draft/bootstrap`, {
556
+ method: "POST",
557
+ headers: { "Content-Type": "application/json" },
558
+ body: JSON.stringify({ session, siteId, pages: [], overwrite: false }),
559
+ });
560
+ }
561
+ catch { }
562
+ // Start dev server and listen for "Ready" message
563
+ const { spawn } = await import("node:child_process");
564
+ const devProcess = spawn("pnpm", ["dev"], {
565
+ cwd: projectDir,
566
+ stdio: ["ignore", "pipe", "pipe"],
567
+ detached: true,
568
+ env: { ...process.env, WATCHPACK_POLLING: "true", WATCHPACK_POLLING_INTERVAL: "1000", CHOKIDAR_USEPOLLING: "true", CHOKIDAR_INTERVAL: "1000" },
569
+ });
570
+ devProcess.unref();
571
+ let serverReady = false;
572
+ const previewUrl = `http://localhost:${port}`;
573
+ await new Promise((resolve) => {
574
+ const timeout = setTimeout(() => { resolve(); }, 60_000);
575
+ const onData = (chunk) => {
576
+ const text = chunk.toString();
577
+ if (text.includes("Ready") || text.includes(`localhost:${port}`)) {
578
+ serverReady = true;
579
+ clearTimeout(timeout);
580
+ devProcess.stdout?.removeAllListeners();
581
+ devProcess.stderr?.removeAllListeners();
582
+ resolve();
583
+ }
584
+ };
585
+ devProcess.stdout?.on("data", onData);
586
+ devProcess.stderr?.on("data", onData);
587
+ devProcess.on("exit", () => { clearTimeout(timeout); resolve(); });
588
+ });
589
+ const siteName = name ?? siteId;
590
+ return {
591
+ content: [{
592
+ type: "text",
593
+ text: JSON.stringify({ status: serverReady ? "running" : "timeout", siteId, port, previewUrl, name: siteName, serverReady }),
594
+ }],
595
+ };
596
+ }
597
+ catch (e) {
598
+ return { content: [{ type: "text", text: `Error: ${e instanceof Error ? e.message : String(e)}` }], isError: true };
599
+ }
600
+ });
601
+ // ── register_site ──
602
+ server.tool("register_site", "Register an existing site project with the editor and start its dev server. Call this AFTER integrating the SDK. The site will appear in the editor's site list.", {
603
+ siteId: z.string().describe("Site ID (kebab-case, matches directory name in apps/)"),
604
+ name: z.string().describe("Human-readable site name"),
605
+ purpose: z.string().optional().describe("What the site is about"),
606
+ port: z.number().optional().describe("Dev server port (auto-detected if omitted)"),
607
+ }, async ({ siteId, name, purpose, port: explicitPort }) => {
608
+ try {
609
+ const root = monorepoRoot();
610
+ const projectDir = join(root, "apps", siteId);
611
+ if (!existsSync(join(projectDir, "package.json"))) {
612
+ return { content: [{ type: "text", text: `Error: apps/${siteId}/package.json not found.` }], isError: true };
613
+ }
614
+ const port = explicitPort ?? await detectSitePort(projectDir);
615
+ // Install if needed
616
+ if (!existsSync(join(projectDir, "node_modules"))) {
617
+ const { execFile } = await import("node:child_process");
618
+ const { promisify } = await import("node:util");
619
+ await promisify(execFile)("pnpm", ["install", "--no-frozen-lockfile"], { cwd: root, timeout: 60_000 });
620
+ }
621
+ // Start dev server
622
+ const { spawn } = await import("node:child_process");
623
+ const devProcess = spawn("pnpm", ["--filter", `@ai-site-editor/${siteId}`, "dev"], {
624
+ cwd: root,
625
+ stdio: "ignore",
626
+ detached: true,
627
+ env: { ...process.env, WATCHPACK_POLLING: "true", WATCHPACK_POLLING_INTERVAL: "1000", CHOKIDAR_USEPOLLING: "true", CHOKIDAR_INTERVAL: "1000" },
628
+ });
629
+ devProcess.unref();
630
+ // Sync with orchestrator draft state
631
+ const orchestratorUrl = process.env.ORCHESTRATOR_URL ?? "http://localhost:4200";
632
+ try {
633
+ await fetch(`${orchestratorUrl}/draft/bootstrap`, {
634
+ method: "POST",
635
+ headers: { "Content-Type": "application/json" },
636
+ body: JSON.stringify({ session, siteId, pages: [], overwrite: false }),
637
+ });
638
+ }
639
+ catch { /* orchestrator may not be running */ }
640
+ return {
641
+ content: [{
642
+ type: "text",
643
+ text: JSON.stringify({
644
+ status: "registered",
645
+ siteId,
646
+ port,
647
+ name,
648
+ previewUrl: `http://localhost:${port}`,
649
+ devServerStarted: true,
650
+ }),
651
+ }],
652
+ };
653
+ }
654
+ catch (e) {
655
+ return { content: [{ type: "text", text: `Error: ${e instanceof Error ? e.message : String(e)}` }], isError: true };
656
+ }
657
+ });
658
+ // ── analyze_codebase ──
659
+ server.tool("analyze_codebase", "Analyze an existing site project to detect framework, CMS, routes, styling, and readiness for AI Site Editor integration", {
660
+ projectPath: z.string().describe("Absolute path to the project root directory"),
661
+ }, async ({ projectPath }) => {
662
+ try {
663
+ const analysis = await analyzeCodebase(projectPath);
664
+ return { content: [{ type: "text", text: JSON.stringify(analysis, null, 2) }] };
665
+ }
666
+ catch (e) {
667
+ return { content: [{ type: "text", text: `Error: ${e instanceof Error ? e.message : String(e)}` }], isError: true };
668
+ }
669
+ });
670
+ // Start stdio transport
671
+ const transport = new StdioServerTransport();
672
+ await server.connect(transport);