@avocadostudio-ai/orchestrator-core 0.1.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.
Files changed (196) hide show
  1. package/LICENSE +201 -0
  2. package/dist/agent/agent-context.d.ts +19 -0
  3. package/dist/agent/agent-context.js +67 -0
  4. package/dist/agent/agent-logger.d.ts +5 -0
  5. package/dist/agent/agent-logger.js +22 -0
  6. package/dist/agent/agent-loop-openai.d.ts +8 -0
  7. package/dist/agent/agent-loop-openai.js +172 -0
  8. package/dist/agent/agent-loop.d.ts +56 -0
  9. package/dist/agent/agent-loop.js +167 -0
  10. package/dist/agent/agent-provider.d.ts +28 -0
  11. package/dist/agent/agent-provider.js +63 -0
  12. package/dist/agent/agent-tools.d.ts +28 -0
  13. package/dist/agent/agent-tools.js +899 -0
  14. package/dist/agent/context/editing-guidelines.md +46 -0
  15. package/dist/agent/context/role.md +39 -0
  16. package/dist/agent/integration-prompt.d.ts +9 -0
  17. package/dist/agent/integration-prompt.js +154 -0
  18. package/dist/agent/sites-agent-context.d.ts +12 -0
  19. package/dist/agent/sites-agent-context.js +316 -0
  20. package/dist/agent/sites-agent-shared.d.ts +161 -0
  21. package/dist/agent/sites-agent-shared.js +1101 -0
  22. package/dist/agent/sites-agent-tools.d.ts +18 -0
  23. package/dist/agent/sites-agent-tools.js +1227 -0
  24. package/dist/chat/anthropic-cache.d.ts +20 -0
  25. package/dist/chat/anthropic-cache.js +54 -0
  26. package/dist/chat/anthropic-planner.d.ts +98 -0
  27. package/dist/chat/anthropic-planner.js +1012 -0
  28. package/dist/chat/changelog-coverage-validator.d.ts +37 -0
  29. package/dist/chat/changelog-coverage-validator.js +215 -0
  30. package/dist/chat/chat-pipeline-context.d.ts +211 -0
  31. package/dist/chat/chat-pipeline-context.js +249 -0
  32. package/dist/chat/chat-pipeline-deterministic.d.ts +61 -0
  33. package/dist/chat/chat-pipeline-deterministic.js +407 -0
  34. package/dist/chat/chat-pipeline-image.d.ts +86 -0
  35. package/dist/chat/chat-pipeline-image.js +897 -0
  36. package/dist/chat/chat-pipeline-shared.d.ts +69 -0
  37. package/dist/chat/chat-pipeline-shared.js +212 -0
  38. package/dist/chat/chat-pipeline-translation.d.ts +27 -0
  39. package/dist/chat/chat-pipeline-translation.js +417 -0
  40. package/dist/chat/chat-pipeline-ui.d.ts +14 -0
  41. package/dist/chat/chat-pipeline-ui.js +244 -0
  42. package/dist/chat/chat-pipeline.d.ts +99 -0
  43. package/dist/chat/chat-pipeline.js +3999 -0
  44. package/dist/chat/decomposer.d.ts +21 -0
  45. package/dist/chat/decomposer.js +65 -0
  46. package/dist/chat/gemini-planner.d.ts +70 -0
  47. package/dist/chat/gemini-planner.js +541 -0
  48. package/dist/chat/hallucination-validator.d.ts +36 -0
  49. package/dist/chat/hallucination-validator.js +110 -0
  50. package/dist/chat/locale-strings.d.ts +47 -0
  51. package/dist/chat/locale-strings.js +100 -0
  52. package/dist/chat/plan-json-schema.d.ts +133 -0
  53. package/dist/chat/plan-json-schema.js +112 -0
  54. package/dist/chat/planner-types.d.ts +120 -0
  55. package/dist/chat/planner-types.js +66 -0
  56. package/dist/chat/planner.d.ts +148 -0
  57. package/dist/chat/planner.js +1361 -0
  58. package/dist/chat/prompts.d.ts +67 -0
  59. package/dist/chat/prompts.js +356 -0
  60. package/dist/chat/provider-routing.d.ts +14 -0
  61. package/dist/chat/provider-routing.js +27 -0
  62. package/dist/chat/variation-pipeline.d.ts +135 -0
  63. package/dist/chat/variation-pipeline.js +837 -0
  64. package/dist/chat/vision-alt-generator.d.ts +35 -0
  65. package/dist/chat/vision-alt-generator.js +152 -0
  66. package/dist/cms/adapter.d.ts +62 -0
  67. package/dist/cms/adapter.js +1 -0
  68. package/dist/cms/bootstrap.d.ts +17 -0
  69. package/dist/cms/bootstrap.js +85 -0
  70. package/dist/cms/editor-api-adapter.d.ts +23 -0
  71. package/dist/cms/editor-api-adapter.js +71 -0
  72. package/dist/cms/index.d.ts +4 -0
  73. package/dist/cms/index.js +3 -0
  74. package/dist/cms/json-file-adapter.d.ts +11 -0
  75. package/dist/cms/json-file-adapter.js +62 -0
  76. package/dist/demo-mode.d.ts +59 -0
  77. package/dist/demo-mode.js +201 -0
  78. package/dist/errors.d.ts +67 -0
  79. package/dist/errors.js +129 -0
  80. package/dist/http/chat-stream-resumable.d.ts +108 -0
  81. package/dist/http/chat-stream-resumable.js +290 -0
  82. package/dist/http/chat-stream.d.ts +99 -0
  83. package/dist/http/chat-stream.js +92 -0
  84. package/dist/image/gdrive-client.d.ts +22 -0
  85. package/dist/image/gdrive-client.js +215 -0
  86. package/dist/image/image-helpers.d.ts +95 -0
  87. package/dist/image/image-helpers.js +488 -0
  88. package/dist/index.d.ts +1 -0
  89. package/dist/index.js +1 -0
  90. package/dist/jira/jira-approval.d.ts +22 -0
  91. package/dist/jira/jira-approval.js +51 -0
  92. package/dist/jira/jira-client.d.ts +44 -0
  93. package/dist/jira/jira-client.js +313 -0
  94. package/dist/jira/jira-poller.d.ts +46 -0
  95. package/dist/jira/jira-poller.js +184 -0
  96. package/dist/jira/jira-processor.d.ts +103 -0
  97. package/dist/jira/jira-processor.js +1085 -0
  98. package/dist/jira/jira-types.d.ts +117 -0
  99. package/dist/jira/jira-types.js +38 -0
  100. package/dist/logger.d.ts +12 -0
  101. package/dist/logger.js +28 -0
  102. package/dist/migration/mcp-server-stdio.d.ts +8 -0
  103. package/dist/migration/mcp-server-stdio.js +672 -0
  104. package/dist/migration/migration-prompt.d.ts +7 -0
  105. package/dist/migration/migration-prompt.js +197 -0
  106. package/dist/migration/migration-tools.d.ts +17 -0
  107. package/dist/migration/migration-tools.js +159 -0
  108. package/dist/migration/scrape-cache.d.ts +9 -0
  109. package/dist/migration/scrape-cache.js +19 -0
  110. package/dist/nlp/deterministic-planner-context.d.ts +141 -0
  111. package/dist/nlp/deterministic-planner-context.js +362 -0
  112. package/dist/nlp/deterministic-planner-pages.d.ts +26 -0
  113. package/dist/nlp/deterministic-planner-pages.js +170 -0
  114. package/dist/nlp/deterministic-planner-patches.d.ts +80 -0
  115. package/dist/nlp/deterministic-planner-patches.js +508 -0
  116. package/dist/nlp/deterministic-planner-refs.d.ts +33 -0
  117. package/dist/nlp/deterministic-planner-refs.js +164 -0
  118. package/dist/nlp/deterministic-planner-suggestions.d.ts +49 -0
  119. package/dist/nlp/deterministic-planner-suggestions.js +579 -0
  120. package/dist/nlp/deterministic-planner.d.ts +85 -0
  121. package/dist/nlp/deterministic-planner.js +1631 -0
  122. package/dist/nlp/intent-detection.d.ts +309 -0
  123. package/dist/nlp/intent-detection.js +730 -0
  124. package/dist/nlp/intent-helpers.d.ts +15 -0
  125. package/dist/nlp/intent-helpers.js +243 -0
  126. package/dist/nlp/intent-patterns.d.ts +40 -0
  127. package/dist/nlp/intent-patterns.js +223 -0
  128. package/dist/nlp/plan-normalizer.d.ts +41 -0
  129. package/dist/nlp/plan-normalizer.js +1537 -0
  130. package/dist/ops/destructive-action-gate.d.ts +44 -0
  131. package/dist/ops/destructive-action-gate.js +90 -0
  132. package/dist/ops/ops-engine.d.ts +151 -0
  133. package/dist/ops/ops-engine.js +1394 -0
  134. package/dist/publish/diff-engine.d.ts +18 -0
  135. package/dist/publish/diff-engine.js +305 -0
  136. package/dist/publish/publish-helpers.d.ts +87 -0
  137. package/dist/publish/publish-helpers.js +521 -0
  138. package/dist/publish/publish-target-registry.d.ts +7 -0
  139. package/dist/publish/publish-target-registry.js +61 -0
  140. package/dist/publish/publish-target.d.ts +81 -0
  141. package/dist/publish/publish-target.js +1 -0
  142. package/dist/publish/targets/deploy-hook.d.ts +13 -0
  143. package/dist/publish/targets/deploy-hook.js +123 -0
  144. package/dist/publish/targets/git.d.ts +13 -0
  145. package/dist/publish/targets/git.js +55 -0
  146. package/dist/publish/targets/site-contract.d.ts +19 -0
  147. package/dist/publish/targets/site-contract.js +124 -0
  148. package/dist/state/content-source.d.ts +17 -0
  149. package/dist/state/content-source.js +1 -0
  150. package/dist/state/in-memory-content-source.d.ts +27 -0
  151. package/dist/state/in-memory-content-source.js +51 -0
  152. package/dist/state/session-lock.d.ts +13 -0
  153. package/dist/state/session-lock.js +29 -0
  154. package/dist/state/session-state.d.ts +310 -0
  155. package/dist/state/session-state.js +1083 -0
  156. package/dist/state/sqlite-store-singleton.d.ts +31 -0
  157. package/dist/state/sqlite-store-singleton.js +170 -0
  158. package/dist/state/sqlite-store.d.ts +135 -0
  159. package/dist/state/sqlite-store.js +421 -0
  160. package/dist/telemetry/chat-telemetry.d.ts +105 -0
  161. package/dist/telemetry/chat-telemetry.js +247 -0
  162. package/dist/telemetry/eval-candidate-store.d.ts +50 -0
  163. package/dist/telemetry/eval-candidate-store.js +120 -0
  164. package/dist/telemetry/feedback-store.d.ts +34 -0
  165. package/dist/telemetry/feedback-store.js +76 -0
  166. package/dist/telemetry/jira-telemetry.d.ts +57 -0
  167. package/dist/telemetry/jira-telemetry.js +68 -0
  168. package/dist/telemetry/migration-telemetry.d.ts +35 -0
  169. package/dist/telemetry/migration-telemetry.js +40 -0
  170. package/dist/telemetry/usage.d.ts +24 -0
  171. package/dist/telemetry/usage.js +80 -0
  172. package/dist/tools/builtin-registrations.d.ts +12 -0
  173. package/dist/tools/builtin-registrations.js +33 -0
  174. package/dist/tools/builtins/gdrive-browse.d.ts +3 -0
  175. package/dist/tools/builtins/gdrive-browse.js +68 -0
  176. package/dist/tools/builtins/image-generate.d.ts +3 -0
  177. package/dist/tools/builtins/image-generate.js +211 -0
  178. package/dist/tools/builtins/unsplash-get-by-id.d.ts +23 -0
  179. package/dist/tools/builtins/unsplash-get-by-id.js +119 -0
  180. package/dist/tools/builtins/unsplash-search.d.ts +3 -0
  181. package/dist/tools/builtins/unsplash-search.js +74 -0
  182. package/dist/tools/executor.d.ts +23 -0
  183. package/dist/tools/executor.js +169 -0
  184. package/dist/tools/index.d.ts +5 -0
  185. package/dist/tools/index.js +5 -0
  186. package/dist/tools/registry.d.ts +21 -0
  187. package/dist/tools/registry.js +75 -0
  188. package/dist/tools/runtime.d.ts +27 -0
  189. package/dist/tools/runtime.js +48 -0
  190. package/dist/tools/schema-validator.d.ts +24 -0
  191. package/dist/tools/schema-validator.js +88 -0
  192. package/dist/tools/types.d.ts +86 -0
  193. package/dist/tools/types.js +1 -0
  194. package/dist/variation-images.d.ts +19 -0
  195. package/dist/variation-images.js +12 -0
  196. package/package.json +78 -0
@@ -0,0 +1,35 @@
1
+ export interface VisionAltLogger {
2
+ warn?: (obj: object, msg: string) => void;
3
+ }
4
+ /**
5
+ * For inline "Generate alt text" / "Improve accessibility" / "Make more descriptive"
6
+ * wand actions on imageAlt fields. Bypasses the heavy planner (which routinely returns
7
+ * needs_clarification when the existing alt text looks like a user instruction or is
8
+ * empty) and asks the vision model directly for one clean alt-text string.
9
+ *
10
+ * Provider preference order: anthropic → openai. Falls back to the other if the
11
+ * preferred provider's API key is missing or the call fails. Returns null only
12
+ * when both providers fail or no API key is set anywhere.
13
+ */
14
+ export declare function generateAltTextFromVision(opts: {
15
+ imageUrl: string;
16
+ preferredProvider: "anthropic" | "openai" | "gemini";
17
+ log?: VisionAltLogger;
18
+ }): Promise<string | null>;
19
+ /**
20
+ * Parse an editablePath into an op shape. Supports two cases:
21
+ * - "imageAlt" → update_props { imageAlt: alt }
22
+ * - "cards[0].imageAlt" → update_item cards index 0 { imageAlt: alt }
23
+ *
24
+ * Returns null when the path doesn't match either shape — callers should fall
25
+ * back to the planner.
26
+ */
27
+ export declare function parseAltPathForOp(path: string): {
28
+ kind: "props";
29
+ key: string;
30
+ } | {
31
+ kind: "item";
32
+ listKey: string;
33
+ index: number;
34
+ itemKey: string;
35
+ } | null;
@@ -0,0 +1,152 @@
1
+ import Anthropic from "@anthropic-ai/sdk";
2
+ import OpenAI from "openai";
3
+ import { looksLikeUserInstruction } from "./chat-pipeline-ui.js";
4
+ const MIME_BY_EXT = {
5
+ ".png": "image/png",
6
+ ".jpg": "image/jpeg",
7
+ ".jpeg": "image/jpeg",
8
+ ".gif": "image/gif",
9
+ ".webp": "image/webp"
10
+ };
11
+ function resolveImageFetchUrl(rawUrl) {
12
+ if (/^https?:\/\//i.test(rawUrl))
13
+ return rawUrl;
14
+ if (rawUrl.startsWith("/")) {
15
+ const origin = (process.env.ORCHESTRATOR_PUBLIC_ORIGIN ?? "http://localhost:4200").replace(/\/+$/, "");
16
+ return `${origin}${rawUrl}`;
17
+ }
18
+ return null;
19
+ }
20
+ async function fetchImageBytes(url) {
21
+ const resolved = resolveImageFetchUrl(url);
22
+ if (!resolved)
23
+ return null;
24
+ try {
25
+ const res = await fetch(resolved);
26
+ if (!res.ok)
27
+ return null;
28
+ const buffer = Buffer.from(await res.arrayBuffer());
29
+ const contentType = res.headers.get("content-type");
30
+ let mediaType = contentType?.split(";")[0]?.trim() ?? "";
31
+ if (!mediaType || mediaType === "application/octet-stream") {
32
+ const ext = resolved.match(/(\.[a-z]+)(?:\?|$)/i)?.[1]?.toLowerCase() ?? "";
33
+ mediaType = MIME_BY_EXT[ext] ?? "image/jpeg";
34
+ }
35
+ return { base64: buffer.toString("base64"), mediaType };
36
+ }
37
+ catch {
38
+ return null;
39
+ }
40
+ }
41
+ const ALT_PROMPT = "Write a 1-2 sentence noun-phrase description of this image suitable as accessibility alt text. " +
42
+ "Be specific about the main subject, action, and setting. " +
43
+ "Do not start with 'image of', 'photo of', or 'a/an '. " +
44
+ "Do not mention 'AI-generated', watermarks, or metadata. " +
45
+ "Reply with only the alt text — no quotes, no preamble, no labels.";
46
+ /**
47
+ * For inline "Generate alt text" / "Improve accessibility" / "Make more descriptive"
48
+ * wand actions on imageAlt fields. Bypasses the heavy planner (which routinely returns
49
+ * needs_clarification when the existing alt text looks like a user instruction or is
50
+ * empty) and asks the vision model directly for one clean alt-text string.
51
+ *
52
+ * Provider preference order: anthropic → openai. Falls back to the other if the
53
+ * preferred provider's API key is missing or the call fails. Returns null only
54
+ * when both providers fail or no API key is set anywhere.
55
+ */
56
+ export async function generateAltTextFromVision(opts) {
57
+ const img = await fetchImageBytes(opts.imageUrl);
58
+ if (!img) {
59
+ opts.log?.warn?.({ imageUrl: opts.imageUrl }, "vision alt-text: failed to fetch image bytes");
60
+ return null;
61
+ }
62
+ const order = opts.preferredProvider === "openai" ? ["openai", "anthropic"] : ["anthropic", "openai"];
63
+ for (const provider of order) {
64
+ const text = await tryProvider(provider, img, opts.log);
65
+ if (text) {
66
+ const sanitized = sanitizeAltText(text);
67
+ if (sanitized && !looksLikeUserInstruction(sanitized))
68
+ return sanitized;
69
+ }
70
+ }
71
+ return null;
72
+ }
73
+ async function tryProvider(provider, img, log) {
74
+ try {
75
+ if (provider === "anthropic") {
76
+ if (!process.env.ANTHROPIC_API_KEY)
77
+ return null;
78
+ const client = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY });
79
+ const model = process.env.ANTHROPIC_VISION_ALT_MODEL?.trim() || "claude-haiku-4-5-20251001";
80
+ const result = await client.messages.create({
81
+ model,
82
+ max_tokens: 250,
83
+ messages: [
84
+ {
85
+ role: "user",
86
+ content: [
87
+ {
88
+ type: "image",
89
+ source: {
90
+ type: "base64",
91
+ media_type: img.mediaType,
92
+ data: img.base64
93
+ }
94
+ },
95
+ { type: "text", text: ALT_PROMPT }
96
+ ]
97
+ }
98
+ ]
99
+ });
100
+ const textBlock = result.content.find((c) => c.type === "text");
101
+ return textBlock && textBlock.type === "text" ? textBlock.text : null;
102
+ }
103
+ if (!process.env.OPENAI_API_KEY)
104
+ return null;
105
+ const client = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
106
+ const model = process.env.OPENAI_VISION_ALT_MODEL?.trim() || process.env.OPENAI_VISION_MODEL?.trim() || "gpt-4o-mini";
107
+ const dataUrl = `data:${img.mediaType};base64,${img.base64}`;
108
+ const completion = await client.chat.completions.create({
109
+ model,
110
+ max_tokens: 250,
111
+ messages: [
112
+ {
113
+ role: "user",
114
+ content: [
115
+ { type: "text", text: ALT_PROMPT },
116
+ { type: "image_url", image_url: { url: dataUrl } }
117
+ ]
118
+ }
119
+ ]
120
+ });
121
+ return completion.choices[0]?.message?.content ?? null;
122
+ }
123
+ catch (err) {
124
+ log?.warn?.({ provider, err: err instanceof Error ? err.message : String(err) }, "vision alt-text direct call failed");
125
+ return null;
126
+ }
127
+ }
128
+ function sanitizeAltText(raw) {
129
+ return raw
130
+ .trim()
131
+ .replace(/^["'`“‘]+|["'`”’]+$/g, "")
132
+ .replace(/^(alt(\s+text)?\s*[:\-—]\s*)/i, "")
133
+ .trim();
134
+ }
135
+ /**
136
+ * Parse an editablePath into an op shape. Supports two cases:
137
+ * - "imageAlt" → update_props { imageAlt: alt }
138
+ * - "cards[0].imageAlt" → update_item cards index 0 { imageAlt: alt }
139
+ *
140
+ * Returns null when the path doesn't match either shape — callers should fall
141
+ * back to the planner.
142
+ */
143
+ export function parseAltPathForOp(path) {
144
+ const nested = path.match(/^(\w+)\[(\d+)\]\.(\w+)$/);
145
+ if (nested) {
146
+ const [, listKey, indexStr, itemKey] = nested;
147
+ return { kind: "item", listKey, index: Number(indexStr), itemKey };
148
+ }
149
+ if (/^\w+$/.test(path))
150
+ return { kind: "props", key: path };
151
+ return null;
152
+ }
@@ -0,0 +1,62 @@
1
+ import type { PageDoc, SiteConfig } from "@avocadostudio-ai/shared";
2
+ /**
3
+ * A single inline image asset attached to a publish call. Used for
4
+ * chat-generated images that live as base64 blobs (no public URL yet) — the
5
+ * adapter is responsible for uploading these to its content store and
6
+ * swapping the original blob URLs for the resulting public URLs.
7
+ */
8
+ export interface CmsInlineAsset {
9
+ /** base64-encoded image bytes */
10
+ data: string;
11
+ /** MIME type, e.g. "image/png" */
12
+ mimeType: string;
13
+ /** Original filename */
14
+ fileName: string;
15
+ }
16
+ /**
17
+ * Extra context passed to `onPublish` alongside the page list. Optional —
18
+ * adapters that don't care about inline assets can ignore it.
19
+ */
20
+ export interface CmsPublishContext {
21
+ /**
22
+ * Inline assets keyed by their placeholder URL inside the PageDoc[]
23
+ * (typically a `blob:` or `localhost` URL the adapter wouldn't be able to
24
+ * fetch). The adapter should upload each one, replace the placeholder URL
25
+ * in block props, and emit the resulting public URL.
26
+ */
27
+ assets?: Record<string, CmsInlineAsset>;
28
+ }
29
+ /**
30
+ * Result of an `onPublish` call. `void` means "success, nothing to report";
31
+ * an explicit `{ ok: false, error }` lets the adapter surface a per-publish
32
+ * error message that the orchestrator returns to the client.
33
+ */
34
+ export type CmsPublishResult = void | {
35
+ ok: true;
36
+ } | {
37
+ ok: false;
38
+ error?: string;
39
+ };
40
+ export interface CmsAdapter {
41
+ /**
42
+ * Stable identifier for telemetry and per-adapter bootstrap caches.
43
+ * Keep it short and DNS-safe: `json-file`, `editor-api`, `sanity`, etc.
44
+ */
45
+ readonly id: string;
46
+ /**
47
+ * Return every page the orchestrator should know about for this site.
48
+ * Called lazily on the first chat for a fresh session. Failures are
49
+ * logged but non-fatal — the session simply starts empty.
50
+ */
51
+ getPages(): Promise<PageDoc[]>;
52
+ /**
53
+ * Optional publish hook. If present, called when the editor publishes
54
+ * a session's draft. If absent, publish remains a no-op against the
55
+ * external store — SQLite still records the publish locally.
56
+ *
57
+ * Second arg carries the session's current SiteConfig (navLabels, theme
58
+ * overrides, etc.); third arg carries any inline image blobs the editor
59
+ * collected during the session.
60
+ */
61
+ onPublish?(pages: PageDoc[], config: SiteConfig, context?: CmsPublishContext): Promise<CmsPublishResult>;
62
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,17 @@
1
+ import type { CmsAdapter } from "./adapter.ts";
2
+ import type { Logger } from "../logger.ts";
3
+ export interface CmsBootstrapCacheOptions {
4
+ /** Soft cap on remembered "already-attempted" sessions. Default 5000. */
5
+ maxAttempted?: number;
6
+ }
7
+ export interface CmsBootstrapCache {
8
+ ensure(session: string, adapter: CmsAdapter | null | undefined, log: Logger): Promise<void>;
9
+ /** Test helper — drop all remembered state. */
10
+ reset(): void;
11
+ /** Inspection — number of remembered attempts (post-eviction). */
12
+ readonly attemptedSize: number;
13
+ }
14
+ export declare function createCmsBootstrapCache(opts?: CmsBootstrapCacheOptions): CmsBootstrapCache;
15
+ export declare function ensureSessionBootstrapped(session: string, adapter: CmsAdapter | null | undefined, log: Logger): Promise<void>;
16
+ /** Test helper — clears the default process cache. Runtime-scoped caches own their own `reset()`. */
17
+ export declare function _resetCmsBootstrapCache(): void;
@@ -0,0 +1,85 @@
1
+ import { getSessionDraft, ensureHeroImageProps, bumpVersion, schedulePersistState } from "../state/session-state.js";
2
+ const DEFAULT_MAX_ATTEMPTED = 5000;
3
+ export function createCmsBootstrapCache(opts = {}) {
4
+ const inFlight = new Map();
5
+ // Set is insertion-ordered, so FIFO eviction is just "delete the first key".
6
+ const attempted = new Set();
7
+ const maxAttempted = Math.max(1, opts.maxAttempted ?? DEFAULT_MAX_ATTEMPTED);
8
+ function markAttempted(session) {
9
+ attempted.add(session);
10
+ while (attempted.size > maxAttempted) {
11
+ const oldest = attempted.values().next().value;
12
+ if (oldest === undefined)
13
+ break;
14
+ attempted.delete(oldest);
15
+ }
16
+ }
17
+ async function ensure(session, adapter, log) {
18
+ if (!adapter)
19
+ return;
20
+ if (attempted.has(session))
21
+ return;
22
+ const existing = inFlight.get(session);
23
+ if (existing)
24
+ return existing;
25
+ const promise = (async () => {
26
+ const draft = getSessionDraft(session);
27
+ if (draft.size > 0)
28
+ return; // finally-block records the attempt
29
+ try {
30
+ const pages = await adapter.getPages();
31
+ if (pages.length === 0) {
32
+ log.info({ session, adapter: adapter.id }, "cms-bootstrap: adapter returned no pages");
33
+ return;
34
+ }
35
+ for (const page of pages) {
36
+ const copy = structuredClone(page);
37
+ ensureHeroImageProps(copy);
38
+ draft.set(copy.slug, copy);
39
+ }
40
+ bumpVersion(session);
41
+ schedulePersistState(log);
42
+ log.info({ session, adapter: adapter.id, count: pages.length }, "cms-bootstrap: seeded session draft");
43
+ }
44
+ catch (err) {
45
+ log.warn({
46
+ session,
47
+ adapter: adapter.id,
48
+ err: err instanceof Error ? err.stack ?? err.message : String(err)
49
+ }, "cms-bootstrap: adapter.getPages() failed");
50
+ }
51
+ finally {
52
+ markAttempted(session);
53
+ }
54
+ })();
55
+ inFlight.set(session, promise);
56
+ try {
57
+ await promise;
58
+ }
59
+ finally {
60
+ inFlight.delete(session);
61
+ }
62
+ }
63
+ return {
64
+ ensure,
65
+ reset() {
66
+ inFlight.clear();
67
+ attempted.clear();
68
+ },
69
+ get attemptedSize() {
70
+ return attempted.size;
71
+ }
72
+ };
73
+ }
74
+ // Process-default cache, retained so call sites that don't construct their
75
+ // own runtime (e.g. the orchestrator-core test suite) can still call
76
+ // `ensureSessionBootstrapped` directly. Library-mode runtimes built by
77
+ // createOrchestrator() own their own cache via createCmsBootstrapCache.
78
+ const defaultCache = createCmsBootstrapCache();
79
+ export async function ensureSessionBootstrapped(session, adapter, log) {
80
+ return defaultCache.ensure(session, adapter, log);
81
+ }
82
+ /** Test helper — clears the default process cache. Runtime-scoped caches own their own `reset()`. */
83
+ export function _resetCmsBootstrapCache() {
84
+ defaultCache.reset();
85
+ }
@@ -0,0 +1,23 @@
1
+ import type { CmsAdapter } from "./adapter.ts";
2
+ import type { Logger } from "../logger.ts";
3
+ export interface EditorApiAdapterOptions {
4
+ /** Site origin, e.g. `https://example.com`. Defaults to env AUTO_BOOTSTRAP_SITE_ORIGIN. */
5
+ origin?: string;
6
+ /** Path under the origin. Default `/api/editor/pages`. */
7
+ path?: string;
8
+ /** Optional siteId query param. */
9
+ siteId?: string;
10
+ /** Request timeout in ms. Default 8000. */
11
+ timeoutMs?: number;
12
+ /**
13
+ * Extra request headers (e.g. `{ authorization: "Bearer …", cookie: "…" }`)
14
+ * for protected `/api/editor/pages` endpoints. Merged on top of the
15
+ * default `accept: application/json`. Plain object form keeps this
16
+ * adapter usable from Node-only tsconfig setups that don't pull in DOM
17
+ * lib types.
18
+ */
19
+ headers?: Record<string, string>;
20
+ /** Logger for dropped-page warnings. Defaults to consoleLogger(). */
21
+ logger?: Logger;
22
+ }
23
+ export declare function editorApiAdapter(opts?: EditorApiAdapterOptions): CmsAdapter;
@@ -0,0 +1,71 @@
1
+ // editorApiAdapter — fetch PageDoc[] from a site's /api/editor/pages endpoint.
2
+ //
3
+ // This is the "Avocado is mounted inside the site itself" pattern: when the
4
+ // orchestrator runs in library mode under the site's Next.js app, it can call
5
+ // the site's own page-listing API to discover content. The legacy behavior
6
+ // of apps/orchestrator's autoBootstrapIfEmpty is exactly this, modulo the
7
+ // fact that the orchestrator now lives in-process so the loopback is purely
8
+ // for sites that already exposed /api/editor/pages.
9
+ //
10
+ // Pass `origin` if the orchestrator runs separately from the site; omit
11
+ // `origin` and pass `loopbackPath` (default `/api/editor/pages`) for the
12
+ // same-process case where the route is a sibling.
13
+ import { pageDocSchemaLenient } from "@avocadostudio-ai/shared";
14
+ import { consoleLogger } from "../logger.js";
15
+ export function editorApiAdapter(opts = {}) {
16
+ const path = opts.path ?? "/api/editor/pages";
17
+ const timeoutMs = opts.timeoutMs ?? 8000;
18
+ const log = opts.logger ?? consoleLogger();
19
+ return {
20
+ id: "editor-api",
21
+ async getPages() {
22
+ const origin = opts.origin ?? process.env.AUTO_BOOTSTRAP_SITE_ORIGIN?.trim();
23
+ if (!origin)
24
+ throw new Error("editorApiAdapter: no origin configured (pass origin or set AUTO_BOOTSTRAP_SITE_ORIGIN)");
25
+ const url = new URL(path, origin);
26
+ if (opts.siteId)
27
+ url.searchParams.set("siteId", opts.siteId);
28
+ const headers = { accept: "application/json" };
29
+ if (opts.headers) {
30
+ for (const [key, value] of Object.entries(opts.headers)) {
31
+ headers[key] = value;
32
+ }
33
+ }
34
+ const res = await fetch(url.toString(), {
35
+ headers,
36
+ signal: AbortSignal.timeout(timeoutMs)
37
+ });
38
+ if (!res.ok)
39
+ throw new Error(`editorApiAdapter: ${url} returned ${res.status}`);
40
+ const body = (await res.json());
41
+ const rawPages = Array.isArray(body)
42
+ ? body
43
+ : body && typeof body === "object" && Array.isArray(body.pages)
44
+ ? body.pages
45
+ : [];
46
+ const pages = [];
47
+ for (let i = 0; i < rawPages.length; i++) {
48
+ const candidate = rawPages[i];
49
+ const result = pageDocSchemaLenient.safeParse(candidate);
50
+ if (result.success) {
51
+ pages.push(result.data);
52
+ }
53
+ else {
54
+ const firstIssue = result.error.issues[0];
55
+ const slugHint = candidate && typeof candidate === "object" && "slug" in candidate
56
+ ? String(candidate.slug ?? "")
57
+ : "";
58
+ log.warn({
59
+ adapter: "editor-api",
60
+ url: url.toString(),
61
+ index: i,
62
+ slug: slugHint,
63
+ issuePath: firstIssue?.path,
64
+ issueMessage: firstIssue?.message
65
+ }, "editorApiAdapter: dropped page that failed lenient PageDoc parse");
66
+ }
67
+ }
68
+ return pages;
69
+ }
70
+ };
71
+ }
@@ -0,0 +1,4 @@
1
+ export type { CmsAdapter, CmsInlineAsset, CmsPublishContext, CmsPublishResult } from "./adapter.ts";
2
+ export { jsonFileAdapter, type JsonFileAdapterOptions } from "./json-file-adapter.ts";
3
+ export { editorApiAdapter, type EditorApiAdapterOptions } from "./editor-api-adapter.ts";
4
+ export { ensureSessionBootstrapped, createCmsBootstrapCache, _resetCmsBootstrapCache, type CmsBootstrapCache, type CmsBootstrapCacheOptions } from "./bootstrap.ts";
@@ -0,0 +1,3 @@
1
+ export { jsonFileAdapter } from "./json-file-adapter.js";
2
+ export { editorApiAdapter } from "./editor-api-adapter.js";
3
+ export { ensureSessionBootstrapped, createCmsBootstrapCache, _resetCmsBootstrapCache } from "./bootstrap.js";
@@ -0,0 +1,11 @@
1
+ import type { CmsAdapter } from "./adapter.ts";
2
+ import type { Logger } from "../logger.ts";
3
+ export interface JsonFileAdapterOptions {
4
+ /** Path to the JSON file. Relative paths resolve against `process.cwd()`. */
5
+ path: string;
6
+ /** If true, onPublish() will overwrite the file. Default: false. */
7
+ writeOnPublish?: boolean;
8
+ /** Logger for dropped-page warnings. Defaults to consoleLogger(). */
9
+ logger?: Logger;
10
+ }
11
+ export declare function jsonFileAdapter(opts: JsonFileAdapterOptions): CmsAdapter;
@@ -0,0 +1,62 @@
1
+ // jsonFileAdapter — read PageDoc[] from a JSON file on disk.
2
+ //
3
+ // Smallest possible adapter. Suitable for sites that keep their content as
4
+ // a checked-in JSON array (the shape the orchestrator + blocks already
5
+ // understand). villa-puravida-web's lib/published-content.json is the
6
+ // canonical example.
7
+ //
8
+ // If `writeOnPublish` is true, onPublish() will overwrite the file with the
9
+ // new PageDoc[]. Use sparingly — in production deployments the file is
10
+ // typically read-only at runtime and republished via CI / git commit.
11
+ import { readFile, writeFile } from "node:fs/promises";
12
+ import { isAbsolute, resolve } from "node:path";
13
+ import { pageDocSchemaLenient } from "@avocadostudio-ai/shared";
14
+ import { consoleLogger } from "../logger.js";
15
+ export function jsonFileAdapter(opts) {
16
+ const resolved = isAbsolute(opts.path) ? opts.path : resolve(process.cwd(), opts.path);
17
+ const log = opts.logger ?? consoleLogger();
18
+ const adapter = {
19
+ id: "json-file",
20
+ async getPages() {
21
+ const raw = await readFile(resolved, "utf-8");
22
+ const parsed = JSON.parse(raw);
23
+ const candidates = Array.isArray(parsed)
24
+ ? parsed
25
+ : parsed && typeof parsed === "object" && Array.isArray(parsed.pages)
26
+ ? parsed.pages
27
+ : [];
28
+ const pages = [];
29
+ for (let i = 0; i < candidates.length; i++) {
30
+ const candidate = candidates[i];
31
+ const result = pageDocSchemaLenient.safeParse(candidate);
32
+ if (result.success) {
33
+ pages.push(result.data);
34
+ }
35
+ else {
36
+ const firstIssue = result.error.issues[0];
37
+ const slugHint = candidate && typeof candidate === "object" && "slug" in candidate
38
+ ? String(candidate.slug ?? "")
39
+ : "";
40
+ log.warn({
41
+ adapter: "json-file",
42
+ path: resolved,
43
+ index: i,
44
+ slug: slugHint,
45
+ issuePath: firstIssue?.path,
46
+ issueMessage: firstIssue?.message
47
+ }, "jsonFileAdapter: dropped page that failed lenient PageDoc parse");
48
+ }
49
+ }
50
+ return pages;
51
+ }
52
+ };
53
+ if (opts.writeOnPublish === true) {
54
+ // SiteConfig + inline assets aren't relevant to a flat JSON dump — the
55
+ // file shape is just `PageDoc[]`. Adapters that map into a richer store
56
+ // (Contentful, Sanity, Strapi) consume those extra args.
57
+ adapter.onPublish = async (pages) => {
58
+ await writeFile(resolved, JSON.stringify(pages, null, 2) + "\n", "utf-8");
59
+ };
60
+ }
61
+ return adapter;
62
+ }
@@ -0,0 +1,59 @@
1
+ import type { Operation, PageDoc } from "@avocadostudio-ai/shared";
2
+ /** Is demo mode enabled for this orchestrator process? */
3
+ export declare function isDemoModeEnabled(): boolean;
4
+ /** Operation types allowed in demo mode. Defaults to ["update_props"]. */
5
+ export declare function getDemoAllowedOpTypes(): string[];
6
+ /** Block types on which update_props is allowed. Defaults to ["Hero"]. */
7
+ export declare function getDemoAllowedBlockTypes(): string[];
8
+ /** Max requests per IP per hour. Defaults to 20. */
9
+ export declare function getDemoRateLimitPerHour(): number;
10
+ /** Should image generation be disabled in demo mode? Defaults to true. */
11
+ export declare function isDemoImageGenDisabled(): boolean;
12
+ /**
13
+ * Deterministic ephemeral session key for a demo visitor.
14
+ *
15
+ * We hash the IP to avoid leaking it, and keep the key short enough to read
16
+ * in logs. The key does NOT contain "::" so it routes through the default
17
+ * `getSessionDraft` path which auto-seeds from `demoPublishedPages()`.
18
+ */
19
+ export declare function demoSessionKeyForIp(ip: string): string;
20
+ /**
21
+ * Extract a client IP from a Fastify request. Render puts the real client IP
22
+ * in `x-forwarded-for` (first entry). Falls back to request.ip.
23
+ */
24
+ export declare function extractClientIp(request: {
25
+ headers: Record<string, string | string[] | undefined>;
26
+ ip?: string;
27
+ }): string;
28
+ export type DemoEnforcement = {
29
+ allowed: Operation[];
30
+ rejected: Array<{
31
+ op: Operation;
32
+ reason: string;
33
+ }>;
34
+ };
35
+ /**
36
+ * Validate a list of operations against demo-mode rules. Returns the split
37
+ * between allowed and rejected ops; caller decides whether to proceed with
38
+ * the allowed subset or to throw.
39
+ */
40
+ export declare function splitDemoOps(ops: Operation[], staged: Map<string, PageDoc>): DemoEnforcement;
41
+ /**
42
+ * Throws an `OperationError` if any of the given ops violates demo-mode
43
+ * rules. Used as the final gate inside `applyOpsAtomically`, so both chat
44
+ * and direct `/ops` paths are covered.
45
+ */
46
+ export declare function enforceDemoOps(ops: Operation[], staged: Map<string, PageDoc>): void;
47
+ /**
48
+ * Consume one token for the given IP. Returns `{ ok: true }` if allowed,
49
+ * or `{ ok: false, retryAfterSeconds }` if the quota for the current
50
+ * hourly window is exhausted.
51
+ */
52
+ export declare function consumeDemoRateToken(ip: string): {
53
+ ok: true;
54
+ } | {
55
+ ok: false;
56
+ retryAfterSeconds: number;
57
+ };
58
+ /** Reset the rate limiter (test hook). */
59
+ export declare function _resetDemoRateLimiterForTests(): void;