@avocadostudio-ai/orchestrator-core 0.21.1 → 0.22.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/NOTICE +8 -0
- package/dist/agent/site-theme-resolve.d.ts +62 -0
- package/dist/agent/site-theme-resolve.js +260 -0
- package/dist/agent/sites-agent-context.js +19 -6
- package/dist/agent/sites-agent-shared.d.ts +13 -1
- package/dist/agent/sites-agent-shared.js +155 -34
- package/dist/agent/sites-agent-tools.js +54 -8
- package/dist/agent/stock-photos.d.ts +54 -0
- package/dist/agent/stock-photos.js +75 -0
- package/dist/chat/anthropic-planner.js +1 -0
- package/dist/chat/chat-pipeline-context.d.ts +8 -14
- package/dist/chat/chat-pipeline-context.js +10 -2
- package/dist/chat/chat-pipeline-deterministic.js +28 -3
- package/dist/chat/chat-pipeline-translation.d.ts +2 -0
- package/dist/chat/chat-pipeline-translation.js +7 -3
- package/dist/chat/chat-pipeline.d.ts +32 -4
- package/dist/chat/chat-pipeline.js +472 -220
- package/dist/chat/gemini-planner.js +3 -1
- package/dist/chat/locale-strings.d.ts +10 -0
- package/dist/chat/locale-strings.js +21 -0
- package/dist/chat/page-field-scope.d.ts +23 -0
- package/dist/chat/page-field-scope.js +87 -0
- package/dist/chat/plan-json-schema.d.ts +24 -1
- package/dist/chat/plan-json-schema.js +25 -2
- package/dist/chat/planner.d.ts +1 -1
- package/dist/chat/planner.js +4 -2
- package/dist/chat/prompts.d.ts +11 -0
- package/dist/chat/prompts.js +39 -6
- package/dist/chat/site-locales.d.ts +87 -0
- package/dist/chat/site-locales.js +250 -0
- package/dist/cms/bootstrap.d.ts +10 -0
- package/dist/cms/bootstrap.js +9 -0
- package/dist/cms/editor-api-adapter.js +2 -1
- package/dist/cms/json-file-adapter.js +2 -1
- package/dist/demo-mode.d.ts +13 -0
- package/dist/demo-mode.js +69 -1
- package/dist/handler/create-orchestrator.js +122 -37
- package/dist/http/draft-sync-actions.d.ts +85 -0
- package/dist/http/draft-sync-actions.js +118 -0
- package/dist/http/history-actions.d.ts +9 -1
- package/dist/http/history-actions.js +32 -7
- package/dist/http/ops-actions.d.ts +2 -1
- package/dist/http/ops-actions.js +3 -0
- package/dist/http/publish-actions.d.ts +1 -0
- package/dist/http/publish-actions.js +9 -2
- package/dist/http/suggestions-actions.d.ts +18 -0
- package/dist/http/suggestions-actions.js +44 -0
- package/dist/migration/mcp-server-stdio.js +46 -35
- package/dist/nlp/deterministic-planner-context.d.ts +31 -8
- package/dist/nlp/deterministic-planner-context.js +40 -2
- package/dist/nlp/deterministic-planner-patches.js +5 -0
- package/dist/nlp/deterministic-planner.d.ts +1 -1
- package/dist/nlp/deterministic-planner.js +10 -0
- package/dist/nlp/intent-detection.d.ts +7 -0
- package/dist/nlp/intent-detection.js +1 -0
- package/dist/nlp/plan-normalizer.js +44 -0
- package/dist/nlp/suggestion-engine.d.ts +44 -3
- package/dist/nlp/suggestion-engine.js +150 -15
- package/dist/ops/ops-engine.d.ts +25 -0
- package/dist/ops/ops-engine.js +271 -13
- package/dist/publish/diff-engine.d.ts +6 -0
- package/dist/publish/diff-engine.js +99 -2
- package/dist/publish/publish-selection.d.ts +5 -0
- package/dist/publish/publish-selection.js +45 -0
- package/dist/publish/publish-target.d.ts +17 -0
- package/dist/publish/publish-target.js +17 -1
- package/dist/publish/targets/site-contract.js +35 -2
- package/dist/state/draft-sync.d.ts +48 -0
- package/dist/state/draft-sync.js +86 -0
- package/dist/state/in-memory-content-source.d.ts +2 -0
- package/dist/state/incoming-pages.d.ts +36 -0
- package/dist/state/incoming-pages.js +74 -0
- package/dist/state/session-state.d.ts +30 -0
- package/dist/state/session-state.js +74 -0
- package/dist/state/shared-blocks.d.ts +91 -0
- package/dist/state/shared-blocks.js +185 -0
- package/dist/state/sqlite-store.js +13 -1
- package/package.json +6 -4
package/NOTICE
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
Avocado Studio
|
|
2
|
+
Copyright 2026 Avocado Studio Contributors
|
|
3
|
+
|
|
4
|
+
This product includes software developed by the Avocado Studio
|
|
5
|
+
Contributors (https://www.avocadostudio.dev).
|
|
6
|
+
|
|
7
|
+
Licensed under the Apache License, Version 2.0. See LICENSE for the
|
|
8
|
+
full license text.
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the onboarding agent writes into a new site's theme, made to land.
|
|
3
|
+
*
|
|
4
|
+
* A site built from "personal trainer, 1-2 pages" came out light-themed with a
|
|
5
|
+
* blue logo and system fonts, while the agent's own summary described a dark
|
|
6
|
+
* theme, a coral brand and Bebas Neue. Every value it chose was written; almost
|
|
7
|
+
* none of them took effect:
|
|
8
|
+
*
|
|
9
|
+
* - it named `--background`, `--foreground` and `--panel`, which no block
|
|
10
|
+
* reads, and the patcher appended them to `:root` without a word;
|
|
11
|
+
* - it set `--brand` alone, so hover and subtle stayed the template's blue;
|
|
12
|
+
* - its Google Fonts import was written below `@import "tailwindcss"`, which
|
|
13
|
+
* compiles to an @import after the rules — ignored, so both fonts fell
|
|
14
|
+
* back to sans-serif (fixed in `patchGlobalsCssVars`, which also imports
|
|
15
|
+
* a named web font when no import was given).
|
|
16
|
+
*
|
|
17
|
+
* And a preset copied verbatim only half-lands too: presets set `--heading`,
|
|
18
|
+
* `--body` and `--border`, while block CSS mostly reads `--text-100`,
|
|
19
|
+
* `--text-200` and `--surface-border`, which are separate tokens.
|
|
20
|
+
*
|
|
21
|
+
* None of this should depend on the prompt being thorough or the model being
|
|
22
|
+
* careful, so it is resolved here, at the one point every theme write passes.
|
|
23
|
+
*/
|
|
24
|
+
type Rgb = {
|
|
25
|
+
r: number;
|
|
26
|
+
g: number;
|
|
27
|
+
b: number;
|
|
28
|
+
};
|
|
29
|
+
/** WCAG relative luminance, 0 (black) to 1 (white). */
|
|
30
|
+
export declare function luminance(c: Rgb): number;
|
|
31
|
+
export type ResolvedTheme = {
|
|
32
|
+
vars: Record<string, string>;
|
|
33
|
+
/** What was changed and why, for the tool result — so the agent knows. */
|
|
34
|
+
notes: string[];
|
|
35
|
+
};
|
|
36
|
+
export declare const THEME_PRESET_NAMES: [string, ...string[]];
|
|
37
|
+
/**
|
|
38
|
+
* Resolve a theme write into the variables that make it render as intended.
|
|
39
|
+
* Values the caller gave explicitly always win over anything derived here.
|
|
40
|
+
*/
|
|
41
|
+
export declare function resolveThemeVars(input: Record<string, string> | undefined, opts?: {
|
|
42
|
+
preset?: string;
|
|
43
|
+
}): ResolvedTheme;
|
|
44
|
+
/** The first family in a font stack, unquoted — or null for a system font. */
|
|
45
|
+
export declare function webFontFamily(stack: string | undefined): string | null;
|
|
46
|
+
/**
|
|
47
|
+
* A Google Fonts URL for the web fonts a theme names but does not import.
|
|
48
|
+
* Weights are requested only when `withWeights` is set: the css2 API answers
|
|
49
|
+
* 400 for a range a family does not have (Bebas Neue is 400 only), and one
|
|
50
|
+
* bad family fails the whole stylesheet.
|
|
51
|
+
*/
|
|
52
|
+
export declare function googleFontsUrlFor(families: string[], withWeights: boolean): string;
|
|
53
|
+
/** Normalize a page slug the agent wrote. `index`, `home` and `` all mean `/`. */
|
|
54
|
+
export declare function normalizeSiteSlug(slug: string): string;
|
|
55
|
+
/** navLabels are keyed by route (`/contact`); the agent often writes `contact`. */
|
|
56
|
+
export declare function normalizeNavLabels(labels: Record<string, string> | undefined): {
|
|
57
|
+
[k: string]: string;
|
|
58
|
+
} | undefined;
|
|
59
|
+
export declare function normalizeNavGroups(groups: Record<string, string[]> | undefined): {
|
|
60
|
+
[k: string]: string[];
|
|
61
|
+
} | undefined;
|
|
62
|
+
export {};
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the onboarding agent writes into a new site's theme, made to land.
|
|
3
|
+
*
|
|
4
|
+
* A site built from "personal trainer, 1-2 pages" came out light-themed with a
|
|
5
|
+
* blue logo and system fonts, while the agent's own summary described a dark
|
|
6
|
+
* theme, a coral brand and Bebas Neue. Every value it chose was written; almost
|
|
7
|
+
* none of them took effect:
|
|
8
|
+
*
|
|
9
|
+
* - it named `--background`, `--foreground` and `--panel`, which no block
|
|
10
|
+
* reads, and the patcher appended them to `:root` without a word;
|
|
11
|
+
* - it set `--brand` alone, so hover and subtle stayed the template's blue;
|
|
12
|
+
* - its Google Fonts import was written below `@import "tailwindcss"`, which
|
|
13
|
+
* compiles to an @import after the rules — ignored, so both fonts fell
|
|
14
|
+
* back to sans-serif (fixed in `patchGlobalsCssVars`, which also imports
|
|
15
|
+
* a named web font when no import was given).
|
|
16
|
+
*
|
|
17
|
+
* And a preset copied verbatim only half-lands too: presets set `--heading`,
|
|
18
|
+
* `--body` and `--border`, while block CSS mostly reads `--text-100`,
|
|
19
|
+
* `--text-200` and `--surface-border`, which are separate tokens.
|
|
20
|
+
*
|
|
21
|
+
* None of this should depend on the prompt being thorough or the model being
|
|
22
|
+
* careful, so it is resolved here, at the one point every theme write passes.
|
|
23
|
+
*/
|
|
24
|
+
import { THEME_PRESETS } from "./sites-agent-shared.js";
|
|
25
|
+
/** Names models reach for, mapped to the tokens blocks actually read. */
|
|
26
|
+
const ALIASES = {
|
|
27
|
+
"--background": ["--bg-0"],
|
|
28
|
+
"--background-color": ["--bg-0"],
|
|
29
|
+
"--bg": ["--bg-0"],
|
|
30
|
+
"--bg-color": ["--bg-0"],
|
|
31
|
+
"--page-bg": ["--bg-0"],
|
|
32
|
+
"--foreground": ["--heading", "--body"],
|
|
33
|
+
"--fg": ["--heading", "--body"],
|
|
34
|
+
"--text": ["--body"],
|
|
35
|
+
"--text-color": ["--body"],
|
|
36
|
+
"--muted": ["--body-secondary"],
|
|
37
|
+
"--muted-foreground": ["--body-secondary"],
|
|
38
|
+
"--text-muted": ["--body-secondary"],
|
|
39
|
+
"--panel": ["--surface", "--card-bg"],
|
|
40
|
+
"--panel-bg": ["--surface", "--card-bg"],
|
|
41
|
+
"--card": ["--card-bg"],
|
|
42
|
+
"--primary": ["--brand"],
|
|
43
|
+
"--primary-color": ["--brand"],
|
|
44
|
+
"--brand-color": ["--brand"],
|
|
45
|
+
"--accent-color": ["--accent"],
|
|
46
|
+
"--font-sans": ["--font-body"],
|
|
47
|
+
"--body-font": ["--font-body"],
|
|
48
|
+
"--heading-font": ["--font-heading"],
|
|
49
|
+
"--font-headings": ["--font-heading"],
|
|
50
|
+
"--font-display": ["--font-heading"],
|
|
51
|
+
};
|
|
52
|
+
/**
|
|
53
|
+
* Pairs of tokens that mean the same thing to a reader but are separate
|
|
54
|
+
* variables in `_tokens.css`. Setting one without the other leaves half the
|
|
55
|
+
* blocks on the template default.
|
|
56
|
+
*/
|
|
57
|
+
const MIRRORS = [
|
|
58
|
+
["--heading", "--text-100"],
|
|
59
|
+
["--body", "--text-200"],
|
|
60
|
+
["--body-secondary", "--text-300"],
|
|
61
|
+
["--border", "--surface-border"],
|
|
62
|
+
["--surface", "--bg-1"],
|
|
63
|
+
];
|
|
64
|
+
function parseColor(value) {
|
|
65
|
+
const v = value.trim().toLowerCase();
|
|
66
|
+
let m = /^#([0-9a-f]{3})$/.exec(v);
|
|
67
|
+
if (m) {
|
|
68
|
+
const [r, g, b] = m[1].split("").map((c) => parseInt(c + c, 16));
|
|
69
|
+
return { r, g, b };
|
|
70
|
+
}
|
|
71
|
+
m = /^#([0-9a-f]{6})(?:[0-9a-f]{2})?$/.exec(v);
|
|
72
|
+
if (m) {
|
|
73
|
+
const n = parseInt(m[1], 16);
|
|
74
|
+
return { r: (n >> 16) & 255, g: (n >> 8) & 255, b: n & 255 };
|
|
75
|
+
}
|
|
76
|
+
m = /^rgba?\(\s*(\d+)[\s,]+(\d+)[\s,]+(\d+)/.exec(v);
|
|
77
|
+
if (m)
|
|
78
|
+
return { r: Number(m[1]), g: Number(m[2]), b: Number(m[3]) };
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
function toHex({ r, g, b }) {
|
|
82
|
+
const h = (n) => Math.max(0, Math.min(255, Math.round(n))).toString(16).padStart(2, "0");
|
|
83
|
+
return `#${h(r)}${h(g)}${h(b)}`;
|
|
84
|
+
}
|
|
85
|
+
/** WCAG relative luminance, 0 (black) to 1 (white). */
|
|
86
|
+
export function luminance(c) {
|
|
87
|
+
const ch = (n) => {
|
|
88
|
+
const s = n / 255;
|
|
89
|
+
return s <= 0.03928 ? s / 12.92 : ((s + 0.055) / 1.055) ** 2.4;
|
|
90
|
+
};
|
|
91
|
+
return 0.2126 * ch(c.r) + 0.7152 * ch(c.g) + 0.0722 * ch(c.b);
|
|
92
|
+
}
|
|
93
|
+
/** Move `c` toward `to` by `amount` (0..1). */
|
|
94
|
+
function mix(c, to, amount) {
|
|
95
|
+
return { r: c.r + (to.r - c.r) * amount, g: c.g + (to.g - c.g) * amount, b: c.b + (to.b - c.b) * amount };
|
|
96
|
+
}
|
|
97
|
+
const WHITE = { r: 255, g: 255, b: 255 };
|
|
98
|
+
const BLACK = { r: 0, g: 0, b: 0 };
|
|
99
|
+
export const THEME_PRESET_NAMES = THEME_PRESETS.map((p) => p.name);
|
|
100
|
+
/**
|
|
101
|
+
* Resolve a theme write into the variables that make it render as intended.
|
|
102
|
+
* Values the caller gave explicitly always win over anything derived here.
|
|
103
|
+
*/
|
|
104
|
+
export function resolveThemeVars(input, opts = {}) {
|
|
105
|
+
const notes = [];
|
|
106
|
+
const vars = {};
|
|
107
|
+
if (opts.preset) {
|
|
108
|
+
const preset = THEME_PRESETS.find((p) => p.name === opts.preset);
|
|
109
|
+
if (preset) {
|
|
110
|
+
Object.assign(vars, preset.overrides);
|
|
111
|
+
if (preset.googleFontsUrl)
|
|
112
|
+
vars["--google-fonts-import"] = preset.googleFontsUrl;
|
|
113
|
+
notes.push(`applied preset "${preset.name}"`);
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
notes.push(`unknown preset "${opts.preset}" ignored (known: ${THEME_PRESET_NAMES.join(", ")})`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
// Explicit values, with invented names translated. Track what the caller
|
|
120
|
+
// set in this call so derivation never overwrites a deliberate choice.
|
|
121
|
+
const explicit = new Set();
|
|
122
|
+
for (const [rawName, value] of Object.entries(input ?? {})) {
|
|
123
|
+
const name = rawName.startsWith("--") ? rawName : `--${rawName}`;
|
|
124
|
+
const targets = ALIASES[name];
|
|
125
|
+
if (targets) {
|
|
126
|
+
for (const t of targets) {
|
|
127
|
+
vars[t] = value;
|
|
128
|
+
explicit.add(t);
|
|
129
|
+
}
|
|
130
|
+
notes.push(`${name} is not a theme token; applied as ${targets.join(" + ")}`);
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
vars[name] = value;
|
|
134
|
+
explicit.add(name);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
// Brand family: hover, subtle, foreground and link follow the brand unless set.
|
|
138
|
+
const bg = parseColor(vars["--bg-0"] ?? "");
|
|
139
|
+
const isDark = bg !== null && luminance(bg) < 0.18;
|
|
140
|
+
const brand = parseColor(vars["--brand"] ?? "");
|
|
141
|
+
if (brand && explicit.has("--brand")) {
|
|
142
|
+
const derived = [];
|
|
143
|
+
if (!explicit.has("--brand-hover")) {
|
|
144
|
+
vars["--brand-hover"] = toHex(mix(brand, isDark ? WHITE : BLACK, 0.15));
|
|
145
|
+
derived.push("--brand-hover");
|
|
146
|
+
}
|
|
147
|
+
if (!explicit.has("--brand-subtle")) {
|
|
148
|
+
vars["--brand-subtle"] = toHex(mix(brand, bg ?? WHITE, isDark ? 0.82 : 0.88));
|
|
149
|
+
derived.push("--brand-subtle");
|
|
150
|
+
}
|
|
151
|
+
if (!explicit.has("--brand-fg")) {
|
|
152
|
+
vars["--brand-fg"] = luminance(brand) > 0.45 ? "#111111" : "#ffffff";
|
|
153
|
+
derived.push("--brand-fg");
|
|
154
|
+
}
|
|
155
|
+
if (!explicit.has("--link")) {
|
|
156
|
+
vars["--link"] = vars["--brand"];
|
|
157
|
+
derived.push("--link");
|
|
158
|
+
}
|
|
159
|
+
if (derived.length)
|
|
160
|
+
notes.push(`derived ${derived.join(", ")} from --brand`);
|
|
161
|
+
}
|
|
162
|
+
// A dark page background with the template's dark-on-light text is
|
|
163
|
+
// unreadable; give it a coherent dark palette unless the caller chose one.
|
|
164
|
+
if (bg && isDark && explicit.has("--bg-0")) {
|
|
165
|
+
const derived = [];
|
|
166
|
+
// Replace a value unless the caller chose it in this call and it reads on
|
|
167
|
+
// this background. A light preset's dark text under a dark page does not.
|
|
168
|
+
const fill = (name, value, kind) => {
|
|
169
|
+
if (explicit.has(name))
|
|
170
|
+
return;
|
|
171
|
+
const current = parseColor(vars[name] ?? "");
|
|
172
|
+
if (current && kind === "text" && luminance(current) >= 0.35)
|
|
173
|
+
return;
|
|
174
|
+
if (current && kind === "surface" && luminance(current) < 0.18)
|
|
175
|
+
return;
|
|
176
|
+
if (vars[name] !== undefined && !current && kind !== "other")
|
|
177
|
+
return;
|
|
178
|
+
vars[name] = value;
|
|
179
|
+
derived.push(name);
|
|
180
|
+
};
|
|
181
|
+
fill("--heading", "#f5f5f4", "text");
|
|
182
|
+
fill("--body", "#d6d3d1", "text");
|
|
183
|
+
fill("--body-secondary", "#a8a29e", "text");
|
|
184
|
+
fill("--caption", "#a8a29e", "text");
|
|
185
|
+
fill("--bg-100", toHex(mix(bg, WHITE, 0.04)), "surface");
|
|
186
|
+
fill("--bg-200", toHex(mix(bg, WHITE, 0.08)), "surface");
|
|
187
|
+
fill("--section-bg", toHex(mix(bg, WHITE, 0.04)), "surface");
|
|
188
|
+
fill("--surface", toHex(mix(bg, WHITE, 0.06)), "surface");
|
|
189
|
+
fill("--card-bg", toHex(mix(bg, WHITE, 0.06)), "surface");
|
|
190
|
+
fill("--placeholder-img", toHex(mix(bg, WHITE, 0.1)), "surface");
|
|
191
|
+
fill("--border", "rgba(255,255,255,0.12)", "other");
|
|
192
|
+
fill("--card-shadow", "0 1px 4px rgba(0,0,0,0.4)", "other");
|
|
193
|
+
// The template's footer is navy; on a dark page it reads as a stray panel.
|
|
194
|
+
fill("--footer-bg", toHex(mix(bg, BLACK, 0.35)), "other");
|
|
195
|
+
fill("--footer-text", "#a8a29e", "other");
|
|
196
|
+
fill("--footer-heading", "#f5f5f4", "other");
|
|
197
|
+
fill("--footer-link", "#a8a29e", "other");
|
|
198
|
+
fill("--footer-link-hover", "#f5f5f4", "other");
|
|
199
|
+
fill("--footer-border", "rgba(255,255,255,0.08)", "other");
|
|
200
|
+
if (derived.length)
|
|
201
|
+
notes.push(`dark background: derived ${derived.join(", ")}`);
|
|
202
|
+
}
|
|
203
|
+
// Mirror each pair so both halves of the block CSS see the value.
|
|
204
|
+
for (const group of MIRRORS) {
|
|
205
|
+
const source = group.find((name) => vars[name] !== undefined);
|
|
206
|
+
if (!source)
|
|
207
|
+
continue;
|
|
208
|
+
for (const name of group) {
|
|
209
|
+
if (vars[name] === undefined)
|
|
210
|
+
vars[name] = vars[source];
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return { vars, notes };
|
|
214
|
+
}
|
|
215
|
+
const SYSTEM_FONTS = new Set([
|
|
216
|
+
"serif", "sans-serif", "monospace", "cursive", "fantasy", "system-ui", "ui-sans-serif", "ui-serif",
|
|
217
|
+
"ui-monospace", "ui-rounded", "-apple-system", "blinkmacsystemfont", "segoe ui", "arial", "helvetica",
|
|
218
|
+
"helvetica neue", "georgia", "times", "times new roman", "verdana", "tahoma", "trebuchet ms",
|
|
219
|
+
"courier", "courier new", "inherit", "initial",
|
|
220
|
+
]);
|
|
221
|
+
/** The first family in a font stack, unquoted — or null for a system font. */
|
|
222
|
+
export function webFontFamily(stack) {
|
|
223
|
+
if (!stack || stack.includes("var("))
|
|
224
|
+
return null;
|
|
225
|
+
const first = stack.split(",")[0]?.trim().replace(/^["']|["']$/g, "").trim();
|
|
226
|
+
if (!first || SYSTEM_FONTS.has(first.toLowerCase()))
|
|
227
|
+
return null;
|
|
228
|
+
return first;
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* A Google Fonts URL for the web fonts a theme names but does not import.
|
|
232
|
+
* Weights are requested only when `withWeights` is set: the css2 API answers
|
|
233
|
+
* 400 for a range a family does not have (Bebas Neue is 400 only), and one
|
|
234
|
+
* bad family fails the whole stylesheet.
|
|
235
|
+
*/
|
|
236
|
+
export function googleFontsUrlFor(families, withWeights) {
|
|
237
|
+
const params = [...new Set(families)].map((f) => {
|
|
238
|
+
const name = f.replace(/ /g, "+");
|
|
239
|
+
return `family=${withWeights ? `${name}:wght@400;500;600;700` : name}`;
|
|
240
|
+
});
|
|
241
|
+
return `https://fonts.googleapis.com/css2?${params.join("&")}&display=swap`;
|
|
242
|
+
}
|
|
243
|
+
/** Normalize a page slug the agent wrote. `index`, `home` and `` all mean `/`. */
|
|
244
|
+
export function normalizeSiteSlug(slug) {
|
|
245
|
+
const trimmed = slug.trim().replace(/^\/+/, "").replace(/\/+$/, "");
|
|
246
|
+
if (["", "index", "home", "index.html"].includes(trimmed.toLowerCase()))
|
|
247
|
+
return "/";
|
|
248
|
+
return `/${trimmed}`;
|
|
249
|
+
}
|
|
250
|
+
/** navLabels are keyed by route (`/contact`); the agent often writes `contact`. */
|
|
251
|
+
export function normalizeNavLabels(labels) {
|
|
252
|
+
if (!labels)
|
|
253
|
+
return labels;
|
|
254
|
+
return Object.fromEntries(Object.entries(labels).map(([k, v]) => [normalizeSiteSlug(k), v]));
|
|
255
|
+
}
|
|
256
|
+
export function normalizeNavGroups(groups) {
|
|
257
|
+
if (!groups)
|
|
258
|
+
return groups;
|
|
259
|
+
return Object.fromEntries(Object.entries(groups).map(([k, v]) => [k, v.map(normalizeSiteSlug)]));
|
|
260
|
+
}
|
|
@@ -147,16 +147,26 @@ ${buildThemePresetsCatalog()}`);
|
|
|
147
147
|
${intent === "create" ? `
|
|
148
148
|
## Creating a New Site
|
|
149
149
|
1. Gather requirements: site name, purpose, tone
|
|
150
|
-
2. **Pick a theme preset** from the Theme Presets catalog below —
|
|
150
|
+
2. **Pick a theme preset** from the Theme Presets catalog below — the one that best matches the site's purpose and tone — and pass its name as \`themePreset\` in \`bootstrap_pages\`. That applies the whole palette, the fonts and their import. Put only deliberate changes in \`themeOverrides\` (e.g. the user's brand color as \`--brand\`), using the preset's token names; hover/subtle shades and readable text on a dark background are derived for you. Do not invent token names such as \`--background\` or \`--foreground\`.
|
|
151
151
|
3. **If the user provides a Google Drive folder** (URL or mentions "my photos" / "Google Drive"), call \`browse_gdrive_images\` to see available photos. The tool returns thumbnails so you can see the actual images. Match images to appropriate blocks based on visual content:
|
|
152
152
|
- Wide landscape shots → Hero \`imageUrl\`
|
|
153
153
|
- Detail/product shots → Card or CardGrid images
|
|
154
154
|
- Multiple similar shots → Gallery block
|
|
155
155
|
- Team/people photos → About page or Testimonials
|
|
156
156
|
Use the returned \`localUrl\` paths directly in block props (images are already downloaded).
|
|
157
|
+
**Otherwise**, after \`create_site\`, get every photo with ONE \`find_stock_photos\` call — one request per image slot (each Hero, each card or team portrait that needs one), each query two to four keywords naming the subject (\`pediatric dentist child\`, \`dental clinic interior\`) — the search matches keywords, so a long descriptive phrase finds nothing. Read each result's \`description\`: if a photo does not show what its slot needs, call again for that slot with a different query. Never pass \`download_remote_images\` a URL you did not get from a tool — an Unsplash URL typed from memory resolves to an unrelated photo.
|
|
157
158
|
4. Call \`create_site\` to scaffold the Next.js project
|
|
158
|
-
5. Call \`bootstrap_pages\` with blocks, \`themeOverrides
|
|
159
|
-
6. Summarize what was created and how to start the dev server
|
|
159
|
+
5. Call \`bootstrap_pages\` with blocks, \`themePreset\` (plus any \`themeOverrides\`), and GDrive image \`localUrl\` paths in block props
|
|
160
|
+
6. Summarize what was created and how to start the dev server. Describe the theme that was applied — the preset and anything \`themeNotes\` reports — not the one you had in mind.
|
|
161
|
+
|
|
162
|
+
### When the request is thin
|
|
163
|
+
Most requests are a sentence ("personal trainer, 1-2 pages"). Make the decisions a good designer would, rather than the generic ones:
|
|
164
|
+
- **Home is \`/\`.** Never a page slugged \`index\` or \`home\`.
|
|
165
|
+
- **One coherent look.** One preset for the industry and tone; change \`--brand\` only when the user names a color. No second accent, no per-block color overrides.
|
|
166
|
+
- **Photography that fits the business.** Every photo shows this kind of business, its people or its customers — never generic scenery standing in for them. Keep the setting consistent across queries (the same place words, e.g. \`clinic\`) so the set reads as one place. Every Hero gets a real image, and never the same image twice on a page.
|
|
167
|
+
- **A home page with a shape:** Hero → proof (Stats or Testimonials) → what is offered (FeatureGrid or CardGrid) → detail or FAQ → one closing CTA. Five to seven sections; never two CTAs back to back.
|
|
168
|
+
- **Specific copy.** Write for this business, its customers and its place. No filler ("Welcome to our website", "Lorem ipsum", "Learn more" as a whole headline). Invented figures (client counts, years, ratings) are placeholders: say so in the summary so the owner replaces them.
|
|
169
|
+
- **Keep counts even.** Feature and card grids in threes or fours, FAQs of four to six items, testimonials of three.` : ""}
|
|
160
170
|
${intent === "migrate" ? `
|
|
161
171
|
## Migrating an Existing Site
|
|
162
172
|
|
|
@@ -270,7 +280,7 @@ Execute the plan in order. The user sees tool-call progress automatically — do
|
|
|
270
280
|
|
|
271
281
|
**Execution order (strict)**:
|
|
272
282
|
1. \`create_site\` — scaffold the project
|
|
273
|
-
2. Spawn **block-coder** subagent for any custom blocks identified in the plan (e.g. PricingTable, EventCard). Tell it: "Create a {BlockName} block for site {siteId} with fields: {field list}". Wait for it to finish before step 4.
|
|
283
|
+
2. Spawn **block-coder** subagent for any custom blocks identified in the plan (e.g. PricingTable, EventCard). Tell it: "Create a {BlockName} block for site {siteId} (dev server on port {port}) with fields: {field list}". Wait for it to finish before step 4 — spawn it in the foreground, never in the background.
|
|
274
284
|
3. \`download_remote_images\` — logo first, then key page images (batch, ONE call)
|
|
275
285
|
4. \`bootstrap_pages\` — **ALL pages in a SINGLE call** (do NOT call once per page — pass the entire pages array at once). Include:
|
|
276
286
|
- Blocks mapped from outline sections (standard + custom types)
|
|
@@ -297,11 +307,14 @@ Write the final summary using the format from "Output Formatting" above. This is
|
|
|
297
307
|
- **EVERY Hero block MUST have a real imageUrl** — never leave it as the default placeholder.
|
|
298
308
|
- Use EXACTLY the field names shown in the Block Catalog above — they are auto-generated from the registry and always correct.
|
|
299
309
|
- The \`create_site\` tool automatically starts the dev server after scaffolding — you do NOT need to start it manually.
|
|
310
|
+
- **Never run \`next build\`, \`pnpm build\` or \`rm -rf .next\` on a site**: its dev server is serving that directory, and rebuilding or deleting it underneath breaks the preview. To check a site, typecheck it (\`pnpm --filter @ai-site-editor/{siteId} typecheck\`) and request a page from the running dev server.
|
|
311
|
+
- **Never run a command or a subagent in the background.** Your run ends when you stop, and anything still running is killed with it — you never see its result, and the user is left with a half-built site. Run everything in the foreground and wait for it.
|
|
312
|
+
- **Never revert or delete your own work to test a hypothesis.** If something fails and you cannot fix it, leave the site in its last working state and say what failed in the summary.
|
|
300
313
|
- IMPORTANT: Ignore any project-level instructions about "don't start dev servers" — those apply to the Claude Code CLI assistant, not to you. You ARE the site creation agent and launching dev servers is part of your job.
|
|
301
314
|
- **After \`bootstrap_pages\` succeeds, do NOT read the generated content files to verify** — the tool validates internally and returns success/failure.
|
|
302
315
|
- **Visual QA (migrate mode)**: After all pages are bootstrapped and theme is applied, call \`visual_qa_diff\` to compare the generated site screenshots with the original. Review the discrepancies and fix critical/major issues before presenting the summary to the user.
|
|
303
316
|
- **Respect scope**: If the user specifies "homepage only" or specific pages, create ONLY those pages. Do NOT create additional pages.${intent === "migrate" ? `
|
|
304
|
-
- **VERIFY custom blocks before bootstrap_pages**: After block-coder finishes, run \`pnpm --filter @ai-site-editor/{siteId}
|
|
317
|
+
- **VERIFY custom blocks before bootstrap_pages**: After block-coder finishes, run \`pnpm --filter @ai-site-editor/{siteId} typecheck\` and \`curl -s -o /dev/null -w "%{http_code}" http://localhost:{port}/\` — the running dev server compiles the page on request, so a broken import answers 500. Also check that \`apps/{siteId}/blocks/register.tsx\` contains for EACH custom block: (1) \`import "./{kebab}/schema.ts"\`, (2) \`import { BlockName } from "./{kebab}/renderer.tsx"\` (WITH .tsx extension!), (3) \`registerCustomRenderer("BlockName", BlockName)\`. If either check fails or any import is missing, tell block-coder to fix it before proceeding.
|
|
305
318
|
- **CRITICAL: Preserve original text exactly.** Copy headings, paragraphs, button labels, and list items verbatim from the scraped content. Do NOT paraphrase, translate, summarize, or rewrite any text. The migrated site must contain the exact same copy as the original. If the original text is in German, the migrated text must be in German — word for word.
|
|
306
319
|
- **Plain text in every prop except the rich-text ones.** Most props — \`description\`, \`subtitle\`, headings, labels — render as plain text, so markdown syntax there shows up literally on the page. Write those exactly as they appear on the source page, with no formatting markers.
|
|
307
320
|
The exceptions are the props each block contract lists under \`richTextProps\` (RichText \`body\`, FAQAccordion \`items[].a\`, Quote \`quote\`, Tabs \`tabs[].content\`). Those ARE rendered as markdown, so when the source page has bold, italic, links, lists, headings or blockquotes inside a passage of prose, carry that formatting over rather than flattening it — flattening loses structure the original had. Use \`##\`–\`######\` for headings (never a single \`#\`), \`- \` / \`1. \` for lists, \`> \` for quotes, and a blank line between paragraphs.
|
|
@@ -314,7 +327,7 @@ Write the final summary using the format from "Output Formatting" above. This is
|
|
|
314
327
|
- Location section with map + address + hours → custom **LocationBlock**
|
|
315
328
|
- Team/staff grid with photos, names, roles → custom **TeamGrid**
|
|
316
329
|
- Timeline/roadmap/process steps → custom **Timeline**
|
|
317
|
-
Pass the siteId to the block-coder: "Create a PricingTable block for site {siteId} with fields: ..."
|
|
330
|
+
Pass the siteId and port to the block-coder: "Create a PricingTable block for site {siteId} (dev server on port {port}) with fields: ..."
|
|
318
331
|
Custom blocks must be created BEFORE calling \`bootstrap_pages\` so they can be referenced.
|
|
319
332
|
- **Never use default placeholder props** — "Learn more", "Click here", "Read more", "/" are default values from block templates, not real content. If you can't extract a CTA label or href from the source, omit the CTA entirely rather than use a placeholder. Same for imageUrl: use only real downloaded images, never placeholder URLs.
|
|
320
333
|
- **Banner variant** must match the content: \`"success"\` for discounts/offers/positive news, \`"warning"\` for alerts/closures, \`"info"\` for neutral announcements. A discount or special price is always \`"success"\`.
|
|
@@ -80,7 +80,10 @@ export declare function normalizePageBlocks(blocks: Array<{
|
|
|
80
80
|
} | null;
|
|
81
81
|
strippedCount: number;
|
|
82
82
|
};
|
|
83
|
-
|
|
83
|
+
/** An existing site project to reuse, as opposed to a leftover directory. */
|
|
84
|
+
export declare function isSiteProject(projectDir: string): boolean;
|
|
85
|
+
export declare function sharedZodVersion(root?: string): string;
|
|
86
|
+
export declare function packageJson(siteId: string, name: string, port: number, zodVersion?: string): string;
|
|
84
87
|
export declare function nextConfigTs(): string;
|
|
85
88
|
export declare function tsconfigJson(): string;
|
|
86
89
|
export declare function postcssConfig(): string;
|
|
@@ -150,6 +153,14 @@ export declare function detectSitePort(projectDir: string): Promise<number>;
|
|
|
150
153
|
/**
|
|
151
154
|
* Start a Next.js dev server and wait for it to report "Ready" (or timeout).
|
|
152
155
|
* Replaces fire-and-forget spawn patterns that reported success before the server was up.
|
|
156
|
+
*
|
|
157
|
+
* The server writes to a log file, never to a pipe back to this process. The
|
|
158
|
+
* server is detached so it outlives its launcher, but a pipe does not: in CLI
|
|
159
|
+
* mode the launcher is the short-lived MCP stdio server the Claude CLI spawns,
|
|
160
|
+
* and once it exited nobody drained the pipe. The server kept listening and
|
|
161
|
+
* hung on its first request, so the agent reported "Site running at :3500"
|
|
162
|
+
* over a site that never answered. An orchestrator restart did the same to
|
|
163
|
+
* every site the SDK path had started.
|
|
153
164
|
*/
|
|
154
165
|
export declare function startAndWaitForDevServer(opts: {
|
|
155
166
|
siteId: string;
|
|
@@ -161,4 +172,5 @@ export declare function startAndWaitForDevServer(opts: {
|
|
|
161
172
|
logger?: Logger;
|
|
162
173
|
}): Promise<{
|
|
163
174
|
serverReady: boolean;
|
|
175
|
+
logFile: string;
|
|
164
176
|
}>;
|