import { createServer } from "node:http"; import { mkdir, readFile, writeFile } from "node:fs/promises"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; import { execSync } from "node:child_process"; import { joinSession, createCanvas, CanvasError } from "@github/copilot-sdk/extension"; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); const ARTIFACTS_DIR = join(__dirname, "artifacts"); const STATE_FILE = join(ARTIFACTS_DIR, "state.json"); const REPO_ROOT_FALLBACK = join(__dirname, "..", "..", ".."); const SECTION_TEMPLATE = [ { id: "design-system", name: "Design System", icon: "π¨" }, { id: "hero", name: "Hero", icon: "π " }, { id: "nav", name: "Navigation", icon: "π§" }, { id: "about", name: "About", icon: "π€" }, { id: "talks", name: "Conference Talks", icon: "π€" }, { id: "videos", name: "Videos", icon: "π₯" }, { id: "contact", name: "Contact", icon: "π¬" }, ]; const VALID_STATUS = new Set(["Not Started", "In Progress", "Built", "Review", "Approved", "Needs Changes"]); const VALID_CHANGE_TYPES = new Set(["file_modified", "file_added", "file_deleted", "status_change", "commit", "milestone", "ai_request"]); const STATUS_ORDER = ["Not Started", "In Progress", "Built", "Review", "Approved", "Needs Changes"]; const SECTION_FIELD_SUGGESTIONS = { "design-system": [ { field: "palette", prompt: "What are your core brand colors (base, accent, muted, success, warning)?" }, { field: "typography", prompt: "What fonts and weights should headings/body/code use?" }, { field: "spacing-scale", prompt: "What spacing scale should components follow?" }, { field: "radius", prompt: "What border radius values should be standard?" }, { field: "motion", prompt: "What motion durations/easing should be standard?" }, { field: "component-rules", prompt: "What visual rules should cards, buttons, and inputs follow?" }, ], hero: [ { field: "headline", prompt: "What should your hero headline say?" }, { field: "subheadline", prompt: "What supporting subheadline should appear below it?" }, { field: "cta-primary", prompt: "Primary CTA button link (full URL)?" }, { field: "cta-secondary", prompt: "Secondary CTA button link (full URL)?" }, ], nav: [ { field: "links", prompt: "Which nav links should appear and in what order?" }, { field: "style", prompt: "How should nav styling behave on scroll?" }, ], about: [ { field: "bio", prompt: "What short bio should appear?" }, { field: "stats", prompt: "Which key stats should be shown?" }, ], talks: [ { field: "featured", prompt: "Which featured talk should be highlighted first?" }, { field: "list", prompt: "Which talks should be listed and how?" }, ], videos: [ { field: "featured", prompt: "Which featured video should be highlighted first?" }, { field: "grid", prompt: "What metadata should each video card show?" }, ], contact: [ { field: "intro", prompt: "What contact intro copy should be displayed?" }, { field: "links", prompt: "Which social/contact links should be shown?" }, ], }; const SECTION_DEFAULT_CONTENT = { "design-system": { palette: "[Sample: List your core brand colors β base, accent, and muted]", typography: "[Sample: Name the fonts for headings, body, and code]", }, hero: { headline: "[Sample: Write a short headline that says who you are]", subheadline: "[Sample: Add one sentence on what you do and who you help]", "cta-primary": "[Sample: Add the link your main button should point to]", }, nav: { links: "[Sample: List your nav links in order, e.g. Home, About, Work, Contact]", }, about: { bio: "[Sample: Write two or three sentences about your background and focus]", }, talks: { featured: "[Sample: Name a talk β title, event, and year]", }, videos: { featured: "[Sample: Add a link to a video you want to feature]", }, contact: { intro: "[Sample: Write a short line inviting people to reach out]", links: "[Sample: List your social or contact links]", email: "[Sample: Add the email address to display]", }, }; const DEFAULT_TEMPLATE_PACK = "personal-site-classic"; const TEMPLATE_PACKS = { "personal-site-classic": { label: "Personal Site (Classic)", defaultContent: SECTION_DEFAULT_CONTENT, sections: SECTION_TEMPLATE, fieldSuggestions: SECTION_FIELD_SUGGESTIONS, sectionSchemas: { hero: { headline: { type: "text", required: true, maxLength: 120 }, subheadline: { type: "text", required: true, maxLength: 220 }, "cta-primary": { type: "url", required: true }, "cta-secondary": { type: "url", required: false }, }, about: { bio: { type: "text", required: true, maxLength: 1600 }, }, talks: { featured: { type: "text", required: true, maxLength: 1200 }, }, videos: { featured: { type: "url", required: false }, }, contact: { intro: { type: "text", required: true, maxLength: 400 }, links: { type: "text", required: true, maxLength: 1200 }, email: { type: "email", required: false }, }, }, }, "developer-portfolio": { label: "Developer Portfolio", defaultContent: { hero: { headline: "[Sample: Write a headline describing the kind of engineer you are]", subheadline: "[Sample: Add one line on what you build and your focus]", "cta-primary": "[Sample: Add the link your main button should point to]", }, projects: { featured: "[Sample: Describe a project β what it does, the stack, and the impact]", }, skills: { core: "[Sample: List your core technical skills]", tooling: "[Sample: List the tools and platforms you use]", }, experience: { summary: "[Sample: Summarize your recent role, company, and key wins]", }, writing: { featured: "[Sample: Add an article title and where it was published]", }, contact: { intro: "[Sample: Write a short line about how to reach you]", email: "[Sample: Add the email address to display]", }, }, sections: [ { id: "hero", name: "Hero", icon: "π " }, { id: "projects", name: "Projects", icon: "π§ͺ" }, { id: "skills", name: "Skills", icon: "π οΈ" }, { id: "experience", name: "Experience", icon: "πΌ" }, { id: "writing", name: "Writing", icon: "βοΈ" }, { id: "contact", name: "Contact", icon: "π¬" }, ], fieldSuggestions: { hero: [ { field: "headline", prompt: "What should your hero headline say?" }, { field: "subheadline", prompt: "What short positioning line should appear below it?" }, { field: "cta-primary", prompt: "Primary CTA button link (full URL)?" }, ], projects: [ { field: "featured", prompt: "Which project should be featured first?" }, { field: "list", prompt: "Which additional projects should be listed?" }, ], skills: [ { field: "core", prompt: "What are your core technical skills?" }, { field: "tooling", prompt: "What tools/platforms should be highlighted?" }, ], experience: [ { field: "summary", prompt: "How should your recent experience be summarized?" }, ], writing: [ { field: "featured", prompt: "Any featured article/post to highlight?" }, ], contact: [ { field: "intro", prompt: "What contact intro copy should be displayed?" }, { field: "email", prompt: "What contact email should be shown?" }, ], }, sectionSchemas: { hero: { headline: { type: "text", required: true, maxLength: 120 }, subheadline: { type: "text", required: true, maxLength: 220 }, "cta-primary": { type: "url", required: true }, }, projects: { featured: { type: "text", required: true, maxLength: 900 }, }, contact: { intro: { type: "text", required: true, maxLength: 300 }, email: { type: "email", required: false }, }, }, }, "speaker-site": { label: "Speaker Site", defaultContent: { hero: { headline: "[Sample: Write a headline describing your speaking focus]", positioning: "[Sample: Add a sentence on the value you bring to audiences]", }, talks: { featured: "[Sample: Name your signature keynote and its core idea]", catalog: "[Sample: List your talks with a one-line description each]", }, workshops: { offerings: "[Sample: Describe the workshops you offer]", }, videos: { featured: "[Sample: Add a link to a talk video to feature]", }, testimonials: { quotes: "[Sample: Add an audience or client quote with attribution]", }, contact: { intro: "[Sample: Write a short line for booking inquiries]", email: "[Sample: Add the booking email to display]", }, }, sections: [ { id: "hero", name: "Hero", icon: "π " }, { id: "talks", name: "Talks", icon: "π€" }, { id: "workshops", name: "Workshops", icon: "π§βπ«" }, { id: "videos", name: "Videos", icon: "π₯" }, { id: "testimonials", name: "Testimonials", icon: "π¬" }, { id: "contact", name: "Contact", icon: "π¬" }, ], fieldSuggestions: { hero: [ { field: "headline", prompt: "What should your speaker headline say?" }, { field: "positioning", prompt: "How should your expertise be positioned?" }, ], talks: [ { field: "featured", prompt: "Which keynote/talk should be featured?" }, { field: "catalog", prompt: "What talks should be included in your catalog?" }, ], workshops: [ { field: "offerings", prompt: "Which workshops do you offer?" }, ], videos: [ { field: "featured", prompt: "Which featured video should be highlighted first?" }, ], testimonials: [ { field: "quotes", prompt: "Which audience/client quotes should be highlighted?" }, ], contact: [ { field: "intro", prompt: "What booking/contact intro should be shown?" }, { field: "email", prompt: "What booking email should be shown?" }, ], }, sectionSchemas: { hero: { headline: { type: "text", required: true, maxLength: 120 }, }, talks: { featured: { type: "text", required: true, maxLength: 1000 }, }, contact: { intro: { type: "text", required: true, maxLength: 300 }, email: { type: "email", required: true }, }, }, }, "founder-site": { label: "Founder Site", defaultContent: { hero: { headline: "[Sample: Write your product's one-line value proposition]", "cta-primary": "[Sample: Add the link your main button should point to]", }, product: { "value-prop": "[Sample: Describe what the product does and why it's better]", }, story: { "founder-story": "[Sample: Share why you started the company and your mission]", }, press: { highlights: "[Sample: List notable press mentions, awards, or milestones]", }, contact: { intro: "[Sample: Write a short line for partnership or press inquiries]", email: "[Sample: Add the email address to display]", }, }, sections: [ { id: "hero", name: "Hero", icon: "π" }, { id: "product", name: "Product", icon: "π¦" }, { id: "story", name: "Story", icon: "π" }, { id: "press", name: "Press", icon: "π°" }, { id: "contact", name: "Contact", icon: "π¬" }, ], fieldSuggestions: { hero: [ { field: "headline", prompt: "What should the company/product headline be?" }, { field: "cta-primary", prompt: "Primary CTA button link (full URL)?" }, ], product: [ { field: "value-prop", prompt: "What is the primary product value proposition?" }, ], story: [ { field: "founder-story", prompt: "How should the founder story be told?" }, ], press: [ { field: "highlights", prompt: "Which press mentions should be included?" }, ], contact: [ { field: "intro", prompt: "What investor/customer contact intro should be shown?" }, { field: "email", prompt: "What contact email should be shown?" }, ], }, sectionSchemas: { hero: { headline: { type: "text", required: true, maxLength: 120 }, "cta-primary": { type: "url", required: true }, }, contact: { email: { type: "email", required: true }, }, }, }, "designer-site": { label: "Designer Site", defaultContent: { hero: { headline: "[Sample: Write a headline describing your design focus]", subheadline: "[Sample: Add a sentence on how you help brands or products]", }, work: { featured: "[Sample: Describe a featured project β brief, your role, outcome]", }, "case-studies": { list: "[Sample: List case studies with problem, approach, and result]", }, process: { steps: "[Sample: Outline your design process in a few steps]", }, about: { bio: "[Sample: Write a short bio about your design background]", }, contact: { email: "[Sample: Add the email address to display]", }, }, sections: [ { id: "hero", name: "Hero", icon: "π¨" }, { id: "work", name: "Work", icon: "πΌοΈ" }, { id: "case-studies", name: "Case Studies", icon: "π" }, { id: "process", name: "Process", icon: "π§" }, { id: "about", name: "About", icon: "π€" }, { id: "contact", name: "Contact", icon: "π¬" }, ], fieldSuggestions: { hero: [ { field: "headline", prompt: "What should your design headline say?" }, { field: "subheadline", prompt: "What supporting statement should be shown?" }, ], work: [ { field: "featured", prompt: "What featured visual/project should be shown first?" }, ], "case-studies": [ { field: "list", prompt: "Which case studies should be included?" }, ], process: [ { field: "steps", prompt: "How should your design process be presented?" }, ], about: [ { field: "bio", prompt: "What short bio should appear?" }, ], contact: [ { field: "email", prompt: "What contact email should be shown?" }, ], }, sectionSchemas: { hero: { headline: { type: "text", required: true, maxLength: 120 }, }, contact: { email: { type: "email", required: true }, }, }, }, }; const TEMPLATE_PACK_OPTIONS = Object.entries(TEMPLATE_PACKS).map(([id, pack]) => ({ id, label: pack.label })); let stateCache = null; let stateLoaded = false; const servers = new Map(); let workspacePath = undefined; function nowIso() { return new Date().toISOString(); } function uid(prefix) { return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`; } function safeString(value, fallback = "") { return typeof value === "string" ? value : fallback; } function readBranchFromGit() { if (process.env.COPILOT_WORKSPACE_BRANCH) { return process.env.COPILOT_WORKSPACE_BRANCH; } const candidates = [workspacePath, process.cwd(), REPO_ROOT_FALLBACK].filter((value, index, all) => value && all.indexOf(value) === index); for (const cwd of candidates) { try { const branch = execSync("git --no-pager rev-parse --abbrev-ref HEAD", { cwd, stdio: ["ignore", "pipe", "ignore"], timeout: 2000, maxBuffer: 1024 * 1024 }) .toString() .trim(); if (branch) { return branch; } } catch {} } return "unknown"; } function captureWorkingDirectory(ctx) { if (!workspacePath && ctx?.session?.workingDirectory) { workspacePath = ctx.session.workingDirectory; } } function cloneJson(value) { return JSON.parse(JSON.stringify(value)); } function getTemplatePackOrThrow(packId) { const id = safeString(packId, DEFAULT_TEMPLATE_PACK); const pack = TEMPLATE_PACKS[id]; if (!pack) { throw new CanvasError("unknown_template_pack", `Unknown template pack: ${id}`); } return { id, pack }; } function getFieldSuggestionsForSection(sectionId) { if (!stateCache?.fieldSuggestions || typeof stateCache.fieldSuggestions !== "object") { return []; } return Array.isArray(stateCache.fieldSuggestions[sectionId]) ? stateCache.fieldSuggestions[sectionId] : []; } function getFieldSchemaForSection(sectionId, field) { if (!stateCache?.sectionSchemas || typeof stateCache.sectionSchemas !== "object") { return null; } const sectionSchema = stateCache.sectionSchemas[sectionId]; if (!sectionSchema || typeof sectionSchema !== "object") { return null; } const fieldSchema = sectionSchema[field]; if (!fieldSchema || typeof fieldSchema !== "object") { return null; } return fieldSchema; } function isValidEmail(value) { return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value); } function isValidUrl(value) { try { const url = new URL(value); return ["http:", "https:"].includes(url.protocol); } catch { return false; } } function validateFieldValue(sectionId, field, value, mark = "draft") { const schema = getFieldSchemaForSection(sectionId, field); if (!schema) { return; } const text = safeString(value, ""); const trimmed = text.trim(); const required = Boolean(schema.required); if (required && mark === "final" && !trimmed) { throw new CanvasError("required_field_missing", `"${field}" is required for ${sectionId}.`); } if (!trimmed) { return; } if (isSamplePlaceholder(trimmed)) { return; } const maxLength = Number.isInteger(schema.maxLength) ? schema.maxLength : null; if (maxLength && text.length > maxLength) { throw new CanvasError("max_length_exceeded", `"${field}" exceeds max length (${maxLength}).`); } const type = safeString(schema.type, "text"); if (type === "url" && !isValidUrl(trimmed)) { throw new CanvasError("invalid_url", `"${field}" must be a valid http/https URL.`); } if (type === "email" && !isValidEmail(trimmed)) { throw new CanvasError("invalid_email", `"${field}" must be a valid email address.`); } } function applyTemplatePack(packId, author = "agent") { const { id, pack } = getTemplatePackOrThrow(packId); const ts = nowIso(); stateCache.templatePackId = id; stateCache.fieldSuggestions = cloneJson(pack.fieldSuggestions || {}); stateCache.sectionSchemas = cloneJson(pack.sectionSchemas || {}); stateCache.sections = (pack.sections || []).map((section) => ({ ...section, status: "Not Started", notes: [], content: cloneJson(pack.defaultContent?.[section.id] || {}), filesChanged: [], lastModified: ts, })); stateCache.changeLog = []; stateCache.contentProposals = []; stateCache.milestones = []; stateCache.lastUpdated = ts; addChange({ type: "status_change", summary: `Template pack applied: ${pack.label}.`, timestamp: ts, }); addNote( stateCache.sections[0]?.id || "hero", `Applied template pack "${pack.label}".`, author === "human" ? "human" : "agent", ); return { templatePackId: id, label: pack.label, sections: stateCache.sections }; } function createInitialState() { const ts = nowIso(); const { id, pack } = getTemplatePackOrThrow(DEFAULT_TEMPLATE_PACK); return { sections: pack.sections.map((section) => ({ ...section, status: "Not Started", notes: [], content: cloneJson(pack.defaultContent?.[section.id] || {}), filesChanged: [], lastModified: ts, })), templatePackId: id, fieldSuggestions: cloneJson(pack.fieldSuggestions || {}), sectionSchemas: cloneJson(pack.sectionSchemas || {}), changeLog: [], contentProposals: [], milestones: [], theme: { style: "dark", palette: "#050A14/#2F80ED" }, branch: readBranchFromGit(), lastUpdated: ts, }; } let persistQueue = Promise.resolve(); function persistState() { if (!stateCache) { return persistQueue; } // Snapshot synchronously and serialize writes so concurrent mutations // persist in call order β a slow earlier write can't clobber a newer one. const snapshot = JSON.stringify(stateCache, null, 2); persistQueue = persistQueue .catch(() => {}) .then(async () => { await mkdir(ARTIFACTS_DIR, { recursive: true }); await writeFile(STATE_FILE, snapshot, "utf8"); }); return persistQueue; } function normalizeState(raw) { if (!raw || typeof raw !== "object") { return createInitialState(); } const fallback = createInitialState(); const templatePackId = safeString(raw.templatePackId, fallback.templatePackId); const normalizedTemplatePackId = TEMPLATE_PACKS[templatePackId] ? templatePackId : fallback.templatePackId; return { sections: Array.isArray(raw.sections) ? raw.sections : fallback.sections, templatePackId: normalizedTemplatePackId, fieldSuggestions: raw.fieldSuggestions && typeof raw.fieldSuggestions === "object" ? raw.fieldSuggestions : fallback.fieldSuggestions, sectionSchemas: raw.sectionSchemas && typeof raw.sectionSchemas === "object" ? raw.sectionSchemas : fallback.sectionSchemas, changeLog: Array.isArray(raw.changeLog) ? raw.changeLog : [], contentProposals: Array.isArray(raw.contentProposals) ? raw.contentProposals : [], milestones: Array.isArray(raw.milestones) ? raw.milestones : [], theme: raw.theme && typeof raw.theme === "object" ? raw.theme : fallback.theme, branch: safeString(raw.branch, fallback.branch), lastUpdated: safeString(raw.lastUpdated, fallback.lastUpdated), }; } async function loadState() { if (stateLoaded) { return stateCache; } try { const content = await readFile(STATE_FILE, "utf8"); stateCache = normalizeState(JSON.parse(content)); } catch { stateCache = createInitialState(); await persistState(); } stateLoaded = true; return stateCache; } function escapeHtml(value) { return String(value) .replaceAll("&", "&") .replaceAll("<", "<") .replaceAll(">", ">") .replaceAll('"', """) .replaceAll("'", "'"); } function findSectionOrThrow(sectionId) { const section = stateCache.sections.find((item) => item.id === sectionId); if (!section) { throw new CanvasError("section_not_found", `Unknown section: ${sectionId}`); } return section; } function addChange(change) { const timestamp = safeString(change.timestamp, nowIso()); const entry = { id: uid("change"), type: change.type, path: typeof change.path === "string" ? change.path : undefined, summary: safeString(change.summary, ""), timestamp, sectionId: typeof change.sectionId === "string" ? change.sectionId : undefined, }; stateCache.changeLog.unshift(entry); stateCache.lastUpdated = timestamp; return entry; } function addNote(sectionId, text, author) { const section = findSectionOrThrow(sectionId); const note = { text: safeString(text), author: author === "human" ? "human" : "agent", timestamp: nowIso(), }; section.notes.push(note); section.lastModified = note.timestamp; stateCache.lastUpdated = note.timestamp; return note; } function updateSectionStatus(sectionId, status, message, author = "agent") { if (!VALID_STATUS.has(status)) { throw new CanvasError("invalid_status", `Unsupported status: ${status}`); } const section = findSectionOrThrow(sectionId); section.status = status; section.lastModified = nowIso(); stateCache.lastUpdated = section.lastModified; addChange({ type: "status_change", summary: `${section.name} moved to ${status}`, timestamp: section.lastModified, sectionId, }); if (message) { addNote(sectionId, message, author); } return section; } function appendFileChange(sectionId, filePath) { if (!filePath) { return; } const section = stateCache.sections.find((item) => item.id === sectionId); if (!section) { return; } if (!section.filesChanged.includes(filePath)) { section.filesChanged.push(filePath); } } function markMilestone(title, description, sectionsAffected) { const milestone = { id: uid("milestone"), title: safeString(title), description: safeString(description), sectionsAffected: Array.isArray(sectionsAffected) ? sectionsAffected.filter((value) => typeof value === "string") : [], timestamp: nowIso(), }; stateCache.milestones.unshift(milestone); addChange({ type: "milestone", summary: `${milestone.title}: ${milestone.description}`, timestamp: milestone.timestamp, }); return milestone; } const UNSAFE_FIELD_NAMES = new Set(["__proto__", "prototype", "constructor"]); function isUnsafeFieldName(name) { return UNSAFE_FIELD_NAMES.has(name); } function upsertSectionContent(sectionId, field, value, mark = "draft", author = "agent") { const section = findSectionOrThrow(sectionId); const normalizedField = safeString(field).trim(); if (!normalizedField) { throw new CanvasError("invalid_field", "Field name is required."); } if (isUnsafeFieldName(normalizedField)) { throw new CanvasError("invalid_field", `Field name "${normalizedField}" is not allowed.`); } const normalizedValue = safeString(value, ""); validateFieldValue(section.id, normalizedField, normalizedValue, mark); const current = safeString(section.content[normalizedField], ""); section.content[normalizedField] = normalizedValue; section.lastModified = nowIso(); stateCache.lastUpdated = section.lastModified; addChange({ type: "file_modified", summary: `Content updated for ${section.name}.${normalizedField} (${mark}).`, timestamp: section.lastModified, sectionId: section.id, }); if (author === "human") { addNote(sectionId, `Updated field "${normalizedField}" (${mark}).`, "human"); } return { section, field: normalizedField, current, value: normalizedValue }; } function isSamplePlaceholder(value) { return /^\[sample:/i.test(safeString(value).trim()); } function deleteSectionContent(sectionId, field, author = "agent") { const section = findSectionOrThrow(sectionId); const normalizedField = safeString(field).trim(); if (!normalizedField) { throw new CanvasError("invalid_field", "Field name is required."); } if (!section.content || !Object.prototype.hasOwnProperty.call(section.content, normalizedField)) { return { section, field: normalizedField, removed: false }; } delete section.content[normalizedField]; section.lastModified = nowIso(); stateCache.lastUpdated = section.lastModified; addChange({ type: "file_modified", summary: `Field removed from ${section.name}.${normalizedField}.`, timestamp: section.lastModified, sectionId: section.id, }); if (author === "human") { addNote(sectionId, `Removed field "${normalizedField}".`, "human"); } return { section, field: normalizedField, removed: true }; } function getSectionGuide(sectionId) { const section = findSectionOrThrow(sectionId); const templates = getFieldSuggestionsForSection(section.id); const missingFields = templates .map((item) => item.field) .filter((field) => { const value = section.content?.[field]; return !safeString(value).trim() || isSamplePlaceholder(value); }); return { sectionId: section.id, sectionName: section.name, suggestedFields: templates, fieldSchemas: stateCache.sectionSchemas?.[section.id] || {}, missingFields, nextSuggestedQuestion: templates.find((item) => missingFields.includes(item.field))?.prompt ?? null, }; } function detectSectionIdFromPrompt(prompt) { const normalized = prompt.toLowerCase(); if (normalized.includes("design system") || normalized.includes("design-system")) return "design-system"; if (normalized.includes("hero")) return "hero"; if (normalized.includes("navigation") || normalized.includes(" nav ")) return "nav"; if (normalized.includes("about")) return "about"; if (normalized.includes("talk")) return "talks"; if (normalized.includes("video")) return "videos"; if (normalized.includes("contact")) return "contact"; return null; } const INTERACTIVE_FILL_CONTEXT = [ "When users ask to fill Site Studio section content interactively:", "1) Call get_dashboard first.", "2) Call get_section_guide for the target section.", "3) Ask exactly one focused follow-up question at a time.", "4) After each answer, call upsert_section_content (or bulk) to persist draft values immediately.", "5) Reflect what was saved and ask the next guide question until the section is complete.", "6) Offer moving the section status to Review when core fields are filled.", ].join("\n"); const AI_GENERATION_CONTEXT = [ "When the user asks to AI-generate Site Studio content (or clicks the canvas 'β¨ Generate with AI' button):", "1) Call list_ai_requests to see which sections requested generation and to read existing board content as grounding context.", "2) For each requested section, draft each suggested/missing field GROUNDED in information already provided in other sections β do not invent unrelated facts; if there is too little context, ask one clarifying question first.", "3) Persist drafts with upsert_section_content (mark='draft'), or use propose_content when the change should be human-reviewed before applying.", "4) Call resolve_ai_request for each section when done so the canvas clears its request badge.", "5) Briefly summarize what you generated and invite the user to review or move the section to Review.", ].join("\n"); function listDiffLines(before, after) { const oldLines = String(before ?? "").split("\n"); const newLines = String(after ?? "").split("\n"); const max = Math.max(oldLines.length, newLines.length); const result = []; for (let i = 0; i < max; i += 1) { const oldLine = oldLines[i] ?? ""; const newLine = newLines[i] ?? ""; if (oldLine !== newLine) { if (oldLine) { result.push(`- ${oldLine}`); } if (newLine) { result.push(`+ ${newLine}`); } } } return result.join("\n"); } function sendJson(res, code, payload) { res.statusCode = code; res.setHeader("Content-Type", "application/json; charset=utf-8"); res.end(JSON.stringify(payload)); } const MAX_BODY_BYTES = 1024 * 1024; async function readBodyJson(req) { const chunks = []; let received = 0; for await (const chunk of req) { received += chunk.length; if (received > MAX_BODY_BYTES) { req.destroy(); throw new CanvasError("payload_too_large", "Request body exceeds the maximum allowed size."); } chunks.push(chunk); } if (!chunks.length) { return {}; } const body = Buffer.concat(chunks).toString("utf8"); if (!body.trim()) { return {}; } try { return JSON.parse(body); } catch { throw new CanvasError("invalid_json", "Request body must be valid JSON."); } } function publishSse(event, payload) { const serialized = `event: ${event}\ndata: ${JSON.stringify(payload)}\n\n`; for (const entry of servers.values()) { for (const client of entry.clients) { try { client.write(serialized); } catch { entry.clients.delete(client); } } } } async function mutateState(mutator) { await loadState(); const result = await mutator(stateCache); await persistState(); publishSse("state_update", stateCache); return result; } function renderHtml(instanceId) { return `