@avocadostudio-ai/orchestrator-core 0.4.0 → 0.5.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/agent-logger.js +2 -1
- package/dist/handler/create-orchestrator.js +27 -2
- package/dist/handler/library-mount.d.ts +6 -0
- package/dist/handler/library-mount.js +45 -0
- package/dist/image/image-helpers.js +3 -2
- package/dist/publish/publish-helpers.js +2 -1
- package/dist/state/data-dir.d.ts +6 -0
- package/dist/state/data-dir.js +35 -0
- package/dist/state/sqlite-store-singleton.js +3 -17
- package/package.json +3 -3
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { appendFileSync } from "node:fs";
|
|
6
6
|
import { resolve } from "node:path";
|
|
7
|
-
|
|
7
|
+
import { resolveDataDir } from "../state/data-dir.js";
|
|
8
|
+
const LOG_PATH = resolve(resolveDataDir(), "agent-log.ndjson");
|
|
8
9
|
export function logAgent(streamId, event, detail, startedAt) {
|
|
9
10
|
const entry = {
|
|
10
11
|
ts: Date.now(),
|
|
@@ -51,6 +51,7 @@ import { createCmsBootstrapCache } from "../cms/bootstrap.js";
|
|
|
51
51
|
import { resolveCapabilities } from "../cms/adapter.js";
|
|
52
52
|
import { mediaSourceFromUnknown } from "../cms/media-sources.js";
|
|
53
53
|
import { isAccessGateEnabled, mintAccessToken, verifyAccessPassword } from "../http/access-tokens.js";
|
|
54
|
+
import { declareLibraryMount, observeLibraryMount } from "./library-mount.js";
|
|
54
55
|
import { checkAuth, resolveAuth } from "./auth.js";
|
|
55
56
|
import { setSiteAssetLister, invalidateSiteAssets } from "../state/site-assets.js";
|
|
56
57
|
const defaultModelLookup = () => ({
|
|
@@ -416,16 +417,40 @@ export function createOrchestrator(config = {}) {
|
|
|
416
417
|
const gateLogger = config.logger ?? consoleLogger();
|
|
417
418
|
{
|
|
418
419
|
const resolved = resolveAuth(config.auth);
|
|
419
|
-
|
|
420
|
+
/*
|
|
421
|
+
* `next build` evaluates this route module to collect its exports, with
|
|
422
|
+
* `NODE_ENV=production` and none of the deployment's environment. With no
|
|
423
|
+
* credential configured the gate resolves to `closed` and this printed a
|
|
424
|
+
* red `[error]` in the middle of an otherwise clean, *successful* build —
|
|
425
|
+
* about a request that is not being served, on a machine that is not the
|
|
426
|
+
* deployment.
|
|
427
|
+
*
|
|
428
|
+
* It is still the only notice anyone gets of a real problem, so it is not
|
|
429
|
+
* silenced: during a build it is a warning that says which state it is
|
|
430
|
+
* describing. At runtime it stays an error, because then it means every
|
|
431
|
+
* request is actually being refused.
|
|
432
|
+
*/
|
|
433
|
+
const building = process.env.NEXT_PHASE === "phase-production-build";
|
|
434
|
+
const line = building
|
|
435
|
+
? `[auth] library mode: ${resolved.mode} — ${resolved.reason} ` +
|
|
436
|
+
`(evaluated during the build; set ACCESS_PASSWORD_HASH or ` +
|
|
437
|
+
`ORCHESTRATOR_ACCESS_TOKEN in the deployment's environment, not here)`
|
|
438
|
+
: `[auth] library mode: ${resolved.mode} — ${resolved.reason}`;
|
|
420
439
|
if (resolved.mode === "closed")
|
|
421
|
-
gateLogger.error(line);
|
|
440
|
+
building ? gateLogger.warn(line) : gateLogger.error(line);
|
|
422
441
|
else if (resolved.mode === "open-dev")
|
|
423
442
|
gateLogger.warn(line);
|
|
424
443
|
else
|
|
425
444
|
gateLogger.info(line);
|
|
426
445
|
}
|
|
446
|
+
// Tell the SDK's draft fetch where we are, so it stops defaulting to the
|
|
447
|
+
// standalone orchestrator on :4200 that a library-mode site does not run.
|
|
448
|
+
// See `library-mount.ts` for why this is not simply a config value.
|
|
449
|
+
if (config.previewUrl)
|
|
450
|
+
declareLibraryMount(`${config.previewUrl.replace(/\/+$/, "")}${basePath}`);
|
|
427
451
|
const handler = async function handler(request) {
|
|
428
452
|
const url = new URL(request.url);
|
|
453
|
+
observeLibraryMount(`${url.origin}${basePath}`);
|
|
429
454
|
const path = stripBasePath(url.pathname, basePath);
|
|
430
455
|
const cors = corsHeadersFor(request, config);
|
|
431
456
|
if (request.method === "OPTIONS") {
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export declare const LIBRARY_MOUNT_KEY = "__avocado_library_mount__";
|
|
2
|
+
/** Called at construction, from `previewUrl` + `basePath`. */
|
|
3
|
+
export declare function declareLibraryMount(url: string): void;
|
|
4
|
+
/** Called on each request, from the URL the runtime actually received. */
|
|
5
|
+
export declare function observeLibraryMount(url: string): void;
|
|
6
|
+
export declare function getLibraryMount(): string | null;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Where this process's own orchestrator is mounted, if it has one.
|
|
3
|
+
*
|
|
4
|
+
* In library mode the orchestrator is a route inside the site's own Next app,
|
|
5
|
+
* and the SDK's draft fetch had no way to know that: with `ORCHESTRATOR_URL`
|
|
6
|
+
* unset it fell back to `http://127.0.0.1:4200`, the *standalone* orchestrator,
|
|
7
|
+
* which a library-mode site by definition does not run. The failure is silent
|
|
8
|
+
* when nothing is listening there (the draft fetch fails, the preview falls back
|
|
9
|
+
* to published content, and every edit appears to do nothing) and worse when
|
|
10
|
+
* something is — the fetch succeeds against a foreign process and the preview
|
|
11
|
+
* renders another project's pages. Anyone integrating is likely to have a :4200
|
|
12
|
+
* up, because that is what the standalone stack runs on.
|
|
13
|
+
*
|
|
14
|
+
* So the handler records its own address, and `getOrchestratorUrl()` in the SDK
|
|
15
|
+
* reads it through the same `globalThis` key. The key is a string on both sides
|
|
16
|
+
* rather than an import because `@avocadostudio-ai/orchestrator-core` is an
|
|
17
|
+
* *optional* peer of the SDK — a site that is not in library mode does not have
|
|
18
|
+
* it installed, and neither package may require the other.
|
|
19
|
+
*
|
|
20
|
+
* Two sources, in order of trust:
|
|
21
|
+
* - the origin of a real request this handler served, which cannot be wrong;
|
|
22
|
+
* - `config.previewUrl + basePath` at construction time, which is available
|
|
23
|
+
* before any request and is what covers a preview page that renders before
|
|
24
|
+
* the orchestrator route module has ever been evaluated.
|
|
25
|
+
*/
|
|
26
|
+
export const LIBRARY_MOUNT_KEY = "__avocado_library_mount__";
|
|
27
|
+
function slot() {
|
|
28
|
+
const g = globalThis;
|
|
29
|
+
return g[LIBRARY_MOUNT_KEY] ?? (g[LIBRARY_MOUNT_KEY] = {});
|
|
30
|
+
}
|
|
31
|
+
/** Called at construction, from `previewUrl` + `basePath`. */
|
|
32
|
+
export function declareLibraryMount(url) {
|
|
33
|
+
slot().declared = url.replace(/\/+$/, "");
|
|
34
|
+
}
|
|
35
|
+
/** Called on each request, from the URL the runtime actually received. */
|
|
36
|
+
export function observeLibraryMount(url) {
|
|
37
|
+
const s = slot();
|
|
38
|
+
const next = url.replace(/\/+$/, "");
|
|
39
|
+
if (s.observed !== next)
|
|
40
|
+
s.observed = next;
|
|
41
|
+
}
|
|
42
|
+
export function getLibraryMount() {
|
|
43
|
+
const s = slot();
|
|
44
|
+
return s.observed ?? s.declared ?? null;
|
|
45
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { mkdir, writeFile } from "node:fs/promises";
|
|
2
2
|
import { resolve } from "node:path";
|
|
3
3
|
import OpenAI from "openai";
|
|
4
|
+
import { resolveDataDir } from "../state/data-dir.js";
|
|
4
5
|
import { listImages, fileNameToAlt, resolveGdriveFolderId } from "./gdrive-client.js";
|
|
5
6
|
// ---------------------------------------------------------------------------
|
|
6
7
|
// Image generation timing — rolling average for progress estimation
|
|
@@ -218,7 +219,7 @@ export async function generateVariationImageWithOpenAI(args) {
|
|
|
218
219
|
const background = args.background ?? "auto";
|
|
219
220
|
const outputFormat = args.outputFormat ?? "png";
|
|
220
221
|
const client = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
|
|
221
|
-
const generatedImageDir = process.env.ORCHESTRATOR_GENERATED_IMAGE_DIR ?? resolve(
|
|
222
|
+
const generatedImageDir = process.env.ORCHESTRATOR_GENERATED_IMAGE_DIR ?? resolve(resolveDataDir(), "generated-images");
|
|
222
223
|
const orchestratorPublicOrigin = (process.env.ORCHESTRATOR_PUBLIC_ORIGIN ?? "http://localhost:4200").replace(/\/+$/, "");
|
|
223
224
|
args.log?.info({ event: "openai_image_start", model, size, background, outputFormat, promptLength: args.prompt.length }, "Starting OpenAI image generation");
|
|
224
225
|
const genStartMs = Date.now();
|
|
@@ -270,7 +271,7 @@ export async function generateVariationImageWithOpenAI(args) {
|
|
|
270
271
|
// Shared image save utility
|
|
271
272
|
// ---------------------------------------------------------------------------
|
|
272
273
|
export async function saveGeneratedImage(bytes, prefix = "gen", ext = "png") {
|
|
273
|
-
const generatedImageDir = process.env.ORCHESTRATOR_GENERATED_IMAGE_DIR ?? resolve(
|
|
274
|
+
const generatedImageDir = process.env.ORCHESTRATOR_GENERATED_IMAGE_DIR ?? resolve(resolveDataDir(), "generated-images");
|
|
274
275
|
const orchestratorPublicOrigin = (process.env.ORCHESTRATOR_PUBLIC_ORIGIN ?? "http://localhost:4200").replace(/\/+$/, "");
|
|
275
276
|
const fileName = `${prefix}_${Date.now()}_${Math.random().toString(36).slice(2, 8)}.${ext}`;
|
|
276
277
|
await mkdir(generatedImageDir, { recursive: true });
|
|
@@ -4,6 +4,7 @@ import { existsSync } from "node:fs";
|
|
|
4
4
|
import { copyFile, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
5
5
|
import { promisify } from "node:util";
|
|
6
6
|
import { resolve } from "node:path";
|
|
7
|
+
import { resolveDataDir } from "../state/data-dir.js";
|
|
7
8
|
import { pageDocSchema } from "@avocadostudio-ai/shared";
|
|
8
9
|
import { draftPages, versions, ensureHeroImageProps, persistStateNow, getSessionPages, getSiteConfig, isLegacySiteId } from "../state/session-state.js";
|
|
9
10
|
import { toErrorDetail } from "../ops/ops-engine.js";
|
|
@@ -413,7 +414,7 @@ export async function publishViaGit(session, content) {
|
|
|
413
414
|
let copiedImages = false;
|
|
414
415
|
if (imageUrlMap.size > 0) {
|
|
415
416
|
const generatedImageDir = process.env.ORCHESTRATOR_GENERATED_IMAGE_DIR ??
|
|
416
|
-
resolve(
|
|
417
|
+
resolve(resolveDataDir(), "generated-images");
|
|
417
418
|
await mkdir(imageDestDir, { recursive: true });
|
|
418
419
|
for (const [, fileName] of imageUrlMap) {
|
|
419
420
|
const src = resolve(generatedImageDir, fileName);
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export declare function isLegacyMonorepoLayout(cwd?: string): boolean;
|
|
2
|
+
/**
|
|
3
|
+
* The `.data` directory for this process, or the legacy monorepo one when this
|
|
4
|
+
* really is a package inside the monorepo and that directory already exists.
|
|
5
|
+
*/
|
|
6
|
+
export declare function resolveDataDir(cwd?: string): string;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
/*
|
|
4
|
+
* Where this process keeps its own files: the database, generated images, the
|
|
5
|
+
* agent log.
|
|
6
|
+
*
|
|
7
|
+
* The answer is `<cwd>/.data`. It used to be `<cwd>/../../.data`, which is
|
|
8
|
+
* `apps/orchestrator`'s position in *this* repo and nobody else's — a
|
|
9
|
+
* library-mode host runs from its own project root, so it wrote two directories
|
|
10
|
+
* above itself, outside the project and outside version control, into a
|
|
11
|
+
* directory shared with every sibling checkout that made the same mistake.
|
|
12
|
+
*
|
|
13
|
+
* The legacy location is still honoured for an existing monorepo checkout, but
|
|
14
|
+
* only on proof of workspace membership. The first version of that back-compat
|
|
15
|
+
* check asked `existsSync(legacy)` alone — "has anyone ever created that file",
|
|
16
|
+
* not "am I a package in the workspace that owns it" — so one mistaken write
|
|
17
|
+
* captured every unrelated project under that parent, permanently, because the
|
|
18
|
+
* bug is what created the file that re-triggered it.
|
|
19
|
+
*/
|
|
20
|
+
export function isLegacyMonorepoLayout(cwd = process.cwd()) {
|
|
21
|
+
return (existsSync(resolve(cwd, "../../pnpm-workspace.yaml")) &&
|
|
22
|
+
existsSync(resolve(cwd, "package.json")));
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* The `.data` directory for this process, or the legacy monorepo one when this
|
|
26
|
+
* really is a package inside the monorepo and that directory already exists.
|
|
27
|
+
*/
|
|
28
|
+
export function resolveDataDir(cwd = process.cwd()) {
|
|
29
|
+
if (isLegacyMonorepoLayout(cwd)) {
|
|
30
|
+
const legacy = resolve(cwd, "../../.data");
|
|
31
|
+
if (existsSync(legacy))
|
|
32
|
+
return legacy;
|
|
33
|
+
}
|
|
34
|
+
return resolve(cwd, ".data");
|
|
35
|
+
}
|
|
@@ -2,6 +2,7 @@ import { existsSync } from "node:fs";
|
|
|
2
2
|
import { rename, readFile, readdir, stat, unlink } from "node:fs/promises";
|
|
3
3
|
import { basename, dirname, resolve } from "node:path";
|
|
4
4
|
import { SqliteStore } from "./sqlite-store.js";
|
|
5
|
+
import { resolveDataDir } from "./data-dir.js";
|
|
5
6
|
// ---------------------------------------------------------------------------
|
|
6
7
|
// Config
|
|
7
8
|
// ---------------------------------------------------------------------------
|
|
@@ -17,23 +18,8 @@ export function resolveDbFile() {
|
|
|
17
18
|
return ":memory:";
|
|
18
19
|
if (process.env.NODE_ENV === "test")
|
|
19
20
|
return ":memory:";
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
*
|
|
23
|
-
* It is right for `apps/orchestrator`, whose cwd is two levels under the repo
|
|
24
|
-
* root, and wrong for everyone else: a library-mode host running from its own
|
|
25
|
-
* project root writes two directories *above* itself. PBA's landed in
|
|
26
|
-
* `~/Projects/.data` — outside the project, outside version control, and
|
|
27
|
-
* shared with any sibling checkout that made the same mistake.
|
|
28
|
-
*
|
|
29
|
-
* The default is now the host's own `.data/`. The legacy path still wins when
|
|
30
|
-
* a database is already sitting there, so an existing monorepo checkout keeps
|
|
31
|
-
* its state without an env var and without a migration step.
|
|
32
|
-
*/
|
|
33
|
-
const legacy = resolve(process.cwd(), "../../.data/orchestrator.db");
|
|
34
|
-
if (existsSync(legacy))
|
|
35
|
-
return legacy;
|
|
36
|
-
return resolve(process.cwd(), ".data/orchestrator.db");
|
|
21
|
+
// `resolveDataDir` carries the whole story of why this is not `../../.data`.
|
|
22
|
+
return resolve(resolveDataDir(), "orchestrator.db");
|
|
37
23
|
}
|
|
38
24
|
export function resolveJsonMigrationTtlDays() {
|
|
39
25
|
const raw = Number(process.env.ORCHESTRATOR_JSON_MIGRATION_TTL_DAYS ?? 14);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@avocadostudio-ai/orchestrator-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
"./package.json": "./package.json",
|
|
@@ -23,8 +23,8 @@
|
|
|
23
23
|
"openai": "^4.87.1",
|
|
24
24
|
"sharp": "^0.34.5",
|
|
25
25
|
"zod": "^4.3.6",
|
|
26
|
-
"@avocadostudio-ai/migration-sdk": "^0.
|
|
27
|
-
"@avocadostudio-ai/shared": "^0.
|
|
26
|
+
"@avocadostudio-ai/migration-sdk": "^0.5.0",
|
|
27
|
+
"@avocadostudio-ai/shared": "^0.5.0"
|
|
28
28
|
},
|
|
29
29
|
"devDependencies": {
|
|
30
30
|
"@google/genai": "^1.46.0",
|