@avocadostudio-ai/orchestrator-core 0.11.8 → 0.12.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/dist/agent/sites-agent-shared.d.ts +7 -4
- package/dist/agent/sites-agent-shared.js +14 -12
- package/dist/agent/sites-agent-tools.d.ts +12 -0
- package/dist/agent/sites-agent-tools.js +85 -74
- package/dist/chat/anthropic-cache.js +25 -2
- package/dist/chat/chat-pipeline.js +10 -9
- package/dist/chat/model-defaults.d.ts +17 -0
- package/dist/chat/model-defaults.js +58 -0
- package/dist/chat/planner.js +4 -3
- package/dist/chat/translation-chunking.d.ts +20 -0
- package/dist/chat/translation-chunking.js +6 -12
- package/dist/demo-mode.js +1 -7
- package/dist/env-flags.d.ts +21 -0
- package/dist/env-flags.js +28 -0
- package/dist/handler/create-orchestrator.d.ts +11 -2
- package/dist/handler/create-orchestrator.js +3 -26
- package/dist/index.d.ts +2 -0
- package/dist/index.js +9 -0
- package/dist/nlp/deterministic-planner-suggestions.js +23 -36
- package/package.json +3 -3
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Contains: prop validation/correction, CSS patching, port allocation, site ID sanitization.
|
|
6
6
|
*/
|
|
7
|
+
import { type Logger } from "../logger.ts";
|
|
7
8
|
export declare function sanitizeSiteId(name: string): string;
|
|
8
9
|
export declare function monorepoRoot(): string;
|
|
9
10
|
/**
|
|
@@ -15,7 +16,7 @@ export declare function monorepoRoot(): string;
|
|
|
15
16
|
* forgotten.
|
|
16
17
|
*/
|
|
17
18
|
export declare function getDraftModeSecret(): string;
|
|
18
|
-
export declare function findAvailablePort(root: string): Promise<number>;
|
|
19
|
+
export declare function findAvailablePort(root: string, log?: Logger): Promise<number>;
|
|
19
20
|
export interface ThemePreset {
|
|
20
21
|
name: string;
|
|
21
22
|
description: string;
|
|
@@ -52,10 +53,10 @@ export type TriageResult = {
|
|
|
52
53
|
* Fast triage using Haiku — classifies intent, extracts params, answers simple questions.
|
|
53
54
|
* ~200ms, ~$0.001 per call.
|
|
54
55
|
*/
|
|
55
|
-
export declare function triageWithHaiku(message: string, apiKey: string): Promise<TriageResult>;
|
|
56
|
+
export declare function triageWithHaiku(message: string, apiKey: string, log?: Logger): Promise<TriageResult>;
|
|
56
57
|
export declare function patchGlobalsCssVars(cssPath: string, vars: Record<string, string>): Promise<void>;
|
|
57
58
|
export declare function fixFooterLinks(props: Record<string, unknown>): Record<string, unknown>;
|
|
58
|
-
export declare function validateAndCorrectProps(blockType: string, props: Record<string, unknown
|
|
59
|
+
export declare function validateAndCorrectProps(blockType: string, props: Record<string, unknown>, log?: Logger): {
|
|
59
60
|
props: Record<string, unknown>;
|
|
60
61
|
corrected: boolean;
|
|
61
62
|
error?: string;
|
|
@@ -67,7 +68,7 @@ export declare function validateAndCorrectProps(blockType: string, props: Record
|
|
|
67
68
|
export declare function normalizePageBlocks(blocks: Array<{
|
|
68
69
|
type: string;
|
|
69
70
|
props: Record<string, unknown>;
|
|
70
|
-
}
|
|
71
|
+
}>, log?: Logger): {
|
|
71
72
|
contentBlocks: Array<{
|
|
72
73
|
id: string;
|
|
73
74
|
type: string;
|
|
@@ -156,6 +157,8 @@ export declare function startAndWaitForDevServer(opts: {
|
|
|
156
157
|
cwd: string;
|
|
157
158
|
useFilter?: boolean;
|
|
158
159
|
timeoutMs?: number;
|
|
160
|
+
/** Where progress goes. Defaults to `consoleLogger()`. */
|
|
161
|
+
logger?: Logger;
|
|
159
162
|
}): Promise<{
|
|
160
163
|
serverReady: boolean;
|
|
161
164
|
}>;
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Contains: prop validation/correction, CSS patching, port allocation, site ID sanitization.
|
|
6
6
|
*/
|
|
7
|
+
import { consoleLogger } from "../logger.js";
|
|
7
8
|
import { mkdir, writeFile, readFile, readdir, stat } from "node:fs/promises";
|
|
8
9
|
import { existsSync } from "node:fs";
|
|
9
10
|
import { resolve, join, dirname } from "node:path";
|
|
@@ -30,7 +31,7 @@ export function monorepoRoot() {
|
|
|
30
31
|
export function getDraftModeSecret() {
|
|
31
32
|
return process.env.DRAFT_MODE_SECRET ?? "top-secret";
|
|
32
33
|
}
|
|
33
|
-
export async function findAvailablePort(root) {
|
|
34
|
+
export async function findAvailablePort(root, log = consoleLogger()) {
|
|
34
35
|
const appsDir = join(root, "apps");
|
|
35
36
|
const usedPorts = new Set([3000, 4100, 4200]);
|
|
36
37
|
try {
|
|
@@ -58,7 +59,7 @@ export async function findAvailablePort(root) {
|
|
|
58
59
|
let port = 3500;
|
|
59
60
|
while (usedPorts.has(port) || !(await isPortFree(createServer, port)))
|
|
60
61
|
port++;
|
|
61
|
-
|
|
62
|
+
log.info(`[sites-agent] Selected port ${port} (used: ${[...usedPorts].sort().join(", ")})`);
|
|
62
63
|
return port;
|
|
63
64
|
}
|
|
64
65
|
function isPortFree(createServer, port) {
|
|
@@ -217,7 +218,7 @@ export const SITES_AGENT_MODELS = {
|
|
|
217
218
|
* Fast triage using Haiku — classifies intent, extracts params, answers simple questions.
|
|
218
219
|
* ~200ms, ~$0.001 per call.
|
|
219
220
|
*/
|
|
220
|
-
export async function triageWithHaiku(message, apiKey) {
|
|
221
|
+
export async function triageWithHaiku(message, apiKey, log = consoleLogger()) {
|
|
221
222
|
const { default: Anthropic } = await import("@anthropic-ai/sdk");
|
|
222
223
|
const client = new Anthropic({ apiKey });
|
|
223
224
|
const response = await client.messages.create({
|
|
@@ -259,11 +260,11 @@ Extract URLs, site names, and scope constraints from the message. If not present
|
|
|
259
260
|
// Strip markdown code fences if present
|
|
260
261
|
const clean = text.replace(/^```json?\s*|\s*```$/g, "").trim();
|
|
261
262
|
const parsed = JSON.parse(clean);
|
|
262
|
-
|
|
263
|
+
log.info(`[sites-agent] Triage: intent=${parsed.intent} url=${parsed.url ?? "-"} site=${parsed.siteName ?? "-"} scope=${parsed.scope ?? "-"}`);
|
|
263
264
|
return parsed;
|
|
264
265
|
}
|
|
265
266
|
catch {
|
|
266
|
-
|
|
267
|
+
log.info(`[sites-agent] Triage parse failed, defaulting to migrate: ${text.slice(0, 100)}`);
|
|
267
268
|
return { intent: "migrate" };
|
|
268
269
|
}
|
|
269
270
|
}
|
|
@@ -366,7 +367,7 @@ function renameKeys(obj, renames) {
|
|
|
366
367
|
}
|
|
367
368
|
return result;
|
|
368
369
|
}
|
|
369
|
-
export function validateAndCorrectProps(blockType, props) {
|
|
370
|
+
export function validateAndCorrectProps(blockType, props, log = consoleLogger()) {
|
|
370
371
|
const first = validateBlockProps(blockType, props);
|
|
371
372
|
if (first.success)
|
|
372
373
|
return { props, corrected: false };
|
|
@@ -382,7 +383,7 @@ export function validateAndCorrectProps(blockType, props) {
|
|
|
382
383
|
corrected = fixFooterLinks(corrected);
|
|
383
384
|
const second = validateBlockProps(blockType, corrected);
|
|
384
385
|
if (second.success) {
|
|
385
|
-
|
|
386
|
+
log.info(`[sites-agent] Auto-corrected ${blockType} props`);
|
|
386
387
|
return { props: corrected, corrected: true };
|
|
387
388
|
}
|
|
388
389
|
const errorMsg = first.success === false && "error" in first
|
|
@@ -394,7 +395,7 @@ export function validateAndCorrectProps(blockType, props) {
|
|
|
394
395
|
* Normalize and validate blocks for a page — applies prop correction, strips chrome blocks,
|
|
395
396
|
* extracts footer. Shared between SDK and stdio bootstrap_pages implementations.
|
|
396
397
|
*/
|
|
397
|
-
export function normalizePageBlocks(blocks) {
|
|
398
|
+
export function normalizePageBlocks(blocks, log = consoleLogger()) {
|
|
398
399
|
let strippedCount = 0;
|
|
399
400
|
let footerBlock = null;
|
|
400
401
|
const contentBlocks = blocks
|
|
@@ -432,7 +433,7 @@ export function normalizePageBlocks(blocks) {
|
|
|
432
433
|
}
|
|
433
434
|
}
|
|
434
435
|
else if (validation.error) {
|
|
435
|
-
|
|
436
|
+
log.warn(`[sites-agent] ${b.type} block validation: ${validation.error}`);
|
|
436
437
|
}
|
|
437
438
|
}
|
|
438
439
|
return { id: `b_${b.type.toLowerCase()}_${i + 1}`, type: b.type, props: mergedProps };
|
|
@@ -1043,6 +1044,7 @@ export async function detectSitePort(projectDir) {
|
|
|
1043
1044
|
* Replaces fire-and-forget spawn patterns that reported success before the server was up.
|
|
1044
1045
|
*/
|
|
1045
1046
|
export async function startAndWaitForDevServer(opts) {
|
|
1047
|
+
const log = opts.logger ?? consoleLogger();
|
|
1046
1048
|
const { spawn } = await import("node:child_process");
|
|
1047
1049
|
const args = opts.useFilter
|
|
1048
1050
|
? ["--filter", `@ai-site-editor/${opts.siteId}`, "dev"]
|
|
@@ -1058,11 +1060,11 @@ export async function startAndWaitForDevServer(opts) {
|
|
|
1058
1060
|
}
|
|
1059
1061
|
catch { /* already dead */ }
|
|
1060
1062
|
}
|
|
1061
|
-
|
|
1063
|
+
log.info(`[sites-agent] Killed existing process(es) on port ${opts.port}`);
|
|
1062
1064
|
}
|
|
1063
1065
|
}
|
|
1064
1066
|
catch { /* no process on port — fine */ }
|
|
1065
|
-
|
|
1067
|
+
log.info(`[sites-agent] Starting dev server for ${opts.siteId} on port ${opts.port}...`);
|
|
1066
1068
|
const devProcess = spawn("pnpm", args, {
|
|
1067
1069
|
cwd: opts.cwd,
|
|
1068
1070
|
stdio: ["ignore", "pipe", "pipe"],
|
|
@@ -1097,6 +1099,6 @@ export async function startAndWaitForDevServer(opts) {
|
|
|
1097
1099
|
devProcess.stderr?.on("data", onData);
|
|
1098
1100
|
devProcess.on("exit", () => { clearTimeout(timer); resolve(); });
|
|
1099
1101
|
});
|
|
1100
|
-
|
|
1102
|
+
log.info(`[sites-agent] Dev server ${serverReady ? "ready" : "timed out"} for ${opts.siteId} at http://localhost:${opts.port}`);
|
|
1101
1103
|
return { serverReady };
|
|
1102
1104
|
}
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* - list_sites, create_site, scrape_url (with screenshot), extract_design_tokens,
|
|
6
6
|
* bootstrap_pages, download_remote_image, apply_theme, discover_site_structure
|
|
7
7
|
*/
|
|
8
|
+
import { type Logger } from "../logger.ts";
|
|
8
9
|
/**
|
|
9
10
|
* Create the MCP server with all sites-agent tools.
|
|
10
11
|
*/
|
|
@@ -15,4 +16,15 @@ export declare function createSitesAgentMcpServer(options: {
|
|
|
15
16
|
tool: string;
|
|
16
17
|
data: Record<string, unknown>;
|
|
17
18
|
}) => void;
|
|
19
|
+
/**
|
|
20
|
+
* Where these tools report progress. Defaults to `consoleLogger()`.
|
|
21
|
+
*
|
|
22
|
+
* It matters that this is a parameter. This module is published, and it had
|
|
23
|
+
* forty-two bare `console.log` calls writing `[create_site] …` onto whatever
|
|
24
|
+
* stdout the host happened to own — unsilenceable, unstructured, and outside
|
|
25
|
+
* the `Logger` seam the rest of the package already threads through. The
|
|
26
|
+
* Fastify host passes its own request logger; a library consumer passes
|
|
27
|
+
* theirs, or takes the console default knowingly.
|
|
28
|
+
*/
|
|
29
|
+
logger?: Logger;
|
|
18
30
|
}): import("@anthropic-ai/claude-agent-sdk").McpSdkServerConfigWithInstance;
|
|
@@ -10,6 +10,7 @@ import { join, dirname } from "node:path";
|
|
|
10
10
|
import { existsSync } from "node:fs";
|
|
11
11
|
import { tool, createSdkMcpServer } from "@anthropic-ai/claude-agent-sdk";
|
|
12
12
|
import { createMigrationTools } from "../migration/migration-tools.js";
|
|
13
|
+
import { consoleLogger } from "../logger.js";
|
|
13
14
|
import { z } from "zod";
|
|
14
15
|
import { getAllBlockMeta, defaultPropsForType } from "@avocadostudio-ai/shared";
|
|
15
16
|
import { fetchPageContent, downloadImage, extractDesignTokens, mapToThemeVariables, discoverSitePages, scrapeFullPage } from "@avocadostudio-ai/migration-sdk";
|
|
@@ -34,18 +35,18 @@ function isRemoteImageUrl(url) {
|
|
|
34
35
|
return IMAGE_URL_RE.test(url) || IMAGE_CDN_RE.test(url);
|
|
35
36
|
}
|
|
36
37
|
/** Recursively walk props and download any remote image URLs, replacing with local paths. */
|
|
37
|
-
async function localizeRemoteImages(props, imagesDir) {
|
|
38
|
+
async function localizeRemoteImages(props, imagesDir, log) {
|
|
38
39
|
let downloaded = 0;
|
|
39
40
|
async function walk(value) {
|
|
40
41
|
if (typeof value === "string" && isRemoteImageUrl(value)) {
|
|
41
42
|
try {
|
|
42
43
|
const result = await downloadImage(value, undefined, imagesDir);
|
|
43
44
|
downloaded++;
|
|
44
|
-
|
|
45
|
+
log.info(`[sites-agent] Auto-downloaded remote image: ${value} → ${result.localPath}`);
|
|
45
46
|
return `/images/${result.fileName}`;
|
|
46
47
|
}
|
|
47
48
|
catch {
|
|
48
|
-
|
|
49
|
+
log.warn(`[sites-agent] Failed to auto-download image: ${value}`);
|
|
49
50
|
return value;
|
|
50
51
|
}
|
|
51
52
|
}
|
|
@@ -70,6 +71,7 @@ async function localizeRemoteImages(props, imagesDir) {
|
|
|
70
71
|
*/
|
|
71
72
|
export function createSitesAgentMcpServer(options) {
|
|
72
73
|
const { session, emitSiteCreated, emitPhaseOutcome } = options;
|
|
74
|
+
const log = options.logger ?? consoleLogger();
|
|
73
75
|
// ── LIST SITES ──
|
|
74
76
|
const listSitesTool = tool("list_sites", "List all site project directories in the monorepo apps/ folder.", {}, async () => {
|
|
75
77
|
try {
|
|
@@ -99,9 +101,9 @@ export function createSitesAgentMcpServer(options) {
|
|
|
99
101
|
url: z.string().describe("Homepage URL of the site to analyze, e.g. 'https://example.com'"),
|
|
100
102
|
}, async (args) => {
|
|
101
103
|
try {
|
|
102
|
-
|
|
104
|
+
log.info(`[discover_structure] Discovering pages on ${args.url}...`);
|
|
103
105
|
const structure = await discoverSitePages(args.url);
|
|
104
|
-
|
|
106
|
+
log.info(`[discover_structure] Found ${structure.totalFound} pages via ${structure.source} on ${structure.origin}`);
|
|
105
107
|
emitPhaseOutcome?.({ tool: "discover_site_structure", data: { totalPages: structure.totalFound, origin: structure.origin } });
|
|
106
108
|
return {
|
|
107
109
|
content: [{
|
|
@@ -140,7 +142,7 @@ export function createSitesAgentMcpServer(options) {
|
|
|
140
142
|
if (existsSync(target))
|
|
141
143
|
await rm(target, { recursive: true, force: true });
|
|
142
144
|
}
|
|
143
|
-
|
|
145
|
+
log.info(`[sites-agent] Cleaned previous content from apps/${siteId}`);
|
|
144
146
|
// Reuse the existing port if not explicitly overridden
|
|
145
147
|
if (!args.port) {
|
|
146
148
|
try {
|
|
@@ -163,49 +165,54 @@ export function createSitesAgentMcpServer(options) {
|
|
|
163
165
|
s.listen(port, "127.0.0.1");
|
|
164
166
|
});
|
|
165
167
|
if (!free) {
|
|
166
|
-
|
|
168
|
+
log.info(`[create_site] Requested port ${port} in use, auto-assigning...`);
|
|
167
169
|
port = 0;
|
|
168
170
|
}
|
|
169
171
|
}
|
|
170
172
|
if (!port)
|
|
171
|
-
port = await findAvailablePort(root);
|
|
172
|
-
|
|
173
|
-
// Create project structure
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
await mkdir(join(projectDir, "app"), { recursive: true });
|
|
180
|
-
await writeFile(join(projectDir, "app/layout.tsx"), layoutTsx(args.name), "utf-8");
|
|
181
|
-
await writeFile(join(projectDir, "app/globals.css"), globalsCss(), "utf-8");
|
|
182
|
-
await mkdir(join(projectDir, "app/api/editor/[...path]"), { recursive: true });
|
|
183
|
-
await writeFile(join(projectDir, "app/api/editor/[...path]/route.ts"), editorApiRoute(), "utf-8");
|
|
184
|
-
await mkdir(join(projectDir, "app/[[...slug]]"), { recursive: true });
|
|
185
|
-
await writeFile(join(projectDir, "app/[[...slug]]/page.tsx"), pageTsx(siteId), "utf-8");
|
|
186
|
-
await mkdir(join(projectDir, "content"), { recursive: true });
|
|
187
|
-
await writeFile(join(projectDir, "content/pages.json"), samplePagesJson(), "utf-8");
|
|
188
|
-
await mkdir(join(projectDir, "lib"), { recursive: true });
|
|
189
|
-
await writeFile(join(projectDir, "lib/defaults.ts"), defaultsTs(siteId, args.name), "utf-8");
|
|
190
|
-
await mkdir(join(projectDir, "public"), { recursive: true });
|
|
191
|
-
await writeFile(join(projectDir, "public/.gitkeep"), "", "utf-8");
|
|
192
|
-
await mkdir(join(projectDir, "blocks"), { recursive: true });
|
|
193
|
-
await writeFile(join(projectDir, "blocks/register.tsx"), blocksRegisterTsx(), "utf-8");
|
|
194
|
-
await writeFile(join(projectDir, "public/logo.svg"), defaultLogoSvg(args.name), "utf-8");
|
|
195
|
-
await writeFile(join(projectDir, "public/favicon.svg"), faviconSvg(args.name), "utf-8");
|
|
173
|
+
port = await findAvailablePort(root, log);
|
|
174
|
+
log.info(`[create_site] START siteId=${siteId} port=${port} name="${args.name}"`);
|
|
175
|
+
// Create project structure.
|
|
176
|
+
//
|
|
177
|
+
// One entry per file, and the log line below counts the list rather than
|
|
178
|
+
// a number someone typed. It used to say "Wrote 13 project files" while
|
|
179
|
+
// writing fifteen — the count was right when the sequence of writeFile
|
|
180
|
+
// calls above it was shorter, and nothing connected the two.
|
|
196
181
|
const envContent = `ORCHESTRATOR_URL=http://localhost:4200\nDRAFT_MODE_SECRET=${getDraftModeSecret()}\nNEXT_PUBLIC_DEFAULT_SITE_ID=${siteId}\nNEXT_PUBLIC_SITE_NAME=${args.name}\nNEXT_PUBLIC_EDITOR_ORIGIN=http://localhost:4100\n`;
|
|
197
|
-
|
|
198
|
-
|
|
182
|
+
const projectFiles = [
|
|
183
|
+
["package.json", packageJson(siteId, args.name, port)],
|
|
184
|
+
["next.config.ts", nextConfigTs()],
|
|
185
|
+
["tsconfig.json", tsconfigJson()],
|
|
186
|
+
["postcss.config.mjs", postcssConfig()],
|
|
187
|
+
["app/layout.tsx", layoutTsx(args.name)],
|
|
188
|
+
["app/globals.css", globalsCss()],
|
|
189
|
+
["app/api/editor/[...path]/route.ts", editorApiRoute()],
|
|
190
|
+
["app/[[...slug]]/page.tsx", pageTsx(siteId)],
|
|
191
|
+
["content/pages.json", samplePagesJson()],
|
|
192
|
+
["lib/defaults.ts", defaultsTs(siteId, args.name)],
|
|
193
|
+
["public/.gitkeep", ""],
|
|
194
|
+
["blocks/register.tsx", blocksRegisterTsx()],
|
|
195
|
+
["public/logo.svg", defaultLogoSvg(args.name)],
|
|
196
|
+
["public/favicon.svg", faviconSvg(args.name)],
|
|
197
|
+
[".env.local", envContent]
|
|
198
|
+
];
|
|
199
|
+
await mkdir(projectDir, { recursive: true });
|
|
200
|
+
for (const [relativePath, contents] of projectFiles) {
|
|
201
|
+
const target = join(projectDir, relativePath);
|
|
202
|
+
await mkdir(dirname(target), { recursive: true });
|
|
203
|
+
await writeFile(target, contents, "utf-8");
|
|
204
|
+
}
|
|
205
|
+
log.info(`[create_site] Wrote ${projectFiles.length} project files to apps/${siteId}/`);
|
|
199
206
|
// Run pnpm install if needed (skip if node_modules exists from previous run)
|
|
200
207
|
if (!existsSync(join(projectDir, "node_modules"))) {
|
|
201
|
-
|
|
208
|
+
log.info(`[create_site] Running pnpm install...`);
|
|
202
209
|
const { execFile } = await import("node:child_process");
|
|
203
210
|
const { promisify } = await import("node:util");
|
|
204
211
|
await promisify(execFile)("pnpm", ["install", "--no-frozen-lockfile"], { cwd: root, timeout: 60_000 });
|
|
205
|
-
|
|
212
|
+
log.info(`[create_site] pnpm install complete`);
|
|
206
213
|
}
|
|
207
214
|
else {
|
|
208
|
-
|
|
215
|
+
log.info(`[create_site] Skipping pnpm install — node_modules exists`);
|
|
209
216
|
}
|
|
210
217
|
const siteConfig = {
|
|
211
218
|
id: siteId,
|
|
@@ -223,14 +230,15 @@ export function createSitesAgentMcpServer(options) {
|
|
|
223
230
|
});
|
|
224
231
|
bumpVersion(sessionKey);
|
|
225
232
|
// Start dev server and wait for it to be ready
|
|
226
|
-
|
|
233
|
+
log.info(`[create_site] Starting dev server on port ${port}...`);
|
|
227
234
|
const { serverReady } = await startAndWaitForDevServer({
|
|
235
|
+
logger: log,
|
|
228
236
|
siteId, port, cwd: root, useFilter: true,
|
|
229
237
|
});
|
|
230
|
-
|
|
238
|
+
log.info(`[create_site] Dev server ${serverReady ? "READY" : "FAILED"} on port ${port}`);
|
|
231
239
|
emitSiteCreated(siteConfig);
|
|
232
240
|
emitPhaseOutcome?.({ tool: "create_site", data: { siteId, port, name: args.name, serverReady } });
|
|
233
|
-
|
|
241
|
+
log.info(`[create_site] DONE siteId=${siteId} port=${port} serverReady=${serverReady}`);
|
|
234
242
|
return {
|
|
235
243
|
content: [{
|
|
236
244
|
type: "text",
|
|
@@ -248,7 +256,7 @@ export function createSitesAgentMcpServer(options) {
|
|
|
248
256
|
};
|
|
249
257
|
}
|
|
250
258
|
catch (e) {
|
|
251
|
-
|
|
259
|
+
log.error(`[create_site] ERROR: ${e instanceof Error ? e.message : String(e)}`);
|
|
252
260
|
return { content: [{ type: "text", text: `Error: ${e instanceof Error ? e.message : String(e)}` }], isError: true };
|
|
253
261
|
}
|
|
254
262
|
}, { annotations: { destructiveHint: false, openWorldHint: false } });
|
|
@@ -261,12 +269,12 @@ export function createSitesAgentMcpServer(options) {
|
|
|
261
269
|
let result = getCachedScrape(args.url);
|
|
262
270
|
const cached = !!result;
|
|
263
271
|
if (!result) {
|
|
264
|
-
|
|
272
|
+
log.info(`[scrape_url] Scraping ${args.url} (browser)...`);
|
|
265
273
|
result = await scrapeFullPage(args.url);
|
|
266
274
|
setCachedScrape(args.url, result);
|
|
267
275
|
}
|
|
268
276
|
const { content, screenshot, sections, outline, nav } = result;
|
|
269
|
-
|
|
277
|
+
log.info(`[scrape_url] ${cached ? "CACHED" : "SCRAPED"} ${args.url} in ${Date.now() - scrapeStart}ms — ${sections.length} sections, screenshot=${!!screenshot}, nav=${!!nav}`);
|
|
270
278
|
// Extract design tokens from CSS (with resolved CSS variables from Playwright)
|
|
271
279
|
const tokens = extractDesignTokens(content.css, result.resolvedCssVars);
|
|
272
280
|
const themeVars = mapToThemeVariables(tokens);
|
|
@@ -305,13 +313,13 @@ export function createSitesAgentMcpServer(options) {
|
|
|
305
313
|
if (mobileScreenshot)
|
|
306
314
|
writes.push(saveScreenshot("mobile", mobileScreenshot.base64, args.url));
|
|
307
315
|
if (writes.length)
|
|
308
|
-
Promise.all(writes).then(() =>
|
|
316
|
+
Promise.all(writes).then(() => log.info(`[scrape_url] Screenshots saved`)).catch(() => { });
|
|
309
317
|
return { content: contentBlocks };
|
|
310
318
|
}
|
|
311
319
|
catch (e) {
|
|
312
320
|
// Fall back to simple HTTP fetch (no browser)
|
|
313
321
|
try {
|
|
314
|
-
|
|
322
|
+
log.warn(`[sites-agent] Browser scrape failed, falling back to HTTP: ${e instanceof Error ? e.message : String(e)}`);
|
|
315
323
|
const content = await fetchPageContent(args.url);
|
|
316
324
|
const { extractSections: extract, extractPageOutline: outline, extractNavigation: navExtract } = await import("@avocadostudio-ai/migration-sdk");
|
|
317
325
|
const sections = extract(content.html, content.baseUrl);
|
|
@@ -391,12 +399,12 @@ export function createSitesAgentMcpServer(options) {
|
|
|
391
399
|
try {
|
|
392
400
|
const totalBlocks = args.pages.reduce((sum, p) => sum + p.blocks.length, 0);
|
|
393
401
|
const slugs = args.pages.map(p => p.slug);
|
|
394
|
-
|
|
402
|
+
log.info(`[bootstrap_pages] START siteId=${args.siteId} pages=${args.pages.length} blocks=${totalBlocks} slugs=[${slugs.join(", ")}]`);
|
|
395
403
|
// Verify the site project was scaffolded first
|
|
396
404
|
const root = monorepoRoot();
|
|
397
405
|
const projectPkg = join(root, "apps", args.siteId, "package.json");
|
|
398
406
|
if (!existsSync(projectPkg)) {
|
|
399
|
-
|
|
407
|
+
log.error(`[bootstrap_pages] GUARD FAILED: apps/${args.siteId}/package.json not found — create_site was not called`);
|
|
400
408
|
return { content: [{ type: "text", text: `Error: Site project apps/${args.siteId} does not exist. Call create_site first.` }], isError: true };
|
|
401
409
|
}
|
|
402
410
|
const sessionKey = scopedSessionKey(session, args.siteId);
|
|
@@ -414,7 +422,7 @@ export function createSitesAgentMcpServer(options) {
|
|
|
414
422
|
// Process pages sequentially to avoid unbounded concurrent image downloads
|
|
415
423
|
const pageDocs = [];
|
|
416
424
|
for (const [pageIdx, page] of normalizedPages.entries()) {
|
|
417
|
-
|
|
425
|
+
log.info(`[bootstrap_pages] Processing page ${pageIdx + 1}/${normalizedPages.length}: ${page.slug} (${page.blocks.length} blocks)`);
|
|
418
426
|
const contentBlocks = page.blocks.filter(b => {
|
|
419
427
|
if (b.type === "SiteHeader") {
|
|
420
428
|
strippedCount++;
|
|
@@ -423,7 +431,7 @@ export function createSitesAgentMcpServer(options) {
|
|
|
423
431
|
if (b.type === "Footer") {
|
|
424
432
|
if (!footerBlock) {
|
|
425
433
|
let fp = fixFooterLinks(b.props);
|
|
426
|
-
const v = validateAndCorrectProps("Footer", fp);
|
|
434
|
+
const v = validateAndCorrectProps("Footer", fp, log);
|
|
427
435
|
if (v.corrected)
|
|
428
436
|
fp = v.props;
|
|
429
437
|
footerBlock = { type: b.type, props: fp };
|
|
@@ -441,26 +449,26 @@ export function createSitesAgentMcpServer(options) {
|
|
|
441
449
|
mergedProps[key] = value;
|
|
442
450
|
}
|
|
443
451
|
if (blockMeta) {
|
|
444
|
-
const validation = validateAndCorrectProps(b.type, mergedProps);
|
|
452
|
+
const validation = validateAndCorrectProps(b.type, mergedProps, log);
|
|
445
453
|
if (validation.corrected) {
|
|
446
|
-
|
|
454
|
+
log.info(`[bootstrap_pages] Block ${b.type}: auto-corrected props`);
|
|
447
455
|
mergedProps = { ...baseProps };
|
|
448
456
|
for (const [key, value] of Object.entries(validation.props)) {
|
|
449
457
|
mergedProps[key] = value;
|
|
450
458
|
}
|
|
451
459
|
}
|
|
452
460
|
else if (validation.error) {
|
|
453
|
-
|
|
461
|
+
log.warn(`[bootstrap_pages] Block ${b.type}: validation error — ${validation.error}`);
|
|
454
462
|
}
|
|
455
463
|
}
|
|
456
464
|
else if (!getAllBlockMeta()[b.type]) {
|
|
457
|
-
|
|
465
|
+
log.info(`[bootstrap_pages] Block ${b.type}: custom type (not in built-in registry)`);
|
|
458
466
|
}
|
|
459
467
|
return { id: `b_${b.type.toLowerCase()}_${i + 1}`, type: b.type, props: mergedProps };
|
|
460
468
|
});
|
|
461
469
|
// Auto-download any remote image URLs the agent missed
|
|
462
470
|
for (const block of blocks) {
|
|
463
|
-
const { props: localizedProps, downloaded } = await localizeRemoteImages(block.props, imagesDir);
|
|
471
|
+
const { props: localizedProps, downloaded } = await localizeRemoteImages(block.props, imagesDir, log);
|
|
464
472
|
if (downloaded > 0) {
|
|
465
473
|
block.props = localizedProps;
|
|
466
474
|
totalAutoDownloaded += downloaded;
|
|
@@ -476,9 +484,9 @@ export function createSitesAgentMcpServer(options) {
|
|
|
476
484
|
});
|
|
477
485
|
}
|
|
478
486
|
if (strippedCount > 0)
|
|
479
|
-
|
|
487
|
+
log.info(`[sites-agent] Stripped ${strippedCount} chrome blocks (SiteHeader/Footer)`);
|
|
480
488
|
if (totalAutoDownloaded > 0)
|
|
481
|
-
|
|
489
|
+
log.info(`[sites-agent] Auto-downloaded ${totalAutoDownloaded} remote images as safety net`);
|
|
482
490
|
// Write to session state
|
|
483
491
|
for (const doc of pageDocs)
|
|
484
492
|
setPage(sessionKey, doc);
|
|
@@ -523,10 +531,10 @@ export function createSitesAgentMcpServer(options) {
|
|
|
523
531
|
}
|
|
524
532
|
await mkdir(dirname(pagesJsonPath), { recursive: true });
|
|
525
533
|
await writeFile(pagesJsonPath, JSON.stringify(allPages, null, 2) + "\n", "utf-8");
|
|
526
|
-
|
|
534
|
+
log.info(`[sites-agent] Wrote ${allPages.length} pages to ${pagesJsonPath}`);
|
|
527
535
|
}
|
|
528
536
|
catch (writeErr) {
|
|
529
|
-
|
|
537
|
+
log.warn({ err: writeErr }, "[sites-agent] Failed to write pages.json");
|
|
530
538
|
}
|
|
531
539
|
// Persist site config (nav labels, nav groups, logo, name, footer) to content/site-config.json
|
|
532
540
|
{
|
|
@@ -559,25 +567,25 @@ export function createSitesAgentMcpServer(options) {
|
|
|
559
567
|
config.footer = { id: "chrome_footer", type: fb.type, props: fb.props };
|
|
560
568
|
}
|
|
561
569
|
await writeFile(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
562
|
-
|
|
570
|
+
log.info(`[sites-agent] Wrote site config to ${configPath}`);
|
|
563
571
|
}
|
|
564
572
|
catch (err) {
|
|
565
|
-
|
|
573
|
+
log.warn({ err }, "[sites-agent] Failed to write site-config.json");
|
|
566
574
|
}
|
|
567
575
|
}
|
|
568
576
|
// Persist theme overrides to globals.css
|
|
569
577
|
if (args.themeOverrides && Object.keys(args.themeOverrides).length > 0) {
|
|
570
578
|
try {
|
|
571
579
|
await patchGlobalsCssVars(join(root, "apps", args.siteId, "app", "globals.css"), args.themeOverrides);
|
|
572
|
-
|
|
580
|
+
log.info(`[sites-agent] Updated ${Object.keys(args.themeOverrides).length} theme vars in globals.css`);
|
|
573
581
|
}
|
|
574
582
|
catch (cssErr) {
|
|
575
|
-
|
|
583
|
+
log.warn({ err: cssErr }, "[sites-agent] Failed to update globals.css");
|
|
576
584
|
}
|
|
577
585
|
}
|
|
578
586
|
const totalBlocksFinal = args.pages.reduce((sum, p) => sum + p.blocks.length, 0);
|
|
579
587
|
emitPhaseOutcome?.({ tool: "bootstrap_pages", data: { pagesCreated: createdPages.length, totalBlocks: totalBlocksFinal, pages: createdPages } });
|
|
580
|
-
|
|
588
|
+
log.info(`[bootstrap_pages] DONE siteId=${args.siteId} pages=${createdPages.length} blocks=${totalBlocksFinal} autoDownloaded=${totalAutoDownloaded}`);
|
|
581
589
|
return {
|
|
582
590
|
content: [{
|
|
583
591
|
type: "text",
|
|
@@ -591,7 +599,7 @@ export function createSitesAgentMcpServer(options) {
|
|
|
591
599
|
};
|
|
592
600
|
}
|
|
593
601
|
catch (e) {
|
|
594
|
-
|
|
602
|
+
log.error(`[bootstrap_pages] ERROR: ${e instanceof Error ? e.message : String(e)}`);
|
|
595
603
|
return { content: [{ type: "text", text: `Error: ${e instanceof Error ? e.message : String(e)}` }], isError: true };
|
|
596
604
|
}
|
|
597
605
|
}, { annotations: { destructiveHint: false, openWorldHint: false } });
|
|
@@ -604,15 +612,15 @@ export function createSitesAgentMcpServer(options) {
|
|
|
604
612
|
try {
|
|
605
613
|
const root = monorepoRoot();
|
|
606
614
|
const outputDir = join(root, "apps", args.siteId, "public", "images");
|
|
607
|
-
|
|
615
|
+
log.info(`[download_image] ${args.url.slice(0, 100)}`);
|
|
608
616
|
const result = await downloadImage(args.url, args.alt, outputDir);
|
|
609
617
|
const localUrl = `/images/${result.fileName}`;
|
|
610
|
-
|
|
618
|
+
log.info(`[download_image] OK → ${localUrl}`);
|
|
611
619
|
emitPhaseOutcome?.({ tool: "download_remote_image", data: { fileName: result.fileName } });
|
|
612
620
|
return { content: [{ type: "text", text: JSON.stringify({ localUrl, fileName: result.fileName }) }] };
|
|
613
621
|
}
|
|
614
622
|
catch (e) {
|
|
615
|
-
|
|
623
|
+
log.warn(`[download_image] FAILED ${args.url.slice(0, 100)} — ${e instanceof Error ? e.message : String(e)}`);
|
|
616
624
|
return { content: [{ type: "text", text: JSON.stringify({ localUrl: args.url, error: "Download failed, using original URL" }) }] };
|
|
617
625
|
}
|
|
618
626
|
}, { annotations: { destructiveHint: false, openWorldHint: true } });
|
|
@@ -644,7 +652,7 @@ export function createSitesAgentMcpServer(options) {
|
|
|
644
652
|
}
|
|
645
653
|
const succeeded = results.filter(r => !r.error).length;
|
|
646
654
|
emitPhaseOutcome?.({ tool: "download_remote_images", data: { succeeded, total: results.length } });
|
|
647
|
-
|
|
655
|
+
log.info(`[sites-agent] Batch downloaded ${succeeded}/${results.length} images`);
|
|
648
656
|
return { content: [{ type: "text", text: JSON.stringify({ results, succeeded, failed: results.length - succeeded }) }] };
|
|
649
657
|
}, { annotations: { destructiveHint: false, openWorldHint: true } });
|
|
650
658
|
// ── BROWSE GOOGLE DRIVE IMAGES ──
|
|
@@ -710,7 +718,7 @@ export function createSitesAgentMcpServer(options) {
|
|
|
710
718
|
contentBlocks.push({ type: "text", text: `↑ ${r.name} → ${r.localUrl}` });
|
|
711
719
|
}
|
|
712
720
|
}
|
|
713
|
-
|
|
721
|
+
log.info(`[sites-agent] Browsed GDrive: ${manifest.length}/${files.length} images downloaded`);
|
|
714
722
|
return {
|
|
715
723
|
content: [
|
|
716
724
|
{ type: "text", text: `Found ${manifest.length} images. Thumbnails below — use localUrl paths in block props.\n${JSON.stringify(manifest, null, 2)}` },
|
|
@@ -734,7 +742,7 @@ export function createSitesAgentMcpServer(options) {
|
|
|
734
742
|
await patchGlobalsCssVars(join(monorepoRoot(), "apps", args.siteId, "app", "globals.css"), args.variables);
|
|
735
743
|
}
|
|
736
744
|
catch (err) {
|
|
737
|
-
|
|
745
|
+
log.warn({ err }, "[sites-agent] Failed to persist theme to globals.css");
|
|
738
746
|
}
|
|
739
747
|
return {
|
|
740
748
|
content: [{
|
|
@@ -973,8 +981,8 @@ export function createSitesAgentMcpServer(options) {
|
|
|
973
981
|
const { execSync } = await import("node:child_process");
|
|
974
982
|
const pids = execSync(`lsof -ti:${port} 2>/dev/null`, { encoding: "utf-8" }).trim();
|
|
975
983
|
if (pids) {
|
|
976
|
-
|
|
977
|
-
port = await findAvailablePort(root);
|
|
984
|
+
log.info(`[integrate_site] Port ${port} in use, finding free port...`);
|
|
985
|
+
port = await findAvailablePort(root, log);
|
|
978
986
|
const pkg = JSON.parse(await readFile(join(projectDir, "package.json"), "utf-8"));
|
|
979
987
|
if (pkg.scripts?.dev) {
|
|
980
988
|
pkg.scripts.dev = pkg.scripts.dev.replace(/-p\s*\d+/, `-p ${port}`);
|
|
@@ -992,6 +1000,7 @@ export function createSitesAgentMcpServer(options) {
|
|
|
992
1000
|
// Start dev server and wait for ready
|
|
993
1001
|
const previewUrl = `http://localhost:${port}`;
|
|
994
1002
|
const { serverReady } = await startAndWaitForDevServer({
|
|
1003
|
+
logger: log,
|
|
995
1004
|
siteId: args.siteId, port, cwd: projectDir,
|
|
996
1005
|
});
|
|
997
1006
|
// Register in editor dashboard (emits site_created SSE event)
|
|
@@ -1043,8 +1052,8 @@ export function createSitesAgentMcpServer(options) {
|
|
|
1043
1052
|
const { execSync } = await import("node:child_process");
|
|
1044
1053
|
const pids = execSync(`lsof -ti:${port} 2>/dev/null`, { encoding: "utf-8" }).trim();
|
|
1045
1054
|
if (pids) {
|
|
1046
|
-
|
|
1047
|
-
port = await findAvailablePort(root);
|
|
1055
|
+
log.info(`[sites-agent] Port ${port} in use, finding free port...`);
|
|
1056
|
+
port = await findAvailablePort(root, log);
|
|
1048
1057
|
// Update dev script with new port
|
|
1049
1058
|
const pkg = JSON.parse(await readFile(join(projectDir, "package.json"), "utf-8"));
|
|
1050
1059
|
if (pkg.scripts?.dev) {
|
|
@@ -1063,6 +1072,7 @@ export function createSitesAgentMcpServer(options) {
|
|
|
1063
1072
|
// Start dev server and wait for it to be ready
|
|
1064
1073
|
const previewUrl = `http://localhost:${port}`;
|
|
1065
1074
|
const { serverReady } = await startAndWaitForDevServer({
|
|
1075
|
+
logger: log,
|
|
1066
1076
|
siteId: args.siteId, port, cwd: projectDir,
|
|
1067
1077
|
});
|
|
1068
1078
|
const siteName = args.name ?? args.siteId;
|
|
@@ -1130,6 +1140,7 @@ export function createSitesAgentMcpServer(options) {
|
|
|
1130
1140
|
}
|
|
1131
1141
|
// Start dev server and wait for readiness
|
|
1132
1142
|
const { serverReady } = await startAndWaitForDevServer({
|
|
1143
|
+
logger: log,
|
|
1133
1144
|
siteId: args.siteId, port, cwd: root, useFilter: true,
|
|
1134
1145
|
});
|
|
1135
1146
|
const siteConfig = {
|
|
@@ -1,7 +1,30 @@
|
|
|
1
|
-
|
|
1
|
+
import { envFlag } from "../env-flags.js";
|
|
2
|
+
/**
|
|
3
|
+
* Prompt caching is ON by default. Every Anthropic request from this package —
|
|
4
|
+
* planner, intent parser, agent loop, variation pipeline — ships the same large
|
|
5
|
+
* stable system prefix plus the `submit_edit_plan` tool schema, which is the
|
|
6
|
+
* shape caching exists for. Set `ANTHROPIC_PROMPT_CACHE=0` to opt out.
|
|
7
|
+
*
|
|
8
|
+
* It defaulted to `false` until 2026-09-17, and the default was the whole
|
|
9
|
+
* setting: only this repo's own `.env` ever named the variable, so CI eval and
|
|
10
|
+
* e2e runs, `pnpm eval` (`eval-cli.ts` never loads dotenv), every deployment,
|
|
11
|
+
* and every library-mode integrator billed full price on a prefix that was
|
|
12
|
+
* byte-identical across requests. A default is the value almost everyone runs.
|
|
13
|
+
*/
|
|
2
14
|
function promptCacheEnabled() {
|
|
3
|
-
return
|
|
15
|
+
return envFlag("ANTHROPIC_PROMPT_CACHE", true);
|
|
4
16
|
}
|
|
17
|
+
/**
|
|
18
|
+
* Unset means the 5-minute TTL, and on this traffic that is the cheaper one —
|
|
19
|
+
* do not "upgrade" it to `1h` globally. The 1h entry costs 2x to write against
|
|
20
|
+
* 1.25x for 5m, and it only repays that on requests whose start-to-start gap
|
|
21
|
+
* exceeds 5 minutes. Replaying 651 real planner calls from
|
|
22
|
+
* `.data/chat-telemetry.ndjson` (2026-06 onward): most sit under a minute
|
|
23
|
+
* apart, only 44 fell in the 5-60 minute window the longer TTL rescues, and
|
|
24
|
+
* paying 2x on every other write to buy those back came out 20-32% *more*
|
|
25
|
+
* expensive. Set `ANTHROPIC_PROMPT_CACHE_TTL=1h` per-deployment if a given
|
|
26
|
+
* install's traffic really is that bursty — measure it there first.
|
|
27
|
+
*/
|
|
5
28
|
function promptCacheTtl() {
|
|
6
29
|
const raw = (process.env.ANTHROPIC_PROMPT_CACHE_TTL ?? "").trim();
|
|
7
30
|
if (raw === "5m" || raw === "1h")
|
|
@@ -7,6 +7,7 @@ import { isLikelyClarificationFollowUp } from "../nlp/intent-helpers.js";
|
|
|
7
7
|
import { versions, pendingClarificationBySession, chatHistoryBySession, continuationChainBySession, imageSourcePreferenceBySession, getSessionDraft, getPage, setPage, pushUndo, bumpVersion, pushRecentEdit, pushVersionEntry, pushChatHistory, schedulePersistState, removePage } from "../state/session-state.js";
|
|
8
8
|
import { getSiteAssets } from "../state/site-assets.js";
|
|
9
9
|
import { loadPendingPlan, savePendingPlan, clearPendingPlan } from "../durable/pending-plan-store.js";
|
|
10
|
+
import { envFlag } from "../env-flags.js";
|
|
10
11
|
import { toErrorDetail, isNoEffectiveChangeError, isAlreadyCurrentError, classifyGuardrailError, formatValidationError, isRepairEligibleCategory, buildDeterministicRepairFeedback, validateOperations, applyOpsAtomically, isStructuralOperation, pickFocusBlockId, pickUpdatedSlug } from "../ops/ops-engine.js";
|
|
11
12
|
import { evaluateDestructiveActions } from "../ops/destructive-action-gate.js";
|
|
12
13
|
import { clarificationSuggestions, postEditSuggestions, keepDemoExecutable, demoPlanFromMessage, withKeylessNotice, plannerContextPack, compileDeterministicPlan, inferDeterministicIntent, isHighConfidenceDeterministicCase, tryCompoundDeterministicPlan, resolveImageUrlForAltField } from "../nlp/deterministic-planner.js";
|
|
@@ -472,7 +473,7 @@ export async function runChatPipeline(ctx, body, options) {
|
|
|
472
473
|
// downgrade below. Honors an explicit caller provider/modelKey and falls back
|
|
473
474
|
// to the prior auto-routing when Anthropic is unavailable. Disable with
|
|
474
475
|
// CHAT_PLANNER_FORCE_SONNET=0. See CLAUDE.md § Chat Pipeline Flags.
|
|
475
|
-
const forceSonnet =
|
|
476
|
+
const forceSonnet = envFlag("CHAT_PLANNER_FORCE_SONNET", false) &&
|
|
476
477
|
!body.provider &&
|
|
477
478
|
!body.modelKey &&
|
|
478
479
|
ctx.availableProviders.includes("anthropic");
|
|
@@ -697,12 +698,12 @@ export async function runChatPipeline(ctx, body, options) {
|
|
|
697
698
|
imageResolutionDurationMs: imageResolutionDurationMs > 0 ? imageResolutionDurationMs : undefined,
|
|
698
699
|
planningAttempts: planningAttempts > 0 ? planningAttempts : undefined
|
|
699
700
|
});
|
|
700
|
-
const incrementalApplyEnabled =
|
|
701
|
+
const incrementalApplyEnabled = envFlag("CHAT_INCREMENTAL_APPLY", true);
|
|
701
702
|
const streamedApplyMinStepMsRaw = Number(process.env.CHAT_STREAM_APPLY_MIN_STEP_MS ?? 260);
|
|
702
703
|
const streamedApplyMinStepMs = Number.isFinite(streamedApplyMinStepMsRaw) && streamedApplyMinStepMsRaw > 0
|
|
703
704
|
? Math.min(Math.max(Math.trunc(streamedApplyMinStepMsRaw), 1), 2000)
|
|
704
705
|
: 0;
|
|
705
|
-
const incrementalPlanStreamEnabled =
|
|
706
|
+
const incrementalPlanStreamEnabled = envFlag("CHAT_INCREMENTAL_PLAN_STREAM", true);
|
|
706
707
|
const withDebugPayload = (payload, extra) => ({
|
|
707
708
|
...payload,
|
|
708
709
|
debug: {
|
|
@@ -1056,8 +1057,8 @@ export async function runChatPipeline(ctx, body, options) {
|
|
|
1056
1057
|
includeFullProps: translationScope === "page" || contentQuery || requestsPlanFirst(plannerMessage),
|
|
1057
1058
|
attachments: body.attachments
|
|
1058
1059
|
});
|
|
1059
|
-
const compactContextExperimentEnabled =
|
|
1060
|
-
const minimalContextExperimentEnabled =
|
|
1060
|
+
const compactContextExperimentEnabled = envFlag("CHAT_COMPACT_CONTEXT_EXPERIMENT", false);
|
|
1061
|
+
const minimalContextExperimentEnabled = envFlag("CHAT_MINIMAL_CONTEXT_EXPERIMENT", false);
|
|
1061
1062
|
const intentComplexity = !contentQuery
|
|
1062
1063
|
? classifyMessageComplexity({
|
|
1063
1064
|
message: plannerMessage,
|
|
@@ -1535,7 +1536,7 @@ export async function runChatPipeline(ctx, body, options) {
|
|
|
1535
1536
|
// Defer image resolution: apply text/structural ops immediately, then
|
|
1536
1537
|
// resolve images in the background and patch them in via follow-up events.
|
|
1537
1538
|
// This avoids blocking the user for 1-15s while images are fetched/generated.
|
|
1538
|
-
const deferImageResolution =
|
|
1539
|
+
const deferImageResolution = envFlag("CHAT_DEFER_IMAGE_RESOLUTION", true);
|
|
1539
1540
|
if (!usedNativeUnsplashTool && !usedNativeImageTool && deferImageResolution) {
|
|
1540
1541
|
// Tag the plan with deferred image metadata so the apply step can
|
|
1541
1542
|
// resolve images after text ops are visible.
|
|
@@ -3197,7 +3198,7 @@ export async function runChatPipeline(ctx, body, options) {
|
|
|
3197
3198
|
return compoundOutcome.response;
|
|
3198
3199
|
}
|
|
3199
3200
|
}
|
|
3200
|
-
const llmIntentRouterEnabled =
|
|
3201
|
+
const llmIntentRouterEnabled = envFlag("CHAT_LLM_INTENT_ROUTER", true);
|
|
3201
3202
|
// Skip the intent router for batch-add requests — the router's deterministic
|
|
3202
3203
|
// compile can't generate meaningful content for "add 3 blocks" without specific
|
|
3203
3204
|
// block types, so go straight to the full planner which can handle it.
|
|
@@ -3228,7 +3229,7 @@ export async function runChatPipeline(ctx, body, options) {
|
|
|
3228
3229
|
// full planner concurrently. The router gets a ~200ms head start. If it succeeds
|
|
3229
3230
|
// and produces a valid plan, we abort the full planner and return immediately.
|
|
3230
3231
|
// If it fails, the full planner is already running — no wasted time.
|
|
3231
|
-
const parallelPlannerEnabled =
|
|
3232
|
+
const parallelPlannerEnabled = envFlag("CHAT_PARALLEL_PLANNER", true);
|
|
3232
3233
|
const routerHeadStartMs = Math.max(0, Math.min(Number(process.env.CHAT_ROUTER_HEAD_START_MS ?? 200), 1000));
|
|
3233
3234
|
// Request-scoped registry. Closures over *Impl vars so test hooks
|
|
3234
3235
|
// (setGeneratePlanWith*ForTests) still take effect.
|
|
@@ -3814,7 +3815,7 @@ export async function runChatPipeline(ctx, body, options) {
|
|
|
3814
3815
|
// sees changes at ~800ms intervals instead of waiting for the full plan.
|
|
3815
3816
|
const streamedPerOpApplyEnabled = incrementalApplyEnabled &&
|
|
3816
3817
|
incrementalPlanStreamEnabled &&
|
|
3817
|
-
|
|
3818
|
+
envFlag("CHAT_STREAMED_OP_APPLY", true) &&
|
|
3818
3819
|
Boolean(options?.onOpApplied);
|
|
3819
3820
|
// Raised when an attempt dies on `max_tokens`. Retrying a truncation with the
|
|
3820
3821
|
// same budget reproduces it exactly — three identical failures cost ~250s and
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { AIProvider, ModelKey } from "../state/session-state.ts";
|
|
2
|
+
export type ModelLookup = Record<AIProvider, Record<ModelKey, string>>;
|
|
3
|
+
/**
|
|
4
|
+
* Per-provider, per-tier model defaults, each overridable by its own env var.
|
|
5
|
+
*
|
|
6
|
+
* A function rather than a constant because the env is read at call time:
|
|
7
|
+
* `createOrchestrator()` may be constructed before a host has finished loading
|
|
8
|
+
* its `.env`, and tests set these variables per-case.
|
|
9
|
+
*/
|
|
10
|
+
export declare const defaultModelLookup: () => ModelLookup;
|
|
11
|
+
/**
|
|
12
|
+
* The providers this process has credentials for, in preference order.
|
|
13
|
+
*
|
|
14
|
+
* Order is load-bearing: the chat pipeline falls back to `availableProviders[0]`
|
|
15
|
+
* when a request names no provider.
|
|
16
|
+
*/
|
|
17
|
+
export declare const defaultProviders: () => AIProvider[];
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// The default model per (provider, tier), and the providers a set of API keys
|
|
2
|
+
// makes available.
|
|
3
|
+
//
|
|
4
|
+
// This lived twice: once in apps/orchestrator/src/index.ts (the Fastify host)
|
|
5
|
+
// and once in handler/create-orchestrator.ts (library mode). Both read the same
|
|
6
|
+
// `OPENAI_MODEL_*` / `ANTHROPIC_MODEL_*` / `GOOGLE_GENAI_MODEL_*` variables, so
|
|
7
|
+
// a deployment that set them got identical behaviour either way and the split
|
|
8
|
+
// was invisible. A deployment that set none — which is every `createOrchestrator()`
|
|
9
|
+
// consumer following the README — got different models depending on which half
|
|
10
|
+
// of the repo was serving it.
|
|
11
|
+
//
|
|
12
|
+
// They drifted the moment that mattered. #42 ("refresh OpenAI + Gemini defaults
|
|
13
|
+
// to current GA models") edited the Fastify copy; the library copy was carved
|
|
14
|
+
// out later in #58 and kept the pre-refresh values, so library mode was still
|
|
15
|
+
// defaulting to `gpt-4o-mini`, `gpt-4o`, `o1`, `o3` and `gemini-2.5-flash` two
|
|
16
|
+
// model generations after the standalone server had moved off them. Nothing
|
|
17
|
+
// failed: the models still answered, just older, slower and worse at planning.
|
|
18
|
+
//
|
|
19
|
+
// One table, one place to edit on the next refresh.
|
|
20
|
+
/**
|
|
21
|
+
* Per-provider, per-tier model defaults, each overridable by its own env var.
|
|
22
|
+
*
|
|
23
|
+
* A function rather than a constant because the env is read at call time:
|
|
24
|
+
* `createOrchestrator()` may be constructed before a host has finished loading
|
|
25
|
+
* its `.env`, and tests set these variables per-case.
|
|
26
|
+
*/
|
|
27
|
+
export const defaultModelLookup = () => ({
|
|
28
|
+
openai: {
|
|
29
|
+
fast: process.env.OPENAI_MODEL_FAST ?? "gpt-5.4-nano",
|
|
30
|
+
balanced: process.env.OPENAI_MODEL_BALANCED ?? "gpt-5.6-terra",
|
|
31
|
+
reasoning: process.env.OPENAI_MODEL_REASONING ?? "gpt-5.6-terra",
|
|
32
|
+
codex: process.env.OPENAI_MODEL_CODEX ?? "gpt-5.3-codex"
|
|
33
|
+
},
|
|
34
|
+
anthropic: {
|
|
35
|
+
fast: process.env.ANTHROPIC_MODEL_FAST ?? "claude-haiku-4-5-20251001",
|
|
36
|
+
balanced: process.env.ANTHROPIC_MODEL_BALANCED ?? "claude-sonnet-5",
|
|
37
|
+
reasoning: process.env.ANTHROPIC_MODEL_REASONING ?? "claude-sonnet-5",
|
|
38
|
+
codex: process.env.ANTHROPIC_MODEL_CODEX ?? "claude-opus-4-8"
|
|
39
|
+
},
|
|
40
|
+
gemini: {
|
|
41
|
+
fast: process.env.GOOGLE_GENAI_MODEL_FAST ?? "gemini-3.5-flash-lite",
|
|
42
|
+
balanced: process.env.GOOGLE_GENAI_MODEL_BALANCED ?? "gemini-3.6-flash",
|
|
43
|
+
// No GA Gemini 3 Pro yet (3.1 Pro is preview-only), so the Pro tiers stay on 2.5 Pro.
|
|
44
|
+
reasoning: process.env.GOOGLE_GENAI_MODEL_REASONING ?? "gemini-2.5-pro",
|
|
45
|
+
codex: process.env.GOOGLE_GENAI_MODEL_CODEX ?? "gemini-2.5-pro"
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
/**
|
|
49
|
+
* The providers this process has credentials for, in preference order.
|
|
50
|
+
*
|
|
51
|
+
* Order is load-bearing: the chat pipeline falls back to `availableProviders[0]`
|
|
52
|
+
* when a request names no provider.
|
|
53
|
+
*/
|
|
54
|
+
export const defaultProviders = () => [
|
|
55
|
+
...(process.env.OPENAI_API_KEY ? ["openai"] : []),
|
|
56
|
+
...(process.env.ANTHROPIC_API_KEY ? ["anthropic"] : []),
|
|
57
|
+
...(process.env.GOOGLE_GENAI_API_KEY ? ["gemini"] : [])
|
|
58
|
+
];
|
package/dist/chat/planner.js
CHANGED
|
@@ -23,6 +23,7 @@ import { extractJsonObject, inferBlockTypeFromText, normalizePlanCandidate, repa
|
|
|
23
23
|
import { extractUsage, ZERO_USAGE } from "../telemetry/usage.js";
|
|
24
24
|
import { intentJsonSchema } from "./plan-json-schema.js";
|
|
25
25
|
import { PlannerError, isPlannerError } from "../errors.js";
|
|
26
|
+
import { envFlag } from "../env-flags.js";
|
|
26
27
|
// Re-export for backward compatibility
|
|
27
28
|
export { PlannerError, isPlannerError };
|
|
28
29
|
/** @deprecated Use `PlannerError` instead. */
|
|
@@ -73,7 +74,7 @@ function extractResponsesOutcome(response) {
|
|
|
73
74
|
return { refusal: null, incomplete: false };
|
|
74
75
|
}
|
|
75
76
|
export function isChatStrictPrimaryOpMode() {
|
|
76
|
-
return
|
|
77
|
+
return envFlag("CHAT_STRICT_PRIMARY_OP_MODE", false);
|
|
77
78
|
}
|
|
78
79
|
export function isPageWideTranslationRequest(message) {
|
|
79
80
|
const lower = message.toLowerCase();
|
|
@@ -97,10 +98,10 @@ const EDIT_PLAN_SHAPE_HINT = {
|
|
|
97
98
|
suggested_next_actions: ["string (optional, 2-4 items)"]
|
|
98
99
|
};
|
|
99
100
|
export function isStrictJsonResponseEnabled() {
|
|
100
|
-
return
|
|
101
|
+
return envFlag("CHAT_STRICT_JSON_RESPONSE", false);
|
|
101
102
|
}
|
|
102
103
|
export function isAdaptiveSchemaContextEnabled() {
|
|
103
|
-
return
|
|
104
|
+
return envFlag("CHAT_ADAPTIVE_SCHEMA_CONTEXT", false);
|
|
104
105
|
}
|
|
105
106
|
function schemaBudgetBytes() {
|
|
106
107
|
const parsed = Number(process.env.CHAT_SCHEMA_BUDGET_BYTES ?? "9000");
|
|
@@ -1,6 +1,26 @@
|
|
|
1
1
|
import type { EditPlan, PageDoc } from "@avocadostudio-ai/shared";
|
|
2
2
|
import type { plannerContextPack } from "../nlp/deterministic-planner.ts";
|
|
3
3
|
import type { CommonGeneratePlanArgs, GeneratePlanResult } from "./planner-types.ts";
|
|
4
|
+
/**
|
|
5
|
+
* Full-page translation, split into independent per-block-group requests.
|
|
6
|
+
*
|
|
7
|
+
* One request for the whole page is the wrong shape for this job. A translation
|
|
8
|
+
* re-emits every string on the page, so its *output* length scales with the page
|
|
9
|
+
* while a single request's output is strictly serial: an 8 KB page cost 9,139
|
|
10
|
+
* output tokens and 75 seconds of streaming, and a page twice that size costs
|
|
11
|
+
* twice as long. Worse, the whole thing is one all-or-nothing unit — overshoot
|
|
12
|
+
* the token ceiling on the last block and the other six are discarded too.
|
|
13
|
+
*
|
|
14
|
+
* The blocks are independent, though. Nothing in translating the FAQ depends on
|
|
15
|
+
* how the hero was translated, so the work fans out: N smaller requests in
|
|
16
|
+
* parallel, each carrying only its own blocks, merged back into one plan. Wall
|
|
17
|
+
* clock becomes the slowest chunk rather than the sum, each chunk is far from
|
|
18
|
+
* any token ceiling, and a chunk that does fail costs only its own blocks.
|
|
19
|
+
*
|
|
20
|
+
* The tradeoff is input tokens — the schema contract and site context repeat per
|
|
21
|
+
* chunk — so each chunk carries only its own slice of the page outline, and the
|
|
22
|
+
* whole path is gated on the page being big enough to be worth it.
|
|
23
|
+
*/
|
|
4
24
|
export declare function translationChunkingConfig(): {
|
|
5
25
|
enabled: boolean;
|
|
6
26
|
/**
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { buildFullPageTranslationChecklist } from "./chat-pipeline-translation.js";
|
|
2
2
|
import { ZERO_USAGE } from "../telemetry/usage.js";
|
|
3
|
+
import { envFlag, envInt } from "../env-flags.js";
|
|
3
4
|
/**
|
|
4
5
|
* Full-page translation, split into independent per-block-group requests.
|
|
5
6
|
*
|
|
@@ -23,16 +24,9 @@ import { ZERO_USAGE } from "../telemetry/usage.js";
|
|
|
23
24
|
// ---------------------------------------------------------------------------
|
|
24
25
|
// Configuration
|
|
25
26
|
// ---------------------------------------------------------------------------
|
|
26
|
-
function envFlagOff(raw) {
|
|
27
|
-
return /^(0|false|no|off)$/i.test((raw ?? "").trim());
|
|
28
|
-
}
|
|
29
|
-
function envInt(raw, fallback) {
|
|
30
|
-
const parsed = Number((raw ?? "").trim());
|
|
31
|
-
return Number.isFinite(parsed) && parsed > 0 ? Math.floor(parsed) : fallback;
|
|
32
|
-
}
|
|
33
27
|
export function translationChunkingConfig() {
|
|
34
28
|
return {
|
|
35
|
-
enabled:
|
|
29
|
+
enabled: envFlag("CHAT_TRANSLATION_CHUNKING", true),
|
|
36
30
|
/**
|
|
37
31
|
* Translatable bytes to aim for per chunk.
|
|
38
32
|
*
|
|
@@ -43,12 +37,12 @@ export function translationChunkingConfig() {
|
|
|
43
37
|
* cost, which is where splitting further stops buying much and starts paying
|
|
44
38
|
* another chunk's input tokens for nothing.
|
|
45
39
|
*/
|
|
46
|
-
targetBytes: envInt(
|
|
40
|
+
targetBytes: envInt("CHAT_TRANSLATION_CHUNK_BYTES", 900),
|
|
47
41
|
/** Upper bound on parallel planner calls — one wave, not a stampede. */
|
|
48
|
-
maxChunks: envInt(
|
|
42
|
+
maxChunks: envInt("CHAT_TRANSLATION_MAX_CHUNKS", 6),
|
|
49
43
|
/** Below these, a single request is already fast and chunking only adds input cost. */
|
|
50
|
-
minBlocks: envInt(
|
|
51
|
-
minBytes: envInt(
|
|
44
|
+
minBlocks: envInt("CHAT_TRANSLATION_CHUNK_MIN_BLOCKS", 3),
|
|
45
|
+
minBytes: envInt("CHAT_TRANSLATION_CHUNK_MIN_BYTES", 1800)
|
|
52
46
|
};
|
|
53
47
|
}
|
|
54
48
|
/**
|
package/dist/demo-mode.js
CHANGED
|
@@ -22,16 +22,10 @@
|
|
|
22
22
|
// and costs bounded.
|
|
23
23
|
import { createHash } from "node:crypto";
|
|
24
24
|
import { OperationError } from "./errors.js";
|
|
25
|
+
import { envFlag } from "./env-flags.js";
|
|
25
26
|
// ---------------------------------------------------------------------------
|
|
26
27
|
// Config — read once at module load, memoized
|
|
27
28
|
// ---------------------------------------------------------------------------
|
|
28
|
-
const TRUE_VALUES = new Set(["1", "true", "yes", "on"]);
|
|
29
|
-
function envFlag(name, fallback = false) {
|
|
30
|
-
const raw = process.env[name];
|
|
31
|
-
if (raw === undefined)
|
|
32
|
-
return fallback;
|
|
33
|
-
return TRUE_VALUES.has(raw.trim().toLowerCase());
|
|
34
|
-
}
|
|
35
29
|
function envList(name, fallback) {
|
|
36
30
|
const raw = process.env[name];
|
|
37
31
|
if (!raw)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `process.env` wrappers over the shared flag table.
|
|
3
|
+
*
|
|
4
|
+
* The table itself lives in `@avocadostudio-ai/shared` because the editor reads
|
|
5
|
+
* the same flags out of `import.meta.env` and `localStorage` — see that module's
|
|
6
|
+
* header for why there was more than one of it.
|
|
7
|
+
*
|
|
8
|
+
* All three read the environment at call time, never at module load. A flag
|
|
9
|
+
* captured at import is a flag a test cannot set and a library-mode host cannot
|
|
10
|
+
* configure after its own `.env` has loaded, and several of these already had to
|
|
11
|
+
* be functions for exactly that reason.
|
|
12
|
+
*/
|
|
13
|
+
/** `true`/`false` for a flag with a documented default. */
|
|
14
|
+
export declare function envFlag(name: string, fallback: boolean): boolean;
|
|
15
|
+
/**
|
|
16
|
+
* `undefined` when the operator said nothing, `false` when they said anything
|
|
17
|
+
* that is not a recognised "on". Fail-closed — see `parseFlagExplicit`.
|
|
18
|
+
*/
|
|
19
|
+
export declare function envFlagExplicit(name: string): boolean | undefined;
|
|
20
|
+
/** A positive integer, or `fallback`. */
|
|
21
|
+
export declare function envInt(name: string, fallback: number): number;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `process.env` wrappers over the shared flag table.
|
|
3
|
+
*
|
|
4
|
+
* The table itself lives in `@avocadostudio-ai/shared` because the editor reads
|
|
5
|
+
* the same flags out of `import.meta.env` and `localStorage` — see that module's
|
|
6
|
+
* header for why there was more than one of it.
|
|
7
|
+
*
|
|
8
|
+
* All three read the environment at call time, never at module load. A flag
|
|
9
|
+
* captured at import is a flag a test cannot set and a library-mode host cannot
|
|
10
|
+
* configure after its own `.env` has loaded, and several of these already had to
|
|
11
|
+
* be functions for exactly that reason.
|
|
12
|
+
*/
|
|
13
|
+
import { parseFlag, parseFlagExplicit, parseIntFlag } from "@avocadostudio-ai/shared";
|
|
14
|
+
/** `true`/`false` for a flag with a documented default. */
|
|
15
|
+
export function envFlag(name, fallback) {
|
|
16
|
+
return parseFlag(process.env[name], fallback);
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* `undefined` when the operator said nothing, `false` when they said anything
|
|
20
|
+
* that is not a recognised "on". Fail-closed — see `parseFlagExplicit`.
|
|
21
|
+
*/
|
|
22
|
+
export function envFlagExplicit(name) {
|
|
23
|
+
return parseFlagExplicit(process.env[name]);
|
|
24
|
+
}
|
|
25
|
+
/** A positive integer, or `fallback`. */
|
|
26
|
+
export function envInt(name, fallback) {
|
|
27
|
+
return parseIntFlag(process.env[name], fallback);
|
|
28
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { type AIProvider
|
|
1
|
+
import { type AIProvider } from "../state/session-state.ts";
|
|
2
|
+
import { type ModelLookup } from "../chat/model-defaults.ts";
|
|
2
3
|
import { type ScreenshotCapture } from "../http/screenshot-actions.ts";
|
|
3
4
|
import { type Logger } from "../logger.ts";
|
|
4
5
|
import type { CmsAdapter, CmsCapabilities } from "../cms/adapter.ts";
|
|
@@ -8,8 +9,16 @@ export interface CreateOrchestratorConfig {
|
|
|
8
9
|
/**
|
|
9
10
|
* Provider model overrides. Defaults to env vars (OPENAI_MODEL_*, ANTHROPIC_MODEL_*,
|
|
10
11
|
* GOOGLE_GENAI_MODEL_*). Pass an explicit object to override per-tier model names.
|
|
12
|
+
*
|
|
13
|
+
* To change one tier and keep the rest current, spread the defaults rather
|
|
14
|
+
* than retyping the table — `defaultModelLookup` is exported for this:
|
|
15
|
+
*
|
|
16
|
+
* ```ts
|
|
17
|
+
* const base = defaultModelLookup()
|
|
18
|
+
* modelLookup: { ...base, openai: { ...base.openai, reasoning: "o5" } }
|
|
19
|
+
* ```
|
|
11
20
|
*/
|
|
12
|
-
modelLookup?:
|
|
21
|
+
modelLookup?: ModelLookup;
|
|
13
22
|
/**
|
|
14
23
|
* Override available providers. Defaults to whichever API keys are present in
|
|
15
24
|
* process.env (OPENAI_API_KEY / ANTHROPIC_API_KEY / GOOGLE_GENAI_API_KEY).
|
|
@@ -30,7 +30,9 @@ import { ResumableStreamStore, runResumableChatStream, TooManyPendingStreamsErro
|
|
|
30
30
|
import { runChatPipeline, collectMentionedSlugsFromOps } from "../chat/chat-pipeline.js";
|
|
31
31
|
import { createChatTelemetryStore } from "../telemetry/chat-telemetry.js";
|
|
32
32
|
import { createToolRuntime } from "../tools/runtime.js";
|
|
33
|
+
import { envFlag } from "../env-flags.js";
|
|
33
34
|
import { loadStateFromDisk, scopedSessionKey, getSessionPages, getPage, getSiteConfig, setSiteConfig, pushUndo, bumpVersion, pushRecentEdit, pushVersionEntry, schedulePersistState, normalizeSiteId, publishStatusBySession, pushPublishLogEntry, persistenceHealth, persistenceWarning } from "../state/session-state.js";
|
|
35
|
+
import { defaultModelLookup, defaultProviders } from "../chat/model-defaults.js";
|
|
34
36
|
import { historyStatus, historyLog, historyUndoAction, historyRedoAction, historyRestoreAction, historyDiscardAction } from "../http/history-actions.js";
|
|
35
37
|
import { whoamiAction } from "../http/session-actions.js";
|
|
36
38
|
import { blocksManifestAction } from "../http/blocks-actions.js";
|
|
@@ -55,31 +57,6 @@ import { isAccessGateEnabled, mintAccessToken, verifyAccessPassword } from "../h
|
|
|
55
57
|
import { declareLibraryMount, observeLibraryMount } from "./library-mount.js";
|
|
56
58
|
import { checkAuth, resolveAuth, CLOSED_HINT } from "./auth.js";
|
|
57
59
|
import { setSiteAssetLister, invalidateSiteAssets } from "../state/site-assets.js";
|
|
58
|
-
const defaultModelLookup = () => ({
|
|
59
|
-
openai: {
|
|
60
|
-
fast: process.env.OPENAI_MODEL_FAST ?? "gpt-4o-mini",
|
|
61
|
-
balanced: process.env.OPENAI_MODEL_BALANCED ?? "gpt-4o",
|
|
62
|
-
reasoning: process.env.OPENAI_MODEL_REASONING ?? "o1",
|
|
63
|
-
codex: process.env.OPENAI_MODEL_CODEX ?? "o3"
|
|
64
|
-
},
|
|
65
|
-
anthropic: {
|
|
66
|
-
fast: process.env.ANTHROPIC_MODEL_FAST ?? "claude-haiku-4-5-20251001",
|
|
67
|
-
balanced: process.env.ANTHROPIC_MODEL_BALANCED ?? "claude-sonnet-5",
|
|
68
|
-
reasoning: process.env.ANTHROPIC_MODEL_REASONING ?? "claude-sonnet-5",
|
|
69
|
-
codex: process.env.ANTHROPIC_MODEL_CODEX ?? "claude-opus-4-8"
|
|
70
|
-
},
|
|
71
|
-
gemini: {
|
|
72
|
-
fast: process.env.GOOGLE_GENAI_MODEL_FAST ?? "gemini-2.5-flash",
|
|
73
|
-
balanced: process.env.GOOGLE_GENAI_MODEL_BALANCED ?? "gemini-2.5-flash",
|
|
74
|
-
reasoning: process.env.GOOGLE_GENAI_MODEL_REASONING ?? "gemini-2.5-pro",
|
|
75
|
-
codex: process.env.GOOGLE_GENAI_MODEL_CODEX ?? "gemini-2.5-pro"
|
|
76
|
-
}
|
|
77
|
-
});
|
|
78
|
-
const defaultProviders = () => [
|
|
79
|
-
...(process.env.OPENAI_API_KEY ? ["openai"] : []),
|
|
80
|
-
...(process.env.ANTHROPIC_API_KEY ? ["anthropic"] : []),
|
|
81
|
-
...(process.env.GOOGLE_GENAI_API_KEY ? ["gemini"] : [])
|
|
82
|
-
];
|
|
83
60
|
async function buildRuntime(config) {
|
|
84
61
|
const log = config.logger ?? consoleLogger();
|
|
85
62
|
// Register host-app block schemas BEFORE the planner ever validates ops.
|
|
@@ -97,7 +74,7 @@ async function buildRuntime(config) {
|
|
|
97
74
|
const chatTelemetry = createChatTelemetryStore({
|
|
98
75
|
filePath: process.env.CHAT_TELEMETRY_FILE ?? "./.data/chat-telemetry.ndjson",
|
|
99
76
|
limit: Number(process.env.CHAT_TELEMETRY_LIMIT ?? 500),
|
|
100
|
-
persistEnabled:
|
|
77
|
+
persistEnabled: envFlag("CHAT_TELEMETRY_PERSIST", true),
|
|
101
78
|
logger: log
|
|
102
79
|
});
|
|
103
80
|
const toolRuntime = await createToolRuntime({ logger: log });
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
export { createOrchestrator, type CreateOrchestratorConfig, type OrchestratorHandler } from "./handler/create-orchestrator.ts";
|
|
2
2
|
export type { OrchestratorAuth, AuthContext } from "./handler/auth.ts";
|
|
3
|
+
export { envFlag, envFlagExplicit, envInt } from "./env-flags.ts";
|
|
4
|
+
export { defaultModelLookup, defaultProviders, type ModelLookup } from "./chat/model-defaults.ts";
|
|
3
5
|
export type { CmsAdapter, CmsCapabilities, CmsInlineAsset, CmsPublishContext, CmsPublishResult, CmsPerspective, CmsReadOptions, CmsMediaItem, CmsMediaPage, CmsMediaQuery, CmsMediaUpload, ResolvedCapabilities } from "./cms/adapter.ts";
|
|
4
6
|
export { jsonFileAdapter, editorApiAdapter, resolveCapabilities, cmsMediaSource, cmsMediaUploader, cmsMediaLabel, type JsonFileAdapterOptions, type EditorApiAdapterOptions, type CmsMediaSource, type CmsMediaUploader, type CmsMediaSourceConfig } from "./cms/index.ts";
|
|
5
7
|
export { registerPublishTarget, selectPublishTarget, getPublishTarget, listPublishTargets } from "./publish/publish-target-registry.ts";
|
package/dist/index.js
CHANGED
|
@@ -18,6 +18,15 @@
|
|
|
18
18
|
//
|
|
19
19
|
// The Fastify HTTP wrapper lives in apps/orchestrator and imports from here.
|
|
20
20
|
export { createOrchestrator } from "./handler/create-orchestrator.js";
|
|
21
|
+
// Flag parsing, so a library-mode host reads its own options the way the
|
|
22
|
+
// orchestrator reads its own — one truth table for "1"/"true"/"yes"/"on".
|
|
23
|
+
export { envFlag, envFlagExplicit, envInt } from "./env-flags.js";
|
|
24
|
+
// The model defaults `createOrchestrator()` applies when `config.modelLookup`
|
|
25
|
+
// is omitted. Public because the sensible way to override one tier is to spread
|
|
26
|
+
// the defaults and replace that entry — which a consumer cannot do if the table
|
|
27
|
+
// they are overriding is unreachable, leaving them to retype all twelve models
|
|
28
|
+
// and inherit none of the next refresh.
|
|
29
|
+
export { defaultModelLookup, defaultProviders } from "./chat/model-defaults.js";
|
|
21
30
|
export { jsonFileAdapter, editorApiAdapter, resolveCapabilities, cmsMediaSource, cmsMediaUploader, cmsMediaLabel } from "./cms/index.js";
|
|
22
31
|
// The publish-target plugin point. `docs-site/integration/publishing.mdx` has
|
|
23
32
|
// documented this as the way to publish somewhere we do not ship a target for
|
|
@@ -174,50 +174,37 @@ export function postEditSuggestions(args) {
|
|
|
174
174
|
return;
|
|
175
175
|
suggestions.push(value);
|
|
176
176
|
};
|
|
177
|
-
|
|
178
|
-
|
|
177
|
+
/*
|
|
178
|
+
* No bare "Edit <thing>" chips.
|
|
179
|
+
*
|
|
180
|
+
* This branch used to emit "Edit heading", "Edit CTA", "Edit Banner",
|
|
181
|
+
* "Edit cards" after every edit. Clicking one sends that literal string to
|
|
182
|
+
* the planner, which knows the field and not the value — so the only possible
|
|
183
|
+
* answer is "edit it to what?". Three clean-room runs measured the same
|
|
184
|
+
* outcome: **zero operations**, and on a keyed plan about $0.012 and five
|
|
185
|
+
* seconds to be asked a question the chip existed to avoid.
|
|
186
|
+
*
|
|
187
|
+
* There is no repair for the shape. A suggestion is a sentence the product is
|
|
188
|
+
* offering to run, and "change this field to something" is not a sentence
|
|
189
|
+
* anybody can run — only the person typing knows the something. The chips
|
|
190
|
+
* that survive below are the ones that name a complete action: add a section,
|
|
191
|
+
* add another item, generate an image.
|
|
192
|
+
*
|
|
193
|
+
* The `isImageUpdate` special-casing went with them; it chose *which*
|
|
194
|
+
* underspecified chip to show, which was never the problem.
|
|
195
|
+
*/
|
|
179
196
|
for (const op of plan.ops) {
|
|
180
|
-
if (op.op === "
|
|
181
|
-
const block = current.blocks.find((b) => b.id === op.blockId);
|
|
182
|
-
if (block) {
|
|
183
|
-
if (isImageUpdate) {
|
|
184
|
-
// After image updates, suggest content edits relevant to the block
|
|
185
|
-
const blockType = block.type;
|
|
186
|
-
if (blockType === "CardGrid" || blockType === "Card") {
|
|
187
|
-
pushUniqueSuggestion("Edit cards");
|
|
188
|
-
pushUniqueSuggestion("Update links");
|
|
189
|
-
}
|
|
190
|
-
else if (blockType === "FeatureGrid") {
|
|
191
|
-
pushUniqueSuggestion("Edit features");
|
|
192
|
-
}
|
|
193
|
-
else if (blockType === "Hero") {
|
|
194
|
-
pushUniqueSuggestion("Edit heading");
|
|
195
|
-
pushUniqueSuggestion("Edit CTA");
|
|
196
|
-
}
|
|
197
|
-
else {
|
|
198
|
-
pushUniqueSuggestion(`Edit ${blockType}`);
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
else {
|
|
202
|
-
const patchKeys = Object.keys(op.patch);
|
|
203
|
-
const otherKeys = editablePropsFromBlock(block)
|
|
204
|
-
.filter((k) => !patchKeys.includes(k) && !NON_CONTENT_PROP_KEYS.has(k));
|
|
205
|
-
for (const key of otherKeys.slice(0, 2)) {
|
|
206
|
-
pushUniqueSuggestion(promptFromPropKey(key, block.type));
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
else if (op.op === "create_page") {
|
|
197
|
+
if (op.op === "create_page") {
|
|
212
198
|
pushUniqueSuggestion("Generate image");
|
|
213
199
|
}
|
|
214
200
|
else if (op.op === "add_block") {
|
|
201
|
+
// Only the curated, complete follow-ups. A block with none falls through
|
|
202
|
+
// to the forward-looking section nudges below rather than to
|
|
203
|
+
// `Edit <Type>`, which is the same dead end as the branch above.
|
|
215
204
|
const followups = ADD_BLOCK_FOLLOWUPS[op.block.type];
|
|
216
205
|
if (followups)
|
|
217
206
|
for (const f of followups)
|
|
218
207
|
pushUniqueSuggestion(f);
|
|
219
|
-
else
|
|
220
|
-
pushUniqueSuggestion(`Edit ${op.block.type}`);
|
|
221
208
|
}
|
|
222
209
|
else if (op.op === "remove_block") {
|
|
223
210
|
if (!hasType("CTA"))
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@avocadostudio-ai/orchestrator-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
"./package.json": "./package.json",
|
|
@@ -22,8 +22,8 @@
|
|
|
22
22
|
"openai": "^4.87.1",
|
|
23
23
|
"sharp": "^0.35.4",
|
|
24
24
|
"zod": "^4.3.6",
|
|
25
|
-
"@avocadostudio-ai/migration-sdk": "^0.
|
|
26
|
-
"@avocadostudio-ai/shared": "^0.
|
|
25
|
+
"@avocadostudio-ai/migration-sdk": "^0.12.0",
|
|
26
|
+
"@avocadostudio-ai/shared": "^0.12.0"
|
|
27
27
|
},
|
|
28
28
|
"devDependencies": {
|
|
29
29
|
"@anthropic-ai/claude-agent-sdk": "^0.3.220",
|