@avocadostudio-ai/orchestrator-core 0.11.5 → 0.11.6

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.
@@ -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": "content-type, x-access-token, authorization",
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
  /*
@@ -75,20 +75,29 @@ export declare function blockContractsSummary(manifest?: BlockManifest): Record<
75
75
  */
76
76
  export declare const KEYLESS_PLANNER_NOTICE: string;
77
77
  /**
78
- * The same notice, naming the file by its full path when we can.
78
+ * The same notice, without the *path*.
79
79
  *
80
- * "Add it to `.env.local`" is a description of a file, not an address, and the
81
- * person reading it is by definition the one who does not yet know the layout:
82
- * the message arrives in a browser tab, on an origin that is not the site's,
83
- * from a server started by a script they did not write. Saying *which*
84
- * `.env.local` removes the only step in this instruction that requires a guess.
80
+ * An earlier version interpolated the absolute path of `.env.local` into the
81
+ * sentence. Correct, and it read appallingly: an 80-character path mid-
82
+ * paragraph, wrapping across four lines of a narrow pane, so a two-line
83
+ * explanation became a wall in which the actual advice was the least visible
84
+ * part — and it duplicated, word for word, the notice sitting directly below
85
+ * the conversation.
85
86
  *
86
- * Falls back to the constant above whenever a path would be wrong or
87
- * unavailable — in production there is no dev server to restart and the file is
88
- * not deployed, so a precise path there would be authoritative and false. See
89
- * `keyFileHint`.
87
+ * The path belongs in exactly one place: the notice, where it is a selectable
88
+ * code block rather than prose. The variable names stay here, though, and a
89
+ * first attempt at removing those was wrong — `library-demo-identity.test.ts`
90
+ * pins them, and it is right to. **The notice is dismissible.** A message that
91
+ * says "see the notice below" is a dangling reference the moment someone closes
92
+ * it, and chat messages are also read in transcripts and telemetry where no
93
+ * notice exists. So this sentence has to stand alone; it just does not have to
94
+ * carry a filesystem path to do it.
95
+ *
96
+ * `envFilePath` is still accepted so callers need not know any of this. It
97
+ * changes nothing today and keeps the seam if a surface without the notice ever
98
+ * needs the long form.
90
99
  */
91
- export declare function keylessPlannerNotice(envFilePath?: string): string;
100
+ export declare function keylessPlannerNotice(_envFilePath?: string): string;
92
101
  export declare function withKeylessNotice(plan: EditPlan, envFilePath?: string): EditPlan;
93
102
  /**
94
103
  * Drop the suggestions this planner could not act on.
@@ -625,27 +625,33 @@ export function blockContractsSummary(manifest) {
625
625
  * So when the rules produce no operations, say why. The suggestions are kept:
626
626
  * they are the phrasings this planner *can* execute without a key.
627
627
  */
628
- export const KEYLESS_PLANNER_NOTICE = "There's no AI key configured, so I'm running on the built-in demo planner — it only handles simple, literal edits. " +
629
- "Add ANTHROPIC_API_KEY (or OPENAI_API_KEY) to .env.local and restart the dev server to chat for real.";
628
+ export const KEYLESS_PLANNER_NOTICE = "No API key is configured, so this is the built-in demo planner, which handles only simple, literal edits. " +
629
+ "Set ANTHROPIC_API_KEY or OPENAI_API_KEY in your project's .env.local and restart the dev server for full chat.";
630
630
  /**
631
- * The same notice, naming the file by its full path when we can.
631
+ * The same notice, without the *path*.
632
632
  *
633
- * "Add it to `.env.local`" is a description of a file, not an address, and the
634
- * person reading it is by definition the one who does not yet know the layout:
635
- * the message arrives in a browser tab, on an origin that is not the site's,
636
- * from a server started by a script they did not write. Saying *which*
637
- * `.env.local` removes the only step in this instruction that requires a guess.
633
+ * An earlier version interpolated the absolute path of `.env.local` into the
634
+ * sentence. Correct, and it read appallingly: an 80-character path mid-
635
+ * paragraph, wrapping across four lines of a narrow pane, so a two-line
636
+ * explanation became a wall in which the actual advice was the least visible
637
+ * part — and it duplicated, word for word, the notice sitting directly below
638
+ * the conversation.
638
639
  *
639
- * Falls back to the constant above whenever a path would be wrong or
640
- * unavailable — in production there is no dev server to restart and the file is
641
- * not deployed, so a precise path there would be authoritative and false. See
642
- * `keyFileHint`.
640
+ * The path belongs in exactly one place: the notice, where it is a selectable
641
+ * code block rather than prose. The variable names stay here, though, and a
642
+ * first attempt at removing those was wrong — `library-demo-identity.test.ts`
643
+ * pins them, and it is right to. **The notice is dismissible.** A message that
644
+ * says "see the notice below" is a dangling reference the moment someone closes
645
+ * it, and chat messages are also read in transcripts and telemetry where no
646
+ * notice exists. So this sentence has to stand alone; it just does not have to
647
+ * carry a filesystem path to do it.
648
+ *
649
+ * `envFilePath` is still accepted so callers need not know any of this. It
650
+ * changes nothing today and keeps the seam if a surface without the notice ever
651
+ * needs the long form.
643
652
  */
644
- export function keylessPlannerNotice(envFilePath) {
645
- if (!envFilePath)
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.`);
653
+ export function keylessPlannerNotice(_envFilePath) {
654
+ return KEYLESS_PLANNER_NOTICE;
649
655
  }
650
656
  export function withKeylessNotice(plan, envFilePath) {
651
657
  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.5",
3
+ "version": "0.11.6",
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.5",
26
- "@avocadostudio-ai/shared": "^0.11.5"
25
+ "@avocadostudio-ai/migration-sdk": "^0.11.6",
26
+ "@avocadostudio-ai/shared": "^0.11.6"
27
27
  },
28
28
  "devDependencies": {
29
29
  "@anthropic-ai/claude-agent-sdk": "^0.3.220",