@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.
- package/LICENSE +201 -0
- package/dist/agent/agent-context.d.ts +19 -0
- package/dist/agent/agent-context.js +67 -0
- package/dist/agent/agent-logger.d.ts +5 -0
- package/dist/agent/agent-logger.js +22 -0
- package/dist/agent/agent-loop-openai.d.ts +8 -0
- package/dist/agent/agent-loop-openai.js +172 -0
- package/dist/agent/agent-loop.d.ts +56 -0
- package/dist/agent/agent-loop.js +167 -0
- package/dist/agent/agent-provider.d.ts +28 -0
- package/dist/agent/agent-provider.js +63 -0
- package/dist/agent/agent-tools.d.ts +28 -0
- package/dist/agent/agent-tools.js +899 -0
- package/dist/agent/context/editing-guidelines.md +46 -0
- package/dist/agent/context/role.md +39 -0
- package/dist/agent/integration-prompt.d.ts +9 -0
- package/dist/agent/integration-prompt.js +154 -0
- package/dist/agent/sites-agent-context.d.ts +12 -0
- package/dist/agent/sites-agent-context.js +316 -0
- package/dist/agent/sites-agent-shared.d.ts +161 -0
- package/dist/agent/sites-agent-shared.js +1101 -0
- package/dist/agent/sites-agent-tools.d.ts +18 -0
- package/dist/agent/sites-agent-tools.js +1227 -0
- package/dist/chat/anthropic-cache.d.ts +20 -0
- package/dist/chat/anthropic-cache.js +54 -0
- package/dist/chat/anthropic-planner.d.ts +98 -0
- package/dist/chat/anthropic-planner.js +1012 -0
- package/dist/chat/changelog-coverage-validator.d.ts +37 -0
- package/dist/chat/changelog-coverage-validator.js +215 -0
- package/dist/chat/chat-pipeline-context.d.ts +211 -0
- package/dist/chat/chat-pipeline-context.js +249 -0
- package/dist/chat/chat-pipeline-deterministic.d.ts +61 -0
- package/dist/chat/chat-pipeline-deterministic.js +407 -0
- package/dist/chat/chat-pipeline-image.d.ts +86 -0
- package/dist/chat/chat-pipeline-image.js +897 -0
- package/dist/chat/chat-pipeline-shared.d.ts +69 -0
- package/dist/chat/chat-pipeline-shared.js +212 -0
- package/dist/chat/chat-pipeline-translation.d.ts +27 -0
- package/dist/chat/chat-pipeline-translation.js +417 -0
- package/dist/chat/chat-pipeline-ui.d.ts +14 -0
- package/dist/chat/chat-pipeline-ui.js +244 -0
- package/dist/chat/chat-pipeline.d.ts +99 -0
- package/dist/chat/chat-pipeline.js +3999 -0
- package/dist/chat/decomposer.d.ts +21 -0
- package/dist/chat/decomposer.js +65 -0
- package/dist/chat/gemini-planner.d.ts +70 -0
- package/dist/chat/gemini-planner.js +541 -0
- package/dist/chat/hallucination-validator.d.ts +36 -0
- package/dist/chat/hallucination-validator.js +110 -0
- package/dist/chat/locale-strings.d.ts +47 -0
- package/dist/chat/locale-strings.js +100 -0
- package/dist/chat/plan-json-schema.d.ts +133 -0
- package/dist/chat/plan-json-schema.js +112 -0
- package/dist/chat/planner-types.d.ts +120 -0
- package/dist/chat/planner-types.js +66 -0
- package/dist/chat/planner.d.ts +148 -0
- package/dist/chat/planner.js +1361 -0
- package/dist/chat/prompts.d.ts +67 -0
- package/dist/chat/prompts.js +356 -0
- package/dist/chat/provider-routing.d.ts +14 -0
- package/dist/chat/provider-routing.js +27 -0
- package/dist/chat/variation-pipeline.d.ts +135 -0
- package/dist/chat/variation-pipeline.js +837 -0
- package/dist/chat/vision-alt-generator.d.ts +35 -0
- package/dist/chat/vision-alt-generator.js +152 -0
- package/dist/cms/adapter.d.ts +62 -0
- package/dist/cms/adapter.js +1 -0
- package/dist/cms/bootstrap.d.ts +17 -0
- package/dist/cms/bootstrap.js +85 -0
- package/dist/cms/editor-api-adapter.d.ts +23 -0
- package/dist/cms/editor-api-adapter.js +71 -0
- package/dist/cms/index.d.ts +4 -0
- package/dist/cms/index.js +3 -0
- package/dist/cms/json-file-adapter.d.ts +11 -0
- package/dist/cms/json-file-adapter.js +62 -0
- package/dist/demo-mode.d.ts +59 -0
- package/dist/demo-mode.js +201 -0
- package/dist/errors.d.ts +67 -0
- package/dist/errors.js +129 -0
- package/dist/http/chat-stream-resumable.d.ts +108 -0
- package/dist/http/chat-stream-resumable.js +290 -0
- package/dist/http/chat-stream.d.ts +99 -0
- package/dist/http/chat-stream.js +92 -0
- package/dist/image/gdrive-client.d.ts +22 -0
- package/dist/image/gdrive-client.js +215 -0
- package/dist/image/image-helpers.d.ts +95 -0
- package/dist/image/image-helpers.js +488 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/jira/jira-approval.d.ts +22 -0
- package/dist/jira/jira-approval.js +51 -0
- package/dist/jira/jira-client.d.ts +44 -0
- package/dist/jira/jira-client.js +313 -0
- package/dist/jira/jira-poller.d.ts +46 -0
- package/dist/jira/jira-poller.js +184 -0
- package/dist/jira/jira-processor.d.ts +103 -0
- package/dist/jira/jira-processor.js +1085 -0
- package/dist/jira/jira-types.d.ts +117 -0
- package/dist/jira/jira-types.js +38 -0
- package/dist/logger.d.ts +12 -0
- package/dist/logger.js +28 -0
- package/dist/migration/mcp-server-stdio.d.ts +8 -0
- package/dist/migration/mcp-server-stdio.js +672 -0
- package/dist/migration/migration-prompt.d.ts +7 -0
- package/dist/migration/migration-prompt.js +197 -0
- package/dist/migration/migration-tools.d.ts +17 -0
- package/dist/migration/migration-tools.js +159 -0
- package/dist/migration/scrape-cache.d.ts +9 -0
- package/dist/migration/scrape-cache.js +19 -0
- package/dist/nlp/deterministic-planner-context.d.ts +141 -0
- package/dist/nlp/deterministic-planner-context.js +362 -0
- package/dist/nlp/deterministic-planner-pages.d.ts +26 -0
- package/dist/nlp/deterministic-planner-pages.js +170 -0
- package/dist/nlp/deterministic-planner-patches.d.ts +80 -0
- package/dist/nlp/deterministic-planner-patches.js +508 -0
- package/dist/nlp/deterministic-planner-refs.d.ts +33 -0
- package/dist/nlp/deterministic-planner-refs.js +164 -0
- package/dist/nlp/deterministic-planner-suggestions.d.ts +49 -0
- package/dist/nlp/deterministic-planner-suggestions.js +579 -0
- package/dist/nlp/deterministic-planner.d.ts +85 -0
- package/dist/nlp/deterministic-planner.js +1631 -0
- package/dist/nlp/intent-detection.d.ts +309 -0
- package/dist/nlp/intent-detection.js +730 -0
- package/dist/nlp/intent-helpers.d.ts +15 -0
- package/dist/nlp/intent-helpers.js +243 -0
- package/dist/nlp/intent-patterns.d.ts +40 -0
- package/dist/nlp/intent-patterns.js +223 -0
- package/dist/nlp/plan-normalizer.d.ts +41 -0
- package/dist/nlp/plan-normalizer.js +1537 -0
- package/dist/ops/destructive-action-gate.d.ts +44 -0
- package/dist/ops/destructive-action-gate.js +90 -0
- package/dist/ops/ops-engine.d.ts +151 -0
- package/dist/ops/ops-engine.js +1394 -0
- package/dist/publish/diff-engine.d.ts +18 -0
- package/dist/publish/diff-engine.js +305 -0
- package/dist/publish/publish-helpers.d.ts +87 -0
- package/dist/publish/publish-helpers.js +521 -0
- package/dist/publish/publish-target-registry.d.ts +7 -0
- package/dist/publish/publish-target-registry.js +61 -0
- package/dist/publish/publish-target.d.ts +81 -0
- package/dist/publish/publish-target.js +1 -0
- package/dist/publish/targets/deploy-hook.d.ts +13 -0
- package/dist/publish/targets/deploy-hook.js +123 -0
- package/dist/publish/targets/git.d.ts +13 -0
- package/dist/publish/targets/git.js +55 -0
- package/dist/publish/targets/site-contract.d.ts +19 -0
- package/dist/publish/targets/site-contract.js +124 -0
- package/dist/state/content-source.d.ts +17 -0
- package/dist/state/content-source.js +1 -0
- package/dist/state/in-memory-content-source.d.ts +27 -0
- package/dist/state/in-memory-content-source.js +51 -0
- package/dist/state/session-lock.d.ts +13 -0
- package/dist/state/session-lock.js +29 -0
- package/dist/state/session-state.d.ts +310 -0
- package/dist/state/session-state.js +1083 -0
- package/dist/state/sqlite-store-singleton.d.ts +31 -0
- package/dist/state/sqlite-store-singleton.js +170 -0
- package/dist/state/sqlite-store.d.ts +135 -0
- package/dist/state/sqlite-store.js +421 -0
- package/dist/telemetry/chat-telemetry.d.ts +105 -0
- package/dist/telemetry/chat-telemetry.js +247 -0
- package/dist/telemetry/eval-candidate-store.d.ts +50 -0
- package/dist/telemetry/eval-candidate-store.js +120 -0
- package/dist/telemetry/feedback-store.d.ts +34 -0
- package/dist/telemetry/feedback-store.js +76 -0
- package/dist/telemetry/jira-telemetry.d.ts +57 -0
- package/dist/telemetry/jira-telemetry.js +68 -0
- package/dist/telemetry/migration-telemetry.d.ts +35 -0
- package/dist/telemetry/migration-telemetry.js +40 -0
- package/dist/telemetry/usage.d.ts +24 -0
- package/dist/telemetry/usage.js +80 -0
- package/dist/tools/builtin-registrations.d.ts +12 -0
- package/dist/tools/builtin-registrations.js +33 -0
- package/dist/tools/builtins/gdrive-browse.d.ts +3 -0
- package/dist/tools/builtins/gdrive-browse.js +68 -0
- package/dist/tools/builtins/image-generate.d.ts +3 -0
- package/dist/tools/builtins/image-generate.js +211 -0
- package/dist/tools/builtins/unsplash-get-by-id.d.ts +23 -0
- package/dist/tools/builtins/unsplash-get-by-id.js +119 -0
- package/dist/tools/builtins/unsplash-search.d.ts +3 -0
- package/dist/tools/builtins/unsplash-search.js +74 -0
- package/dist/tools/executor.d.ts +23 -0
- package/dist/tools/executor.js +169 -0
- package/dist/tools/index.d.ts +5 -0
- package/dist/tools/index.js +5 -0
- package/dist/tools/registry.d.ts +21 -0
- package/dist/tools/registry.js +75 -0
- package/dist/tools/runtime.d.ts +27 -0
- package/dist/tools/runtime.js +48 -0
- package/dist/tools/schema-validator.d.ts +24 -0
- package/dist/tools/schema-validator.js +88 -0
- package/dist/tools/types.d.ts +86 -0
- package/dist/tools/types.js +1 -0
- package/dist/variation-images.d.ts +19 -0
- package/dist/variation-images.js +12 -0
- 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);
|