@avocadostudio-ai/orchestrator-core 0.11.3 → 0.11.5
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/chat/chat-pipeline.js +2 -1
- package/dist/env-file-location.d.ts +22 -0
- package/dist/env-file-location.js +77 -0
- package/dist/handler/create-orchestrator.js +144 -5
- package/dist/http/publish-actions.js +10 -1
- package/dist/nlp/deterministic-planner-suggestions.d.ts +16 -1
- package/dist/nlp/deterministic-planner-suggestions.js +22 -2
- package/package.json +3 -3
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { keyFileHint } from "../env-file-location.js";
|
|
2
3
|
import { blockManifestSchema } from "@avocadostudio-ai/shared";
|
|
3
4
|
import { GENERATING_IMAGE_PLACEHOLDER, SEARCHING_IMAGE_PLACEHOLDER, isGeneratingPlaceholder, cleanupImagePlaceholders, buildPageDirectory, isVariationRequestMessage, variationVerbIntent, resolveEffectiveSlug, throwIfCanceled, raceCancel, sleepMs, suppressCancelOnly } from "./chat-pipeline-shared.js";
|
|
4
5
|
import { siteCapabilitiesSchema, isBatchAddRequest, isDuplicateBlockRequest, isBlockCatalogQuery, isInfoQuery, isContentQuery, isPageListQuery, requestsPlanFirst, plannerMessageWithPendingContext, buildSiteContextBlock, infoResponse } from "../nlp/intent-detection.js";
|
|
@@ -2997,7 +2998,7 @@ export async function runChatPipeline(ctx, body, options) {
|
|
|
2997
2998
|
markPlanningStart();
|
|
2998
2999
|
try {
|
|
2999
3000
|
emitStatusTone("planning");
|
|
3000
|
-
const demoPlan = withKeylessNotice(demoPlanFromMessageImpl(plannerMessage, effectiveSlug, planningActiveBlockId, body.activeBlockType));
|
|
3001
|
+
const demoPlan = withKeylessNotice(demoPlanFromMessageImpl(plannerMessage, effectiveSlug, planningActiveBlockId, body.activeBlockType), keyFileHint());
|
|
3001
3002
|
markPlanningFinish();
|
|
3002
3003
|
const outcome = await respondFromPlan(demoPlan, "demo", applyMode, undefined, "demo");
|
|
3003
3004
|
if (outcome.done)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve the absolute path of the file to add the key to.
|
|
3
|
+
*
|
|
4
|
+
* Walks up from `cwd` for a directory that already holds a `package.json`,
|
|
5
|
+
* taking an existing env file there if there is one and naming the file that
|
|
6
|
+
* *should* exist if there is not. Returns the caller's best guess rather than
|
|
7
|
+
* nothing: a path that does not exist yet is still the right answer to "where
|
|
8
|
+
* do I put this", and creating it is the instruction.
|
|
9
|
+
*/
|
|
10
|
+
export declare function resolveEnvFilePath(cwd?: string): string;
|
|
11
|
+
/**
|
|
12
|
+
* The path to show a user, or `undefined` when showing one would be wrong.
|
|
13
|
+
*
|
|
14
|
+
* Withheld in production for two reasons, and the second is the one that
|
|
15
|
+
* matters. It would put a server filesystem path into a browser — mildly
|
|
16
|
+
* careless on its own. But the advice attached to it ("add it to this file and
|
|
17
|
+
* restart the dev server") is also simply **false** in production: there is no
|
|
18
|
+
* dev server, the file is not deployed, and a key added to it changes nothing.
|
|
19
|
+
* A precise path attached to instructions that cannot work is worse than the
|
|
20
|
+
* vague version, because it reads as authoritative.
|
|
21
|
+
*/
|
|
22
|
+
export declare function keyFileHint(env?: NodeJS.ProcessEnv): string | undefined;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where the user should actually put their API key.
|
|
3
|
+
*
|
|
4
|
+
* Every piece of copy in this product tells someone to "add a key to
|
|
5
|
+
* `.env.local` in your project root — the same folder as `package.json`", and
|
|
6
|
+
* none of it says *which* folder that is. The editor runs on its own origin,
|
|
7
|
+
* usually started by a script the user did not write, from a directory they may
|
|
8
|
+
* not have chosen; the terminal they are reading is running two processes; and
|
|
9
|
+
* the instruction arrives inside a browser tab that has no filesystem at all.
|
|
10
|
+
* "Your project root" is a description, not an address, and the person reading
|
|
11
|
+
* it is precisely the person who does not yet know the layout.
|
|
12
|
+
*
|
|
13
|
+
* The server does know. It is running in that directory. So it says so.
|
|
14
|
+
*
|
|
15
|
+
* Only in development, deliberately — see `keyFileHint`.
|
|
16
|
+
*/
|
|
17
|
+
import { existsSync } from "node:fs";
|
|
18
|
+
import { dirname, join, resolve } from "node:path";
|
|
19
|
+
/**
|
|
20
|
+
* The env files a key could live in, in the order the reader should prefer.
|
|
21
|
+
*
|
|
22
|
+
* `.env.local` first because that is what every doc and the scaffolder name,
|
|
23
|
+
* and because it is the one git ignores. `.env` second so an existing project
|
|
24
|
+
* that already keeps its secrets there is told to use the file it has rather
|
|
25
|
+
* than a second one that will silently take precedence.
|
|
26
|
+
*/
|
|
27
|
+
const CANDIDATES = [".env.local", ".env"];
|
|
28
|
+
/** How far up to look before giving up. A monorepo puts the app a few levels down. */
|
|
29
|
+
const MAX_DEPTH = 5;
|
|
30
|
+
/**
|
|
31
|
+
* Resolve the absolute path of the file to add the key to.
|
|
32
|
+
*
|
|
33
|
+
* Walks up from `cwd` for a directory that already holds a `package.json`,
|
|
34
|
+
* taking an existing env file there if there is one and naming the file that
|
|
35
|
+
* *should* exist if there is not. Returns the caller's best guess rather than
|
|
36
|
+
* nothing: a path that does not exist yet is still the right answer to "where
|
|
37
|
+
* do I put this", and creating it is the instruction.
|
|
38
|
+
*/
|
|
39
|
+
export function resolveEnvFilePath(cwd = process.cwd()) {
|
|
40
|
+
let dir = resolve(cwd);
|
|
41
|
+
for (let depth = 0; depth < MAX_DEPTH; depth++) {
|
|
42
|
+
for (const candidate of CANDIDATES) {
|
|
43
|
+
if (existsSync(join(dir, candidate)))
|
|
44
|
+
return join(dir, candidate);
|
|
45
|
+
}
|
|
46
|
+
// No env file here, but if this is the project root then this is where one goes.
|
|
47
|
+
if (existsSync(join(dir, "package.json")))
|
|
48
|
+
return join(dir, CANDIDATES[0]);
|
|
49
|
+
const parent = dirname(dir);
|
|
50
|
+
if (parent === dir)
|
|
51
|
+
break;
|
|
52
|
+
dir = parent;
|
|
53
|
+
}
|
|
54
|
+
return join(resolve(cwd), CANDIDATES[0]);
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* The path to show a user, or `undefined` when showing one would be wrong.
|
|
58
|
+
*
|
|
59
|
+
* Withheld in production for two reasons, and the second is the one that
|
|
60
|
+
* matters. It would put a server filesystem path into a browser — mildly
|
|
61
|
+
* careless on its own. But the advice attached to it ("add it to this file and
|
|
62
|
+
* restart the dev server") is also simply **false** in production: there is no
|
|
63
|
+
* dev server, the file is not deployed, and a key added to it changes nothing.
|
|
64
|
+
* A precise path attached to instructions that cannot work is worse than the
|
|
65
|
+
* vague version, because it reads as authoritative.
|
|
66
|
+
*/
|
|
67
|
+
export function keyFileHint(env = process.env) {
|
|
68
|
+
if ((env.NODE_ENV ?? "").trim() === "production")
|
|
69
|
+
return undefined;
|
|
70
|
+
try {
|
|
71
|
+
return resolveEnvFilePath();
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
// A sandboxed or read-only filesystem is not a reason to fail a chat turn.
|
|
75
|
+
return undefined;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -21,7 +21,8 @@ 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 } from "@avocadostudio-ai/shared";
|
|
24
|
+
import { operationSchema, blockManifestSchema, siteConfigSchema, declareBlockCatalogue, undeclaredBlockTypes, EDITOR_PROTOCOL_VERSION, withLoopbackSpellings } from "@avocadostudio-ai/shared";
|
|
25
|
+
import { keyFileHint } from "../env-file-location.js";
|
|
25
26
|
import { chatRequestBodySchema } from "../nlp/intent-detection.js";
|
|
26
27
|
import { applyOpsAtomically, pickFocusBlockId, pickUpdatedSlug, toErrorDetail, classifyGuardrailError } from "../ops/ops-engine.js";
|
|
27
28
|
import { runChatStream, formatSseFrame } from "../http/chat-stream.js";
|
|
@@ -230,7 +231,22 @@ function corsHeadersFor(request, config) {
|
|
|
230
231
|
*/
|
|
231
232
|
return isProductionEnv() ? {} : { "access-control-allow-origin": origin, "vary": "Origin" };
|
|
232
233
|
}
|
|
233
|
-
|
|
234
|
+
/*
|
|
235
|
+
* `localhost` and `127.0.0.1` are the same machine, so an allowlist naming
|
|
236
|
+
* one grants the other.
|
|
237
|
+
*
|
|
238
|
+
* A caller writes one spelling — the scaffold's `.env.local` writes
|
|
239
|
+
* `localhost`, our own CLI's banner prints `127.0.0.1` on its first line —
|
|
240
|
+
* and a browser at the other one had every request answered `200` with no
|
|
241
|
+
* `access-control-allow-origin` and discarded before any code could see it.
|
|
242
|
+
* The site's `frame-ancestors` list has expanded both spellings since it was
|
|
243
|
+
* written; this one did not, so the preview frame was permitted and every
|
|
244
|
+
* call inside it was dropped.
|
|
245
|
+
*
|
|
246
|
+
* Only these two names, and only when the configured origin already uses
|
|
247
|
+
* one. Every other widening of an allowlist stays an explicit decision.
|
|
248
|
+
*/
|
|
249
|
+
return withLoopbackSpellings(config.corsOrigins).includes(origin)
|
|
234
250
|
? { "access-control-allow-origin": origin, "vary": "Origin" }
|
|
235
251
|
: {};
|
|
236
252
|
}
|
|
@@ -301,6 +317,7 @@ const SUPPORTED_ROUTES = [
|
|
|
301
317
|
"POST /image/generate",
|
|
302
318
|
"POST /image/generate/chat",
|
|
303
319
|
"POST /image/interpret",
|
|
320
|
+
"POST /attachment/upload",
|
|
304
321
|
"POST /image/upload",
|
|
305
322
|
"GET /generated-images/:fileName"
|
|
306
323
|
];
|
|
@@ -332,7 +349,14 @@ const EXT_TO_MIME = {
|
|
|
332
349
|
webp: "image/webp",
|
|
333
350
|
gif: "image/gif",
|
|
334
351
|
avif: "image/avif",
|
|
335
|
-
svg: "image/svg+xml"
|
|
352
|
+
svg: "image/svg+xml",
|
|
353
|
+
/*
|
|
354
|
+
* Not an image, and it has to be here anyway: `/attachment/upload` accepts
|
|
355
|
+
* PDFs, and this map is what `/generated-images/` serves them back through.
|
|
356
|
+
* Without it a PDF uploads successfully and then 415s on the GET — stored,
|
|
357
|
+
* referenced in the chat, and unreadable.
|
|
358
|
+
*/
|
|
359
|
+
pdf: "application/pdf"
|
|
336
360
|
};
|
|
337
361
|
export { jsonFileAdapter, editorApiAdapter, resolveCapabilities } from "../cms/index.js";
|
|
338
362
|
function stripBasePath(pathname, basePath) {
|
|
@@ -1114,6 +1138,17 @@ export function createOrchestrator(config = {}) {
|
|
|
1114
1138
|
...(config.siteName ? { name: config.siteName } : {}),
|
|
1115
1139
|
demoContent: config.demoContent === true
|
|
1116
1140
|
},
|
|
1141
|
+
/*
|
|
1142
|
+
* Where the user should put their key, as an absolute path.
|
|
1143
|
+
*
|
|
1144
|
+
* Every piece of copy naming `.env.local` describes a file without
|
|
1145
|
+
* addressing it, and the reader is in a browser tab with no
|
|
1146
|
+
* filesystem, on an origin that is not the site's. The server is
|
|
1147
|
+
* running in that directory, so it is the only party that can answer.
|
|
1148
|
+
* Absent in production, where the advice attached to it would be
|
|
1149
|
+
* false — see `env-file-location.ts`.
|
|
1150
|
+
*/
|
|
1151
|
+
...(keyFileHint() ? { keyFilePath: keyFileHint() } : {}),
|
|
1117
1152
|
features: {
|
|
1118
1153
|
googleDrive: false,
|
|
1119
1154
|
unsplash: Boolean(process.env.UNSPLASH_ACCESS_KEY),
|
|
@@ -1319,8 +1354,29 @@ export function createOrchestrator(config = {}) {
|
|
|
1319
1354
|
const scopedSession = scope(body.session, body.siteId);
|
|
1320
1355
|
await runtime.bootstrapCache.ensure(scopedSession, runtime.adapter, runtime.log);
|
|
1321
1356
|
const parsedOps = z.array(operationSchema).safeParse(body.ops);
|
|
1322
|
-
if (!parsedOps.success)
|
|
1323
|
-
|
|
1357
|
+
if (!parsedOps.success) {
|
|
1358
|
+
/*
|
|
1359
|
+
* Name the key, and say what an operation looks like.
|
|
1360
|
+
*
|
|
1361
|
+
* Zod reports the *failure*, not the *expectation*: a body with no
|
|
1362
|
+
* `ops` array produced `{"expected":"array","path":[],"message":
|
|
1363
|
+
* "expected array, received undefined"}` — a root-level path pointing
|
|
1364
|
+
* at nothing, never naming `ops`. Someone wiring this by hand from the
|
|
1365
|
+
* documented op vocabulary needed three consecutive 400s to discover
|
|
1366
|
+
* the envelope key, then that the discriminator is `op` and not `type`,
|
|
1367
|
+
* and only then saw the real shape. Each error revealed exactly one
|
|
1368
|
+
* layer, which is the most expensive way to publish a schema.
|
|
1369
|
+
*
|
|
1370
|
+
* `example` is a valid minimal operation on purpose: it answers all
|
|
1371
|
+
* three of those questions at once, in the first response.
|
|
1372
|
+
*/
|
|
1373
|
+
return jsonResponse({
|
|
1374
|
+
error: "invalid ops payload",
|
|
1375
|
+
expected: "a JSON body of { session, siteId, ops: Operation[] }",
|
|
1376
|
+
example: { session: "dev", siteId: "my-site", ops: [{ op: "update_props", pageSlug: "/", blockId: "hero-1", patch: { heading: "New heading" } }] },
|
|
1377
|
+
details: parsedOps.error.issues
|
|
1378
|
+
}, { status: 400, cors });
|
|
1379
|
+
}
|
|
1324
1380
|
if (parsedOps.data.length === 0)
|
|
1325
1381
|
return jsonResponse({ error: "ops must not be empty" }, { status: 400, cors });
|
|
1326
1382
|
let manifest;
|
|
@@ -1571,6 +1627,23 @@ export function createOrchestrator(config = {}) {
|
|
|
1571
1627
|
{
|
|
1572
1628
|
id: effectiveSiteId ?? normalizeSiteId(query.siteId),
|
|
1573
1629
|
mode: "library",
|
|
1630
|
+
/*
|
|
1631
|
+
* The configured name, which this route had never carried.
|
|
1632
|
+
*
|
|
1633
|
+
* `/status/planner` reports it and `/sites` did not, so the
|
|
1634
|
+
* editor's site registry — which is fed from here — had nothing
|
|
1635
|
+
* to show but the id, and fell back to title-casing it. A
|
|
1636
|
+
* project in `demo-a/` configured as "The Avocado Hub" was
|
|
1637
|
+
* greeted as "Demo A" in the header and in the first sentence of
|
|
1638
|
+
* the welcome message.
|
|
1639
|
+
*
|
|
1640
|
+
* `demoContent` for the same reason: it decides which first-screen
|
|
1641
|
+
* suggestions the editor offers, and depending on a single
|
|
1642
|
+
* endpoint for it is what made those suggestions lose a race in
|
|
1643
|
+
* production.
|
|
1644
|
+
*/
|
|
1645
|
+
...(config.siteName ? { name: config.siteName } : {}),
|
|
1646
|
+
demoContent: config.demoContent === true,
|
|
1574
1647
|
...(config.previewUrl ? { previewUrl: config.previewUrl } : {}),
|
|
1575
1648
|
...(config.draftPath ? { draftPath: config.draftPath } : {}),
|
|
1576
1649
|
// A registration is more specific than a build-time default, so
|
|
@@ -2090,6 +2163,72 @@ export function createOrchestrator(config = {}) {
|
|
|
2090
2163
|
const bytes = Buffer.from(await file.arrayBuffer());
|
|
2091
2164
|
return actionResponse(await interpretImageAction({ bytes, byteLength: bytes.byteLength, mimeType: file.type }, { log: runtime.log }), cors);
|
|
2092
2165
|
}
|
|
2166
|
+
/*
|
|
2167
|
+
* The composer's paperclip, which library mode never answered.
|
|
2168
|
+
*
|
|
2169
|
+
* The editor's `useMediaInput` posts every chat attachment here,
|
|
2170
|
+
* unconditionally and with no capability probe, and this handler had no
|
|
2171
|
+
* such route — so the control shipped enabled on the first keyless screen
|
|
2172
|
+
* of every scaffolded project and answered `405` with
|
|
2173
|
+
* "not handled by createOrchestrator()" rendered in red under the composer.
|
|
2174
|
+
* It existed only in the standalone Fastify orchestrator
|
|
2175
|
+
* (`apps/orchestrator/src/routes/media.ts`), while library mode is what the
|
|
2176
|
+
* scaffold ships and what the docs call the shape most integrations use.
|
|
2177
|
+
* `features/chat-attachments.md` documents the flow with no caveat.
|
|
2178
|
+
*
|
|
2179
|
+
* Implemented rather than hidden, because nothing was actually missing:
|
|
2180
|
+
* `/image/upload` below already writes to `imageDir` and
|
|
2181
|
+
* `/generated-images/` already serves it back. The route was the only
|
|
2182
|
+
* absent part, and a documented feature is better answered than suppressed.
|
|
2183
|
+
*
|
|
2184
|
+
* Accepts either field name. Fastify's multipart reader takes the first
|
|
2185
|
+
* file part whatever it is called, so the standalone route never had to
|
|
2186
|
+
* care; a Web `FormData` does. The composer sends `file`, the image
|
|
2187
|
+
* endpoints send `image`, and making the two disagree across modes is how
|
|
2188
|
+
* this class of defect started.
|
|
2189
|
+
*/
|
|
2190
|
+
if (request.method === "POST" && path === "/attachment/upload") {
|
|
2191
|
+
let form;
|
|
2192
|
+
try {
|
|
2193
|
+
form = await request.formData();
|
|
2194
|
+
}
|
|
2195
|
+
catch {
|
|
2196
|
+
return jsonResponse({ error: "expected multipart/form-data body" }, { status: 400, cors });
|
|
2197
|
+
}
|
|
2198
|
+
const file = form.get("file") ?? form.get("image");
|
|
2199
|
+
if (!(file instanceof File)) {
|
|
2200
|
+
return jsonResponse({ error: "missing 'file' field" }, { status: 400, cors });
|
|
2201
|
+
}
|
|
2202
|
+
/*
|
|
2203
|
+
* PDFs are attachable and are not images, so the image MIME map is not
|
|
2204
|
+
* the right gate here — it is the gate for what a `<img>` can render.
|
|
2205
|
+
*/
|
|
2206
|
+
const ext = file.type === "application/pdf" ? "pdf" : MIME_TO_EXT[file.type];
|
|
2207
|
+
if (!ext) {
|
|
2208
|
+
return jsonResponse({ error: `unsupported attachment type: ${file.type || "unknown"}` }, { status: 415, cors });
|
|
2209
|
+
}
|
|
2210
|
+
if (file.size > MAX_UPLOAD_BYTES) {
|
|
2211
|
+
return jsonResponse({ error: `attachment exceeds ${MAX_UPLOAD_BYTES} byte limit` }, { status: 413, cors });
|
|
2212
|
+
}
|
|
2213
|
+
const fileName = `upload_${Date.now()}_${randomUUID().slice(0, 8)}.${ext}`;
|
|
2214
|
+
try {
|
|
2215
|
+
await mkdir(imageDir, { recursive: true });
|
|
2216
|
+
await writeFile(resolve(imageDir, fileName), Buffer.from(await file.arrayBuffer()));
|
|
2217
|
+
}
|
|
2218
|
+
catch (err) {
|
|
2219
|
+
return jsonResponse({ error: "attachment upload failed", detail: err instanceof Error ? err.message : String(err) }, { status: 500, cors });
|
|
2220
|
+
}
|
|
2221
|
+
return jsonResponse({
|
|
2222
|
+
url: `${basePath}/generated-images/${fileName}`,
|
|
2223
|
+
bytes: file.size,
|
|
2224
|
+
mimeType: file.type,
|
|
2225
|
+
// `name` and `kind` are what the composer renders in the attachment
|
|
2226
|
+
// chip. Without them it falls back to the local File, which is right
|
|
2227
|
+
// but makes the two modes' responses differ for no reason.
|
|
2228
|
+
name: file.name,
|
|
2229
|
+
kind: file.type === "application/pdf" ? "pdf" : "image"
|
|
2230
|
+
}, { status: 200, cors });
|
|
2231
|
+
}
|
|
2093
2232
|
if (request.method === "POST" && path === "/image/upload") {
|
|
2094
2233
|
let form;
|
|
2095
2234
|
try {
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
*/
|
|
18
18
|
import { readFile } from "node:fs/promises";
|
|
19
19
|
import { resolve } from "node:path";
|
|
20
|
+
import { withLoopbackSpellings } from "@avocadostudio-ai/shared";
|
|
20
21
|
import { normalizeSession, scopedSessionKey, getSessionPages, getSiteConfig, publishedPages, publishStatusBySession, listPublishLog, findPublishLogForStatusUpdate, updatePublishLogStatusById } from "../state/session-state.js";
|
|
21
22
|
import { computePublishDiff } from "../publish/diff-engine.js";
|
|
22
23
|
import { refreshPublishStatusFromVercel } from "../publish/publish-helpers.js";
|
|
@@ -165,7 +166,15 @@ export function isSafeOrigin(raw) {
|
|
|
165
166
|
if (isPrivate) {
|
|
166
167
|
if (process.env.NODE_ENV !== "production")
|
|
167
168
|
return true;
|
|
168
|
-
|
|
169
|
+
/*
|
|
170
|
+
* Both loopback spellings, for the same reason CORS grants both: the
|
|
171
|
+
* declaration and the request are written by different hands. The scaffold
|
|
172
|
+
* writes `ORCHESTRATOR_CORS_ORIGINS=http://localhost:3000`; a site opened
|
|
173
|
+
* at `127.0.0.1` sends the other one, and `publish/diff` answered
|
|
174
|
+
* `400 siteOrigin is not an allowed URL` for an origin the deployment had
|
|
175
|
+
* in fact declared trusted.
|
|
176
|
+
*/
|
|
177
|
+
const corsOrigins = withLoopbackSpellings((process.env.ORCHESTRATOR_CORS_ORIGINS ?? "").split(",").map((s) => s.trim().replace(/\/+$/, "")));
|
|
169
178
|
const normalized = parsed.origin;
|
|
170
179
|
if (corsOrigins.some((o) => o === normalized))
|
|
171
180
|
return true;
|
|
@@ -74,7 +74,22 @@ export declare function blockContractsSummary(manifest?: BlockManifest): Record<
|
|
|
74
74
|
* they are the phrasings this planner *can* execute without a key.
|
|
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.
|
|
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.
|
|
85
|
+
*
|
|
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`.
|
|
90
|
+
*/
|
|
91
|
+
export declare function keylessPlannerNotice(envFilePath?: string): string;
|
|
92
|
+
export declare function withKeylessNotice(plan: EditPlan, envFilePath?: string): EditPlan;
|
|
78
93
|
/**
|
|
79
94
|
* Drop the suggestions this planner could not act on.
|
|
80
95
|
*
|
|
@@ -627,10 +627,30 @@ export function blockContractsSummary(manifest) {
|
|
|
627
627
|
*/
|
|
628
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
629
|
"Add ANTHROPIC_API_KEY (or OPENAI_API_KEY) to .env.local and restart the dev server to chat for real.";
|
|
630
|
-
|
|
630
|
+
/**
|
|
631
|
+
* The same notice, naming the file by its full path when we can.
|
|
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.
|
|
638
|
+
*
|
|
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`.
|
|
643
|
+
*/
|
|
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.`);
|
|
649
|
+
}
|
|
650
|
+
export function withKeylessNotice(plan, envFilePath) {
|
|
631
651
|
if (plan.ops.length > 0)
|
|
632
652
|
return plan;
|
|
633
|
-
return { ...plan, summary_for_user:
|
|
653
|
+
return { ...plan, summary_for_user: keylessPlannerNotice(envFilePath) };
|
|
634
654
|
}
|
|
635
655
|
/**
|
|
636
656
|
* Drop the suggestions this planner could not act on.
|
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.5",
|
|
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.5",
|
|
26
|
+
"@avocadostudio-ai/shared": "^0.11.5"
|
|
27
27
|
},
|
|
28
28
|
"devDependencies": {
|
|
29
29
|
"@anthropic-ai/claude-agent-sdk": "^0.3.220",
|