@avocadostudio-ai/orchestrator-core 0.11.5 → 0.11.7
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.
|
@@ -2998,7 +2998,7 @@ export async function runChatPipeline(ctx, body, options) {
|
|
|
2998
2998
|
markPlanningStart();
|
|
2999
2999
|
try {
|
|
3000
3000
|
emitStatusTone("planning");
|
|
3001
|
-
const demoPlan = withKeylessNotice(demoPlanFromMessageImpl(plannerMessage, effectiveSlug, planningActiveBlockId, body.activeBlockType), keyFileHint());
|
|
3001
|
+
const demoPlan = withKeylessNotice(demoPlanFromMessageImpl(plannerMessage, effectiveSlug, planningActiveBlockId, body.activeBlockType, current), keyFileHint());
|
|
3002
3002
|
markPlanningFinish();
|
|
3003
3003
|
const outcome = await respondFromPlan(demoPlan, "demo", applyMode, undefined, "demo");
|
|
3004
3004
|
if (outcome.done)
|
|
@@ -21,7 +21,7 @@ import { mkdir, writeFile, readFile } from "node:fs/promises";
|
|
|
21
21
|
import { resolve, basename } from "node:path";
|
|
22
22
|
import { randomUUID } from "node:crypto";
|
|
23
23
|
import { z } from "zod";
|
|
24
|
-
import { operationSchema, blockManifestSchema, siteConfigSchema, declareBlockCatalogue, undeclaredBlockTypes, EDITOR_PROTOCOL_VERSION, withLoopbackSpellings } from "@avocadostudio-ai/shared";
|
|
24
|
+
import { operationSchema, blockManifestSchema, siteConfigSchema, declareBlockCatalogue, undeclaredBlockTypes, EDITOR_PROTOCOL_VERSION, withLoopbackSpellings, editorAllowHeaders } from "@avocadostudio-ai/shared";
|
|
25
25
|
import { keyFileHint } from "../env-file-location.js";
|
|
26
26
|
import { chatRequestBodySchema } from "../nlp/intent-detection.js";
|
|
27
27
|
import { applyOpsAtomically, pickFocusBlockId, pickUpdatedSlug, toErrorDetail, classifyGuardrailError } from "../ops/ops-engine.js";
|
|
@@ -536,7 +536,7 @@ export function createOrchestrator(config = {}) {
|
|
|
536
536
|
* would have been unreachable from the very client it is for.
|
|
537
537
|
*/
|
|
538
538
|
"access-control-allow-methods": "GET, POST, PUT, PATCH, DELETE, OPTIONS",
|
|
539
|
-
"access-control-allow-headers":
|
|
539
|
+
"access-control-allow-headers": editorAllowHeaders(),
|
|
540
540
|
"access-control-max-age": "600"
|
|
541
541
|
}
|
|
542
542
|
});
|
|
@@ -1158,6 +1158,23 @@ export function createOrchestrator(config = {}) {
|
|
|
1158
1158
|
* editor offers the picker's CMS tab on it, so silence hides the
|
|
1159
1159
|
* tab rather than opening one onto a method nobody implemented. */
|
|
1160
1160
|
cmsMedia: runtime.capabilities.readsMedia,
|
|
1161
|
+
/*
|
|
1162
|
+
* Whether `/audio/transcribe` can actually answer.
|
|
1163
|
+
*
|
|
1164
|
+
* This key was simply absent from library mode's feature list while
|
|
1165
|
+
* the standalone server has always reported it, and the editor's
|
|
1166
|
+
* check is `audioTranscription !== false` — so `undefined` read as
|
|
1167
|
+
* *available* and the microphone shipped visible in every
|
|
1168
|
+
* scaffolded project, including with no key configured at all.
|
|
1169
|
+
* Pressing it records audio and posts it to a route that can only
|
|
1170
|
+
* answer 503.
|
|
1171
|
+
*
|
|
1172
|
+
* The same providers as the standalone server, for the same reason:
|
|
1173
|
+
* transcription tries OpenAI first and falls back to Gemini, so
|
|
1174
|
+
* either key enables it and neither means the control must not be
|
|
1175
|
+
* offered.
|
|
1176
|
+
*/
|
|
1177
|
+
audioTranscription: Boolean(process.env.OPENAI_API_KEY?.trim() || process.env.GOOGLE_GENAI_API_KEY?.trim()),
|
|
1161
1178
|
agentMode: false
|
|
1162
1179
|
},
|
|
1163
1180
|
/*
|
|
@@ -19,7 +19,15 @@ export declare function postEditSuggestions(args: {
|
|
|
19
19
|
current: PageDoc;
|
|
20
20
|
body: ChatRequestBody;
|
|
21
21
|
}): string[];
|
|
22
|
-
export declare function demoPlanFromMessage(message: string, slug: string, activeBlockId?: string, activeBlockType?: string
|
|
22
|
+
export declare function demoPlanFromMessage(message: string, slug: string, activeBlockId?: string, activeBlockType?: string,
|
|
23
|
+
/**
|
|
24
|
+
* The page being edited, so a heading edit can find its own target.
|
|
25
|
+
*
|
|
26
|
+
* Without it the heading branch below could only act on an already-selected
|
|
27
|
+
* block, and a first-run user's first typed sentence is almost never sent
|
|
28
|
+
* with anything selected. See the comment there.
|
|
29
|
+
*/
|
|
30
|
+
page?: PageDoc): EditPlan;
|
|
23
31
|
export declare function titleCaseSentence(text: string): string;
|
|
24
32
|
export declare function pageMetaContractSummary(): {
|
|
25
33
|
op: string;
|
|
@@ -75,20 +83,29 @@ export declare function blockContractsSummary(manifest?: BlockManifest): Record<
|
|
|
75
83
|
*/
|
|
76
84
|
export declare const KEYLESS_PLANNER_NOTICE: string;
|
|
77
85
|
/**
|
|
78
|
-
* The same notice,
|
|
86
|
+
* The same notice, without the *path*.
|
|
87
|
+
*
|
|
88
|
+
* An earlier version interpolated the absolute path of `.env.local` into the
|
|
89
|
+
* sentence. Correct, and it read appallingly: an 80-character path mid-
|
|
90
|
+
* paragraph, wrapping across four lines of a narrow pane, so a two-line
|
|
91
|
+
* explanation became a wall in which the actual advice was the least visible
|
|
92
|
+
* part — and it duplicated, word for word, the notice sitting directly below
|
|
93
|
+
* the conversation.
|
|
79
94
|
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
95
|
+
* The path belongs in exactly one place: the notice, where it is a selectable
|
|
96
|
+
* code block rather than prose. The variable names stay here, though, and a
|
|
97
|
+
* first attempt at removing those was wrong — `library-demo-identity.test.ts`
|
|
98
|
+
* pins them, and it is right to. **The notice is dismissible.** A message that
|
|
99
|
+
* says "see the notice below" is a dangling reference the moment someone closes
|
|
100
|
+
* it, and chat messages are also read in transcripts and telemetry where no
|
|
101
|
+
* notice exists. So this sentence has to stand alone; it just does not have to
|
|
102
|
+
* carry a filesystem path to do it.
|
|
85
103
|
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
* `keyFileHint`.
|
|
104
|
+
* `envFilePath` is still accepted so callers need not know any of this. It
|
|
105
|
+
* changes nothing today and keeps the seam if a surface without the notice ever
|
|
106
|
+
* needs the long form.
|
|
90
107
|
*/
|
|
91
|
-
export declare function keylessPlannerNotice(
|
|
108
|
+
export declare function keylessPlannerNotice(_envFilePath?: string): string;
|
|
92
109
|
export declare function withKeylessNotice(plan: EditPlan, envFilePath?: string): EditPlan;
|
|
93
110
|
/**
|
|
94
111
|
* Drop the suggestions this planner could not act on.
|
|
@@ -233,7 +233,15 @@ export function postEditSuggestions(args) {
|
|
|
233
233
|
pushUniqueSuggestion("Add Stats");
|
|
234
234
|
return suggestions.slice(0, 4);
|
|
235
235
|
}
|
|
236
|
-
export function demoPlanFromMessage(message, slug, activeBlockId, activeBlockType
|
|
236
|
+
export function demoPlanFromMessage(message, slug, activeBlockId, activeBlockType,
|
|
237
|
+
/**
|
|
238
|
+
* The page being edited, so a heading edit can find its own target.
|
|
239
|
+
*
|
|
240
|
+
* Without it the heading branch below could only act on an already-selected
|
|
241
|
+
* block, and a first-run user's first typed sentence is almost never sent
|
|
242
|
+
* with anything selected. See the comment there.
|
|
243
|
+
*/
|
|
244
|
+
page) {
|
|
237
245
|
const lower = message.toLowerCase();
|
|
238
246
|
const quoted = /"([^"]+)"/.exec(message)?.[1];
|
|
239
247
|
// SEO metadata patterns — checked early so "seo title" isn't mistaken for hero heading
|
|
@@ -334,25 +342,55 @@ export function demoPlanFromMessage(message, slug, activeBlockId, activeBlockTyp
|
|
|
334
342
|
]
|
|
335
343
|
};
|
|
336
344
|
}
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
345
|
+
/*
|
|
346
|
+
* "Change the headline to X" — the single most likely first message, and it
|
|
347
|
+
* used to be refused twice over.
|
|
348
|
+
*
|
|
349
|
+
* The notice on the first screen says *"Only simple, literal edits work"*, and
|
|
350
|
+
* a literal assignment to the thing at the top of the page is the most
|
|
351
|
+
* literal edit there is. It failed for two independent reasons:
|
|
352
|
+
*
|
|
353
|
+
* 1. **"headline" was not a keyword.** The test was `title || heading`, and
|
|
354
|
+
* "headline" contains neither. The word the product's own suggestion chips
|
|
355
|
+
* use — "Rewrite the hero headline" — did not match.
|
|
356
|
+
* 2. **It required a selection.** `activeBlockId && activeBlockType === "Hero"`
|
|
357
|
+
* means a block must already be selected, and a user's *typed* first
|
|
358
|
+
* message is almost never sent with anything selected — especially since
|
|
359
|
+
* click-to-select defaults off. So the path was reachable only after
|
|
360
|
+
* discovering an unlabelled toggle and clicking the exact block.
|
|
361
|
+
*
|
|
362
|
+
* Now it resolves its own target: the selected block when there is one,
|
|
363
|
+
* otherwise the first block on the page carrying a `heading` prop. A quoted
|
|
364
|
+
* value is required when nothing is selected, because without quotes the
|
|
365
|
+
* stripping below is guesswork and this planner's whole contract is that it
|
|
366
|
+
* only does the unambiguous thing.
|
|
367
|
+
*/
|
|
368
|
+
const mentionsHeading = lower.includes("title") || lower.includes("heading") || lower.includes("headline");
|
|
369
|
+
if (mentionsHeading) {
|
|
370
|
+
const selectedIsHeadingBlock = Boolean(activeBlockId) && activeBlockType === "Hero";
|
|
371
|
+
const fallback = page?.blocks.find((b) => b.props && typeof b.props.heading === "string");
|
|
372
|
+
const targetId = selectedIsHeadingBlock ? activeBlockId : fallback?.id;
|
|
373
|
+
const headingText = selectedIsHeadingBlock
|
|
374
|
+
? quoted ??
|
|
375
|
+
message
|
|
376
|
+
.replace(/change/i, "")
|
|
377
|
+
.replace(/hero/i, "")
|
|
378
|
+
.replace(/title/i, "")
|
|
379
|
+
.replace(/heading/i, "")
|
|
380
|
+
.replace(/headline/i, "")
|
|
381
|
+
.replace(/\bto\b/i, "")
|
|
382
|
+
.trim()
|
|
383
|
+
: quoted;
|
|
384
|
+
if (targetId && headingText) {
|
|
347
385
|
return {
|
|
348
386
|
intent: "edit_plan",
|
|
349
|
-
summary_for_user: "Updated the
|
|
350
|
-
change_log: [`Changed
|
|
387
|
+
summary_for_user: "Updated the heading.",
|
|
388
|
+
change_log: [`Changed heading to "${headingText}".`],
|
|
351
389
|
ops: [
|
|
352
390
|
{
|
|
353
391
|
op: "update_props",
|
|
354
392
|
pageSlug: slug,
|
|
355
|
-
blockId:
|
|
393
|
+
blockId: targetId,
|
|
356
394
|
patch: { heading: headingText }
|
|
357
395
|
}
|
|
358
396
|
]
|
|
@@ -625,27 +663,33 @@ export function blockContractsSummary(manifest) {
|
|
|
625
663
|
* So when the rules produce no operations, say why. The suggestions are kept:
|
|
626
664
|
* they are the phrasings this planner *can* execute without a key.
|
|
627
665
|
*/
|
|
628
|
-
export const KEYLESS_PLANNER_NOTICE = "
|
|
629
|
-
"
|
|
666
|
+
export const KEYLESS_PLANNER_NOTICE = "No API key is configured, so this is the built-in demo planner, which handles only simple, literal edits. " +
|
|
667
|
+
"Set ANTHROPIC_API_KEY or OPENAI_API_KEY in your project's .env.local and restart the dev server for full chat.";
|
|
630
668
|
/**
|
|
631
|
-
* The same notice,
|
|
669
|
+
* The same notice, without the *path*.
|
|
670
|
+
*
|
|
671
|
+
* An earlier version interpolated the absolute path of `.env.local` into the
|
|
672
|
+
* sentence. Correct, and it read appallingly: an 80-character path mid-
|
|
673
|
+
* paragraph, wrapping across four lines of a narrow pane, so a two-line
|
|
674
|
+
* explanation became a wall in which the actual advice was the least visible
|
|
675
|
+
* part — and it duplicated, word for word, the notice sitting directly below
|
|
676
|
+
* the conversation.
|
|
632
677
|
*
|
|
633
|
-
*
|
|
634
|
-
*
|
|
635
|
-
*
|
|
636
|
-
*
|
|
637
|
-
*
|
|
678
|
+
* The path belongs in exactly one place: the notice, where it is a selectable
|
|
679
|
+
* code block rather than prose. The variable names stay here, though, and a
|
|
680
|
+
* first attempt at removing those was wrong — `library-demo-identity.test.ts`
|
|
681
|
+
* pins them, and it is right to. **The notice is dismissible.** A message that
|
|
682
|
+
* says "see the notice below" is a dangling reference the moment someone closes
|
|
683
|
+
* it, and chat messages are also read in transcripts and telemetry where no
|
|
684
|
+
* notice exists. So this sentence has to stand alone; it just does not have to
|
|
685
|
+
* carry a filesystem path to do it.
|
|
638
686
|
*
|
|
639
|
-
*
|
|
640
|
-
*
|
|
641
|
-
*
|
|
642
|
-
* `keyFileHint`.
|
|
687
|
+
* `envFilePath` is still accepted so callers need not know any of this. It
|
|
688
|
+
* changes nothing today and keeps the seam if a surface without the notice ever
|
|
689
|
+
* needs the long form.
|
|
643
690
|
*/
|
|
644
|
-
export function keylessPlannerNotice(
|
|
645
|
-
|
|
646
|
-
return KEYLESS_PLANNER_NOTICE;
|
|
647
|
-
return ("There's no AI key configured, so I'm running on the built-in demo planner — it only handles simple, literal edits. " +
|
|
648
|
-
`Add ANTHROPIC_API_KEY (or OPENAI_API_KEY) to ${envFilePath} and restart the dev server to chat for real.`);
|
|
691
|
+
export function keylessPlannerNotice(_envFilePath) {
|
|
692
|
+
return KEYLESS_PLANNER_NOTICE;
|
|
649
693
|
}
|
|
650
694
|
export function withKeylessNotice(plan, envFilePath) {
|
|
651
695
|
if (plan.ops.length > 0)
|
|
@@ -159,15 +159,53 @@ function diffPage(before, after) {
|
|
|
159
159
|
const a = after;
|
|
160
160
|
const blockDiffs = diffBlocks(b.blocks, a.blocks);
|
|
161
161
|
const titleChanged = b.title !== a.title;
|
|
162
|
+
const metaDiffs = diffPageMeta(b, a);
|
|
162
163
|
const hasStructuralChange = blockDiffs.some((bd) => bd.status !== "unchanged");
|
|
163
164
|
return {
|
|
164
165
|
slug: a.slug,
|
|
165
|
-
status: titleChanged || hasStructuralChange ? "modified" : "unchanged",
|
|
166
|
+
status: titleChanged || metaDiffs.length > 0 || hasStructuralChange ? "modified" : "unchanged",
|
|
166
167
|
titleBefore: titleChanged ? b.title : undefined,
|
|
167
168
|
titleAfter: titleChanged ? a.title : undefined,
|
|
169
|
+
...(metaDiffs.length > 0 ? { metaDiffs } : {}),
|
|
168
170
|
blockDiffs,
|
|
169
171
|
};
|
|
170
172
|
}
|
|
173
|
+
/**
|
|
174
|
+
* Page-level SEO fields, which the diff did not look at.
|
|
175
|
+
*
|
|
176
|
+
* A page's `meta` carries the title, description, Open Graph image and public
|
|
177
|
+
* path — everything a crawler and a social card read, and the whole content of
|
|
178
|
+
* the property panel's **SEO** section. None of it reached the publish review,
|
|
179
|
+
* so a user who edited their page title saw "1 page modified · 1 field changed"
|
|
180
|
+
* counting only the block they had also touched, and no mention of the title at
|
|
181
|
+
* all. The draft held the change; the screen that exists to show what will
|
|
182
|
+
* change did not show it.
|
|
183
|
+
*
|
|
184
|
+
* `ogImage` is typed as an image so the UI renders a thumbnail rather than a
|
|
185
|
+
* URL, matching how block image fields already behave.
|
|
186
|
+
*/
|
|
187
|
+
const PAGE_META_FIELDS = [
|
|
188
|
+
["title", "text"],
|
|
189
|
+
["description", "text"],
|
|
190
|
+
["ogImage", "image"],
|
|
191
|
+
["path", "other"]
|
|
192
|
+
];
|
|
193
|
+
function diffPageMeta(before, after) {
|
|
194
|
+
const b = (before.meta ?? {});
|
|
195
|
+
const a = (after.meta ?? {});
|
|
196
|
+
const out = [];
|
|
197
|
+
for (const [key, kind] of PAGE_META_FIELDS) {
|
|
198
|
+
const bv = b[key];
|
|
199
|
+
const av = a[key];
|
|
200
|
+
// Absent and empty are the same thing to a reader, and treating them as
|
|
201
|
+
// different produced phantom diffs on pages that had never had a value.
|
|
202
|
+
const normalise = (v) => (v === undefined || v === null || v === "" ? undefined : v);
|
|
203
|
+
if (normalise(bv) === normalise(av))
|
|
204
|
+
continue;
|
|
205
|
+
out.push({ path: `meta.${key}`, before: bv, after: av, kind });
|
|
206
|
+
}
|
|
207
|
+
return out;
|
|
208
|
+
}
|
|
171
209
|
// Keys of SiteConfig that drive the rendered SiteHeader chrome. We diff
|
|
172
210
|
// only these to keep the publish UI focused on user-visible header changes;
|
|
173
211
|
// `purpose`, `tone`, `constraints`, `themeOverrides` are AI/style context
|
|
@@ -276,6 +314,15 @@ export function computePublishDiff(draft, published, options) {
|
|
|
276
314
|
if (bd.fieldDiffs)
|
|
277
315
|
totalChangedFields += bd.fieldDiffs.length;
|
|
278
316
|
}
|
|
317
|
+
/*
|
|
318
|
+
* Page metadata counts as changed fields too.
|
|
319
|
+
*
|
|
320
|
+
* Three counters used to disagree on one screen — "1 field changed" here
|
|
321
|
+
* (blocks only), "2 changes" on the page row (which counted an added
|
|
322
|
+
* block), and "Publish 1 change" on the button — and a metadata edit moved
|
|
323
|
+
* none of them.
|
|
324
|
+
*/
|
|
325
|
+
totalChangedFields += diff.metaDiffs?.length ?? 0;
|
|
279
326
|
}
|
|
280
327
|
const statusOrder = {
|
|
281
328
|
added: 0,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@avocadostudio-ai/orchestrator-core",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.7",
|
|
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.11.
|
|
26
|
-
"@avocadostudio-ai/shared": "^0.11.
|
|
25
|
+
"@avocadostudio-ai/migration-sdk": "^0.11.7",
|
|
26
|
+
"@avocadostudio-ai/shared": "^0.11.7"
|
|
27
27
|
},
|
|
28
28
|
"devDependencies": {
|
|
29
29
|
"@anthropic-ai/claude-agent-sdk": "^0.3.220",
|