@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.
- package/LICENSE +201 -0
- package/dist/agent/agent-context.d.ts +19 -0
- package/dist/agent/agent-context.js +67 -0
- package/dist/agent/agent-logger.d.ts +5 -0
- package/dist/agent/agent-logger.js +22 -0
- package/dist/agent/agent-loop-openai.d.ts +8 -0
- package/dist/agent/agent-loop-openai.js +172 -0
- package/dist/agent/agent-loop.d.ts +56 -0
- package/dist/agent/agent-loop.js +167 -0
- package/dist/agent/agent-provider.d.ts +28 -0
- package/dist/agent/agent-provider.js +63 -0
- package/dist/agent/agent-tools.d.ts +28 -0
- package/dist/agent/agent-tools.js +899 -0
- package/dist/agent/context/editing-guidelines.md +46 -0
- package/dist/agent/context/role.md +39 -0
- package/dist/agent/integration-prompt.d.ts +9 -0
- package/dist/agent/integration-prompt.js +154 -0
- package/dist/agent/sites-agent-context.d.ts +12 -0
- package/dist/agent/sites-agent-context.js +316 -0
- package/dist/agent/sites-agent-shared.d.ts +161 -0
- package/dist/agent/sites-agent-shared.js +1101 -0
- package/dist/agent/sites-agent-tools.d.ts +18 -0
- package/dist/agent/sites-agent-tools.js +1227 -0
- package/dist/chat/anthropic-cache.d.ts +20 -0
- package/dist/chat/anthropic-cache.js +54 -0
- package/dist/chat/anthropic-planner.d.ts +98 -0
- package/dist/chat/anthropic-planner.js +1012 -0
- package/dist/chat/changelog-coverage-validator.d.ts +37 -0
- package/dist/chat/changelog-coverage-validator.js +215 -0
- package/dist/chat/chat-pipeline-context.d.ts +211 -0
- package/dist/chat/chat-pipeline-context.js +249 -0
- package/dist/chat/chat-pipeline-deterministic.d.ts +61 -0
- package/dist/chat/chat-pipeline-deterministic.js +407 -0
- package/dist/chat/chat-pipeline-image.d.ts +86 -0
- package/dist/chat/chat-pipeline-image.js +897 -0
- package/dist/chat/chat-pipeline-shared.d.ts +69 -0
- package/dist/chat/chat-pipeline-shared.js +212 -0
- package/dist/chat/chat-pipeline-translation.d.ts +27 -0
- package/dist/chat/chat-pipeline-translation.js +417 -0
- package/dist/chat/chat-pipeline-ui.d.ts +14 -0
- package/dist/chat/chat-pipeline-ui.js +244 -0
- package/dist/chat/chat-pipeline.d.ts +99 -0
- package/dist/chat/chat-pipeline.js +3999 -0
- package/dist/chat/decomposer.d.ts +21 -0
- package/dist/chat/decomposer.js +65 -0
- package/dist/chat/gemini-planner.d.ts +70 -0
- package/dist/chat/gemini-planner.js +541 -0
- package/dist/chat/hallucination-validator.d.ts +36 -0
- package/dist/chat/hallucination-validator.js +110 -0
- package/dist/chat/locale-strings.d.ts +47 -0
- package/dist/chat/locale-strings.js +100 -0
- package/dist/chat/plan-json-schema.d.ts +133 -0
- package/dist/chat/plan-json-schema.js +112 -0
- package/dist/chat/planner-types.d.ts +120 -0
- package/dist/chat/planner-types.js +66 -0
- package/dist/chat/planner.d.ts +148 -0
- package/dist/chat/planner.js +1361 -0
- package/dist/chat/prompts.d.ts +67 -0
- package/dist/chat/prompts.js +356 -0
- package/dist/chat/provider-routing.d.ts +14 -0
- package/dist/chat/provider-routing.js +27 -0
- package/dist/chat/variation-pipeline.d.ts +135 -0
- package/dist/chat/variation-pipeline.js +837 -0
- package/dist/chat/vision-alt-generator.d.ts +35 -0
- package/dist/chat/vision-alt-generator.js +152 -0
- package/dist/cms/adapter.d.ts +62 -0
- package/dist/cms/adapter.js +1 -0
- package/dist/cms/bootstrap.d.ts +17 -0
- package/dist/cms/bootstrap.js +85 -0
- package/dist/cms/editor-api-adapter.d.ts +23 -0
- package/dist/cms/editor-api-adapter.js +71 -0
- package/dist/cms/index.d.ts +4 -0
- package/dist/cms/index.js +3 -0
- package/dist/cms/json-file-adapter.d.ts +11 -0
- package/dist/cms/json-file-adapter.js +62 -0
- package/dist/demo-mode.d.ts +59 -0
- package/dist/demo-mode.js +201 -0
- package/dist/errors.d.ts +67 -0
- package/dist/errors.js +129 -0
- package/dist/http/chat-stream-resumable.d.ts +108 -0
- package/dist/http/chat-stream-resumable.js +290 -0
- package/dist/http/chat-stream.d.ts +99 -0
- package/dist/http/chat-stream.js +92 -0
- package/dist/image/gdrive-client.d.ts +22 -0
- package/dist/image/gdrive-client.js +215 -0
- package/dist/image/image-helpers.d.ts +95 -0
- package/dist/image/image-helpers.js +488 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/jira/jira-approval.d.ts +22 -0
- package/dist/jira/jira-approval.js +51 -0
- package/dist/jira/jira-client.d.ts +44 -0
- package/dist/jira/jira-client.js +313 -0
- package/dist/jira/jira-poller.d.ts +46 -0
- package/dist/jira/jira-poller.js +184 -0
- package/dist/jira/jira-processor.d.ts +103 -0
- package/dist/jira/jira-processor.js +1085 -0
- package/dist/jira/jira-types.d.ts +117 -0
- package/dist/jira/jira-types.js +38 -0
- package/dist/logger.d.ts +12 -0
- package/dist/logger.js +28 -0
- package/dist/migration/mcp-server-stdio.d.ts +8 -0
- package/dist/migration/mcp-server-stdio.js +672 -0
- package/dist/migration/migration-prompt.d.ts +7 -0
- package/dist/migration/migration-prompt.js +197 -0
- package/dist/migration/migration-tools.d.ts +17 -0
- package/dist/migration/migration-tools.js +159 -0
- package/dist/migration/scrape-cache.d.ts +9 -0
- package/dist/migration/scrape-cache.js +19 -0
- package/dist/nlp/deterministic-planner-context.d.ts +141 -0
- package/dist/nlp/deterministic-planner-context.js +362 -0
- package/dist/nlp/deterministic-planner-pages.d.ts +26 -0
- package/dist/nlp/deterministic-planner-pages.js +170 -0
- package/dist/nlp/deterministic-planner-patches.d.ts +80 -0
- package/dist/nlp/deterministic-planner-patches.js +508 -0
- package/dist/nlp/deterministic-planner-refs.d.ts +33 -0
- package/dist/nlp/deterministic-planner-refs.js +164 -0
- package/dist/nlp/deterministic-planner-suggestions.d.ts +49 -0
- package/dist/nlp/deterministic-planner-suggestions.js +579 -0
- package/dist/nlp/deterministic-planner.d.ts +85 -0
- package/dist/nlp/deterministic-planner.js +1631 -0
- package/dist/nlp/intent-detection.d.ts +309 -0
- package/dist/nlp/intent-detection.js +730 -0
- package/dist/nlp/intent-helpers.d.ts +15 -0
- package/dist/nlp/intent-helpers.js +243 -0
- package/dist/nlp/intent-patterns.d.ts +40 -0
- package/dist/nlp/intent-patterns.js +223 -0
- package/dist/nlp/plan-normalizer.d.ts +41 -0
- package/dist/nlp/plan-normalizer.js +1537 -0
- package/dist/ops/destructive-action-gate.d.ts +44 -0
- package/dist/ops/destructive-action-gate.js +90 -0
- package/dist/ops/ops-engine.d.ts +151 -0
- package/dist/ops/ops-engine.js +1394 -0
- package/dist/publish/diff-engine.d.ts +18 -0
- package/dist/publish/diff-engine.js +305 -0
- package/dist/publish/publish-helpers.d.ts +87 -0
- package/dist/publish/publish-helpers.js +521 -0
- package/dist/publish/publish-target-registry.d.ts +7 -0
- package/dist/publish/publish-target-registry.js +61 -0
- package/dist/publish/publish-target.d.ts +81 -0
- package/dist/publish/publish-target.js +1 -0
- package/dist/publish/targets/deploy-hook.d.ts +13 -0
- package/dist/publish/targets/deploy-hook.js +123 -0
- package/dist/publish/targets/git.d.ts +13 -0
- package/dist/publish/targets/git.js +55 -0
- package/dist/publish/targets/site-contract.d.ts +19 -0
- package/dist/publish/targets/site-contract.js +124 -0
- package/dist/state/content-source.d.ts +17 -0
- package/dist/state/content-source.js +1 -0
- package/dist/state/in-memory-content-source.d.ts +27 -0
- package/dist/state/in-memory-content-source.js +51 -0
- package/dist/state/session-lock.d.ts +13 -0
- package/dist/state/session-lock.js +29 -0
- package/dist/state/session-state.d.ts +310 -0
- package/dist/state/session-state.js +1083 -0
- package/dist/state/sqlite-store-singleton.d.ts +31 -0
- package/dist/state/sqlite-store-singleton.js +170 -0
- package/dist/state/sqlite-store.d.ts +135 -0
- package/dist/state/sqlite-store.js +421 -0
- package/dist/telemetry/chat-telemetry.d.ts +105 -0
- package/dist/telemetry/chat-telemetry.js +247 -0
- package/dist/telemetry/eval-candidate-store.d.ts +50 -0
- package/dist/telemetry/eval-candidate-store.js +120 -0
- package/dist/telemetry/feedback-store.d.ts +34 -0
- package/dist/telemetry/feedback-store.js +76 -0
- package/dist/telemetry/jira-telemetry.d.ts +57 -0
- package/dist/telemetry/jira-telemetry.js +68 -0
- package/dist/telemetry/migration-telemetry.d.ts +35 -0
- package/dist/telemetry/migration-telemetry.js +40 -0
- package/dist/telemetry/usage.d.ts +24 -0
- package/dist/telemetry/usage.js +80 -0
- package/dist/tools/builtin-registrations.d.ts +12 -0
- package/dist/tools/builtin-registrations.js +33 -0
- package/dist/tools/builtins/gdrive-browse.d.ts +3 -0
- package/dist/tools/builtins/gdrive-browse.js +68 -0
- package/dist/tools/builtins/image-generate.d.ts +3 -0
- package/dist/tools/builtins/image-generate.js +211 -0
- package/dist/tools/builtins/unsplash-get-by-id.d.ts +23 -0
- package/dist/tools/builtins/unsplash-get-by-id.js +119 -0
- package/dist/tools/builtins/unsplash-search.d.ts +3 -0
- package/dist/tools/builtins/unsplash-search.js +74 -0
- package/dist/tools/executor.d.ts +23 -0
- package/dist/tools/executor.js +169 -0
- package/dist/tools/index.d.ts +5 -0
- package/dist/tools/index.js +5 -0
- package/dist/tools/registry.d.ts +21 -0
- package/dist/tools/registry.js +75 -0
- package/dist/tools/runtime.d.ts +27 -0
- package/dist/tools/runtime.js +48 -0
- package/dist/tools/schema-validator.d.ts +24 -0
- package/dist/tools/schema-validator.js +88 -0
- package/dist/tools/types.d.ts +86 -0
- package/dist/tools/types.js +1 -0
- package/dist/variation-images.d.ts +19 -0
- package/dist/variation-images.js +12 -0
- package/package.json +78 -0
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
import { getSessionDraft, getRecentEdits, getSiteConfig, orderSlugsHomeFirst } from "../state/session-state.js";
|
|
2
|
+
import { resolveReferencesFromMessage } from "./deterministic-planner-refs.js";
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// Path traversal
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
export function readPathValue(root, path) {
|
|
7
|
+
if (!path)
|
|
8
|
+
return undefined;
|
|
9
|
+
const parts = [];
|
|
10
|
+
const regex = /([^[.\]]+)|\[(\d+)\]/g;
|
|
11
|
+
for (const match of path.matchAll(regex)) {
|
|
12
|
+
if (match[1])
|
|
13
|
+
parts.push(match[1]);
|
|
14
|
+
if (match[2])
|
|
15
|
+
parts.push(Number(match[2]));
|
|
16
|
+
}
|
|
17
|
+
let current = root;
|
|
18
|
+
for (const part of parts) {
|
|
19
|
+
if (typeof part === "number") {
|
|
20
|
+
if (!Array.isArray(current))
|
|
21
|
+
return undefined;
|
|
22
|
+
current = current[part];
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
if (!current || typeof current !== "object" || Array.isArray(current))
|
|
26
|
+
return undefined;
|
|
27
|
+
current = current[part];
|
|
28
|
+
}
|
|
29
|
+
return current;
|
|
30
|
+
}
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
// Image URL helpers
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
/**
|
|
35
|
+
* Given block props and an editablePath targeting an alt-text field,
|
|
36
|
+
* resolve the companion image URL for vision-powered alt text generation.
|
|
37
|
+
*
|
|
38
|
+
* Examples:
|
|
39
|
+
* - "imageAlt" → props.imageUrl
|
|
40
|
+
* - "cards[2].imageAlt" → props.cards[2].imageUrl
|
|
41
|
+
* - "items[0].image.alt" → props.items[0].image.src
|
|
42
|
+
*/
|
|
43
|
+
export function resolveImageUrlForAltField(blockProps, editablePath) {
|
|
44
|
+
if (!editablePath)
|
|
45
|
+
return undefined;
|
|
46
|
+
// Only trigger for paths ending in an alt-text field
|
|
47
|
+
if (!/(^|[.])imageAlt$/.test(editablePath) && !/(^|[.])alt$/.test(editablePath))
|
|
48
|
+
return undefined;
|
|
49
|
+
let companionPath;
|
|
50
|
+
if (editablePath.endsWith(".alt")) {
|
|
51
|
+
// e.g. "items[0].image.alt" → "items[0].image.src"
|
|
52
|
+
companionPath = editablePath.replace(/\.alt$/, ".src");
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
// e.g. "imageAlt" or "cards[2].imageAlt" → "imageUrl" or "cards[2].imageUrl"
|
|
56
|
+
companionPath = editablePath.replace(/imageAlt$/, "imageUrl");
|
|
57
|
+
}
|
|
58
|
+
const value = readPathValue(blockProps, companionPath);
|
|
59
|
+
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
60
|
+
}
|
|
61
|
+
function isLocalUrl(url) {
|
|
62
|
+
try {
|
|
63
|
+
const parsed = new URL(url);
|
|
64
|
+
return parsed.hostname === "localhost" || parsed.hostname === "127.0.0.1" || parsed.hostname === "[::1]";
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
const MIME_BY_EXT = {
|
|
71
|
+
".png": "image/png",
|
|
72
|
+
".jpg": "image/jpeg",
|
|
73
|
+
".jpeg": "image/jpeg",
|
|
74
|
+
".gif": "image/gif",
|
|
75
|
+
".webp": "image/webp",
|
|
76
|
+
".svg": "image/svg+xml"
|
|
77
|
+
};
|
|
78
|
+
/**
|
|
79
|
+
* Fetch an image URL and return base64 + media type.
|
|
80
|
+
* For localhost URLs the AI APIs can't reach, we fetch locally.
|
|
81
|
+
* For remote URLs, returns null so the caller can use the URL directly.
|
|
82
|
+
*/
|
|
83
|
+
export async function fetchImageAsBase64(url) {
|
|
84
|
+
if (!isLocalUrl(url))
|
|
85
|
+
return null;
|
|
86
|
+
try {
|
|
87
|
+
const res = await fetch(url);
|
|
88
|
+
if (!res.ok)
|
|
89
|
+
return null;
|
|
90
|
+
const buffer = Buffer.from(await res.arrayBuffer());
|
|
91
|
+
const contentType = res.headers.get("content-type");
|
|
92
|
+
let mediaType = contentType?.split(";")[0]?.trim() ?? "";
|
|
93
|
+
if (!mediaType || mediaType === "application/octet-stream") {
|
|
94
|
+
const ext = url.match(/(\.[a-z]+)(?:\?|$)/i)?.[1]?.toLowerCase() ?? "";
|
|
95
|
+
mediaType = MIME_BY_EXT[ext] ?? "image/jpeg";
|
|
96
|
+
}
|
|
97
|
+
return { base64: buffer.toString("base64"), mediaType };
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
const MAX_ATTACHMENT_FETCH_BYTES = 32 * 1024 * 1024;
|
|
104
|
+
const ATTACHMENT_FETCH_TIMEOUT_MS = 15_000;
|
|
105
|
+
const ALLOWED_IMAGE_MEDIA_TYPES = new Set(["image/png", "image/jpeg", "image/webp", "image/gif"]);
|
|
106
|
+
function allowedAttachmentOrigin() {
|
|
107
|
+
return (process.env.ORCHESTRATOR_PUBLIC_ORIGIN ?? "http://localhost:4200").replace(/\/+$/, "");
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* SSRF guard: only fetch attachment URLs that point at this orchestrator's own
|
|
111
|
+
* `/generated-images/` store (where `/attachment/upload` writes them). Any
|
|
112
|
+
* other host or path — cloud metadata endpoints, internal services, arbitrary
|
|
113
|
+
* remote URLs — is rejected before we ever issue the request. Attachment URLs
|
|
114
|
+
* arrive in the client-supplied `/chat` body, so this cannot be skipped.
|
|
115
|
+
*/
|
|
116
|
+
function isAllowedAttachmentUrl(rawUrl) {
|
|
117
|
+
try {
|
|
118
|
+
const url = new URL(rawUrl);
|
|
119
|
+
const allowed = new URL(allowedAttachmentOrigin());
|
|
120
|
+
return url.origin === allowed.origin && url.pathname.startsWith("/generated-images/");
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
function normalizeImageMediaType(raw) {
|
|
127
|
+
const value = (raw ?? "").split(";")[0]?.trim().toLowerCase();
|
|
128
|
+
return value && ALLOWED_IMAGE_MEDIA_TYPES.has(value) ? value : "image/png";
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Fetch each chat attachment's bytes and return base64 + media type for the
|
|
132
|
+
* planner to embed as native multimodal content blocks. We always base64 the
|
|
133
|
+
* bytes (rather than pass a URL) so all three providers behave uniformly —
|
|
134
|
+
* OpenAI's `file` part requires inline data, and dev URLs (localhost) aren't
|
|
135
|
+
* reachable by the model APIs anyway. URLs are SSRF-allowlisted, fetches are
|
|
136
|
+
* bounded by a timeout and a byte cap, and anything that fails is skipped.
|
|
137
|
+
*/
|
|
138
|
+
export async function resolveAttachmentsForLlm(attachments) {
|
|
139
|
+
if (!attachments || attachments.length === 0)
|
|
140
|
+
return [];
|
|
141
|
+
const resolved = [];
|
|
142
|
+
for (const attachment of attachments) {
|
|
143
|
+
if (!isAllowedAttachmentUrl(attachment.url))
|
|
144
|
+
continue;
|
|
145
|
+
try {
|
|
146
|
+
const res = await fetch(attachment.url, { signal: AbortSignal.timeout(ATTACHMENT_FETCH_TIMEOUT_MS) });
|
|
147
|
+
if (!res.ok)
|
|
148
|
+
continue;
|
|
149
|
+
const declaredLength = Number(res.headers.get("content-length") ?? "");
|
|
150
|
+
if (Number.isFinite(declaredLength) && declaredLength > MAX_ATTACHMENT_FETCH_BYTES)
|
|
151
|
+
continue;
|
|
152
|
+
const buffer = Buffer.from(await res.arrayBuffer());
|
|
153
|
+
if (buffer.byteLength > MAX_ATTACHMENT_FETCH_BYTES)
|
|
154
|
+
continue;
|
|
155
|
+
const mediaType = attachment.kind === "pdf"
|
|
156
|
+
? "application/pdf"
|
|
157
|
+
: normalizeImageMediaType(attachment.mimeType || res.headers.get("content-type"));
|
|
158
|
+
resolved.push({ kind: attachment.kind, mediaType, base64: buffer.toString("base64"), name: attachment.name });
|
|
159
|
+
}
|
|
160
|
+
catch {
|
|
161
|
+
// Skip attachments we can't fetch (timeout, network error); planning proceeds with the rest.
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return resolved;
|
|
165
|
+
}
|
|
166
|
+
// ---------------------------------------------------------------------------
|
|
167
|
+
// Block snapshot
|
|
168
|
+
// ---------------------------------------------------------------------------
|
|
169
|
+
export function selectedBlockSnapshot(args) {
|
|
170
|
+
if (!args.activeBlockId)
|
|
171
|
+
return null;
|
|
172
|
+
const block = args.currentPage.blocks.find((item) => item.id === args.activeBlockId);
|
|
173
|
+
if (!block)
|
|
174
|
+
return null;
|
|
175
|
+
const editablePath = typeof args.activeEditablePath === "string" && args.activeEditablePath.length > 0 ? args.activeEditablePath : null;
|
|
176
|
+
return {
|
|
177
|
+
id: block.id,
|
|
178
|
+
type: block.type,
|
|
179
|
+
props: block.props,
|
|
180
|
+
selectedEditablePath: editablePath,
|
|
181
|
+
selectedEditableValue: editablePath ? readPathValue(block.props, editablePath) ?? null : null
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
// ---------------------------------------------------------------------------
|
|
185
|
+
// Array prop metadata
|
|
186
|
+
// ---------------------------------------------------------------------------
|
|
187
|
+
export function arrayPropLengths(props) {
|
|
188
|
+
const out = {};
|
|
189
|
+
for (const [key, value] of Object.entries(props)) {
|
|
190
|
+
if (!Array.isArray(value))
|
|
191
|
+
continue;
|
|
192
|
+
const labels = [];
|
|
193
|
+
for (const item of value) {
|
|
194
|
+
if (typeof item === "object" && item !== null) {
|
|
195
|
+
const labelValue = item.label ??
|
|
196
|
+
item.title ??
|
|
197
|
+
item.heading ??
|
|
198
|
+
item.question ??
|
|
199
|
+
item.name;
|
|
200
|
+
if (typeof labelValue === "string")
|
|
201
|
+
labels.push(labelValue);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
out[key] = labels.length > 0 ? { length: value.length, labels } : { length: value.length };
|
|
205
|
+
}
|
|
206
|
+
return out;
|
|
207
|
+
}
|
|
208
|
+
// ---------------------------------------------------------------------------
|
|
209
|
+
// Text hints — lightweight prop snapshot for brand/theme context
|
|
210
|
+
// ---------------------------------------------------------------------------
|
|
211
|
+
const TEXT_HINT_KEYS = ["heading", "subheading", "title", "body", "ctaText", "label", "sectionTitle"];
|
|
212
|
+
// How many of a block's own props to fall back on when it uses none of the
|
|
213
|
+
// names above. Bounded because these hints go into every planner call.
|
|
214
|
+
const MAX_FALLBACK_HINTS = 3;
|
|
215
|
+
// Values that carry no brand/theme signal but do cost tokens.
|
|
216
|
+
const NON_CONTENT_KEY = /(?:url|href|src|id|key|color|slug|icon|variant|align|position)$/i;
|
|
217
|
+
const NON_CONTENT_VALUE = /^(?:https?:\/\/|\/|#[0-9a-f]{3,8}$|data:)/i;
|
|
218
|
+
function truncateHint(value) {
|
|
219
|
+
// Truncate long body text to save tokens
|
|
220
|
+
return value.length > 120 ? value.slice(0, 120) + "…" : value;
|
|
221
|
+
}
|
|
222
|
+
function isContentString(key, value) {
|
|
223
|
+
if (typeof value !== "string" || value.trim().length === 0)
|
|
224
|
+
return false;
|
|
225
|
+
if (NON_CONTENT_KEY.test(key))
|
|
226
|
+
return false;
|
|
227
|
+
return !NON_CONTENT_VALUE.test(value.trim());
|
|
228
|
+
}
|
|
229
|
+
function extractTextHints(props) {
|
|
230
|
+
const hints = {};
|
|
231
|
+
for (const key of TEXT_HINT_KEYS) {
|
|
232
|
+
const value = props[key];
|
|
233
|
+
if (typeof value === "string" && value.trim().length > 0) {
|
|
234
|
+
hints[key] = truncateHint(value);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
if (Object.keys(hints).length >= MAX_FALLBACK_HINTS)
|
|
238
|
+
return hints;
|
|
239
|
+
// The block names its text props something else — every custom block on a
|
|
240
|
+
// real site does. Without this a block using none of the names above reaches
|
|
241
|
+
// the planner as `props: {}`, so it can see the page structure but none of
|
|
242
|
+
// its content, and cannot resolve "the hero heading" to a block. Tops up
|
|
243
|
+
// rather than replaces, so a block mixing canonical and custom names (a
|
|
244
|
+
// `heading` plus an `eyebrow`) surfaces both.
|
|
245
|
+
for (const [key, value] of Object.entries(props)) {
|
|
246
|
+
if (key in hints || !isContentString(key, value))
|
|
247
|
+
continue;
|
|
248
|
+
hints[key] = truncateHint(value);
|
|
249
|
+
if (Object.keys(hints).length >= MAX_FALLBACK_HINTS)
|
|
250
|
+
break;
|
|
251
|
+
}
|
|
252
|
+
return hints;
|
|
253
|
+
}
|
|
254
|
+
// ---------------------------------------------------------------------------
|
|
255
|
+
// Page intent summary
|
|
256
|
+
// ---------------------------------------------------------------------------
|
|
257
|
+
export function pageIntentSummary(args) {
|
|
258
|
+
const { slug, currentPage } = args;
|
|
259
|
+
const typeCounts = new Map();
|
|
260
|
+
for (const block of currentPage.blocks) {
|
|
261
|
+
typeCounts.set(block.type, (typeCounts.get(block.type) ?? 0) + 1);
|
|
262
|
+
}
|
|
263
|
+
const composition = Array.from(typeCounts.entries())
|
|
264
|
+
.map(([type, count]) => (count > 1 ? `${type} x${count}` : type))
|
|
265
|
+
.join(", ");
|
|
266
|
+
const hero = currentPage.blocks.find((b) => b.type === "Hero");
|
|
267
|
+
const heroHeading = hero && typeof hero.props.heading === "string" ? hero.props.heading : "";
|
|
268
|
+
const routeLabel = slug === "/" ? "Home page" : `Page ${slug}`;
|
|
269
|
+
const headingPart = heroHeading ? ` Hero message: "${heroHeading}".` : "";
|
|
270
|
+
return `${routeLabel} with ${currentPage.blocks.length} blocks (${composition}).${headingPart}`;
|
|
271
|
+
}
|
|
272
|
+
// ---------------------------------------------------------------------------
|
|
273
|
+
// Planner context pack
|
|
274
|
+
// ---------------------------------------------------------------------------
|
|
275
|
+
export function plannerContextPack(args) {
|
|
276
|
+
const { session, slug, message, currentPage, activeBlockId, activeBlockType, activeEditablePath } = args;
|
|
277
|
+
const pageRoutes = orderSlugsHomeFirst(Array.from(getSessionDraft(session).keys()));
|
|
278
|
+
const siteConfigSummary = summarizeSiteConfigForPlanner(getSiteConfig(session), pageRoutes);
|
|
279
|
+
const selectedIdx = activeBlockId ? currentPage.blocks.findIndex((b) => b.id === activeBlockId) : -1;
|
|
280
|
+
const neighbors = selectedIdx >= 0
|
|
281
|
+
? {
|
|
282
|
+
previous: selectedIdx > 0 ? currentPage.blocks[selectedIdx - 1] : null,
|
|
283
|
+
next: selectedIdx < currentPage.blocks.length - 1 ? currentPage.blocks[selectedIdx + 1] : null
|
|
284
|
+
}
|
|
285
|
+
: { previous: null, next: null };
|
|
286
|
+
return {
|
|
287
|
+
route: slug,
|
|
288
|
+
pageRoutes,
|
|
289
|
+
blockCount: currentPage.blocks.length,
|
|
290
|
+
// Spread so the key is optional on the inferred type — most turns carry none.
|
|
291
|
+
...(args.attachments !== undefined ? { attachments: args.attachments } : {}),
|
|
292
|
+
selected: {
|
|
293
|
+
blockId: activeBlockId ?? null,
|
|
294
|
+
blockType: activeBlockType ?? null,
|
|
295
|
+
editablePath: activeEditablePath ?? null,
|
|
296
|
+
block: selectedBlockSnapshot({ currentPage, activeBlockId, activeEditablePath }),
|
|
297
|
+
imageUrlForVision: activeBlockId && activeEditablePath
|
|
298
|
+
? resolveImageUrlForAltField((currentPage.blocks.find((b) => b.id === activeBlockId)?.props ?? {}), activeEditablePath) ?? null
|
|
299
|
+
: null
|
|
300
|
+
},
|
|
301
|
+
neighbors: {
|
|
302
|
+
previous: neighbors.previous ? { id: neighbors.previous.id, type: neighbors.previous.type } : null,
|
|
303
|
+
next: neighbors.next ? { id: neighbors.next.id, type: neighbors.next.type } : null
|
|
304
|
+
},
|
|
305
|
+
pageOutline: currentPage.blocks.map((b) => {
|
|
306
|
+
const bProps = b.props;
|
|
307
|
+
const arrProps = arrayPropLengths(bProps);
|
|
308
|
+
// Selected block: send full props for precise editing context
|
|
309
|
+
if (b.id === activeBlockId || args.includeFullProps) {
|
|
310
|
+
return { id: b.id, type: b.type, props: structuredClone(bProps), arrayProps: arrProps };
|
|
311
|
+
}
|
|
312
|
+
// Other blocks: key text props for brand/theme context + array metadata.
|
|
313
|
+
// Full scalar props are available via includeFullProps for content queries/translations.
|
|
314
|
+
const textHints = extractTextHints(bProps);
|
|
315
|
+
return { id: b.id, type: b.type, props: textHints, arrayProps: arrProps };
|
|
316
|
+
}),
|
|
317
|
+
pageMeta: currentPage.meta ?? null,
|
|
318
|
+
pageIntent: pageIntentSummary({ slug, currentPage }),
|
|
319
|
+
recentSuccessfulEdits: getRecentEdits(session, slug),
|
|
320
|
+
resolvedReferences: resolveReferencesFromMessage({ message, currentPage, activeBlockId }),
|
|
321
|
+
siteConfig: siteConfigSummary
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
// Project SiteConfig down to the header-editable surface (name, logo, nav)
|
|
325
|
+
// plus default-label hints for routes that have no override yet, so the planner
|
|
326
|
+
// can issue update_site_config ops with confidence.
|
|
327
|
+
function summarizeSiteConfigForPlanner(config, pageRoutes) {
|
|
328
|
+
const navLabels = {};
|
|
329
|
+
for (const [slug, label] of Object.entries(config.navLabels ?? {})) {
|
|
330
|
+
if (typeof label === "string" && label.length > 0)
|
|
331
|
+
navLabels[slug] = label;
|
|
332
|
+
}
|
|
333
|
+
const navGroups = {};
|
|
334
|
+
for (const [label, slugs] of Object.entries(config.navGroups ?? {})) {
|
|
335
|
+
if (Array.isArray(slugs))
|
|
336
|
+
navGroups[label] = slugs.filter((s) => typeof s === "string");
|
|
337
|
+
}
|
|
338
|
+
const defaultLabels = {};
|
|
339
|
+
for (const slug of pageRoutes) {
|
|
340
|
+
if (navLabels[slug])
|
|
341
|
+
continue;
|
|
342
|
+
defaultLabels[slug] = deriveDefaultNavLabel(slug);
|
|
343
|
+
}
|
|
344
|
+
return {
|
|
345
|
+
name: typeof config.name === "string" && config.name.length > 0 ? config.name : null,
|
|
346
|
+
logo: typeof config.logo === "string" && config.logo.length > 0 ? config.logo : null,
|
|
347
|
+
navLabels,
|
|
348
|
+
navGroups,
|
|
349
|
+
defaultNavLabels: defaultLabels,
|
|
350
|
+
editPath: "use update_site_config to change name, logo, navLabels, or navGroups"
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
function deriveDefaultNavLabel(slug) {
|
|
354
|
+
if (slug === "/")
|
|
355
|
+
return "Home";
|
|
356
|
+
const last = slug.replace(/\/+$/, "").split("/").pop() ?? slug;
|
|
357
|
+
return last
|
|
358
|
+
.split(/[-_]/)
|
|
359
|
+
.filter(Boolean)
|
|
360
|
+
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
361
|
+
.join(" ");
|
|
362
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { type PageDoc } from "@avocadostudio-ai/shared";
|
|
2
|
+
export declare function nextAvailableSlug(session: string, baseSlug: string): string;
|
|
3
|
+
export declare function createPageBlocks(args: {
|
|
4
|
+
requestedSlug: string;
|
|
5
|
+
userMessage?: string;
|
|
6
|
+
}): import("@avocadostudio-ai/shared").BlockInstance[];
|
|
7
|
+
export declare function buildCreatePagePlan(args: {
|
|
8
|
+
session: string;
|
|
9
|
+
requestedSlug: string;
|
|
10
|
+
assumptions?: string[];
|
|
11
|
+
userMessage?: string;
|
|
12
|
+
}): {
|
|
13
|
+
intent: "needs_clarification";
|
|
14
|
+
summary_for_user: string;
|
|
15
|
+
change_log: string[];
|
|
16
|
+
ops: never[];
|
|
17
|
+
} | {
|
|
18
|
+
intent: "edit_plan";
|
|
19
|
+
summary_for_user: string;
|
|
20
|
+
change_log: string[];
|
|
21
|
+
ops: {
|
|
22
|
+
op: "create_page";
|
|
23
|
+
page: PageDoc;
|
|
24
|
+
}[];
|
|
25
|
+
} | null;
|
|
26
|
+
export declare function isPageRouteRenameRequest(message?: string): boolean;
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { normalizeRouteCandidate, toSeedSlug } from "./intent-helpers.js";
|
|
2
|
+
import { stripSiteContextEnvelope, extractSiteContextLineValue } from "./intent-detection.js";
|
|
3
|
+
import { defaultPropsForType, pageIdFromSlug, pageTitleFromSlug } from "./plan-normalizer.js";
|
|
4
|
+
import { getSessionDraft } from "../state/session-state.js";
|
|
5
|
+
import { titleCaseSentence } from "./deterministic-planner-suggestions.js";
|
|
6
|
+
export function nextAvailableSlug(session, baseSlug) {
|
|
7
|
+
const draft = getSessionDraft(session);
|
|
8
|
+
if (!draft.has(baseSlug))
|
|
9
|
+
return baseSlug;
|
|
10
|
+
let idx = 2;
|
|
11
|
+
while (draft.has(`${baseSlug}-${idx}`))
|
|
12
|
+
idx += 1;
|
|
13
|
+
return `${baseSlug}-${idx}`;
|
|
14
|
+
}
|
|
15
|
+
export function createPageBlocks(args) {
|
|
16
|
+
const seed = toSeedSlug(args.requestedSlug.replace(/^\//, "") || "new-page") || "new-page";
|
|
17
|
+
const rawMessage = typeof args.userMessage === "string" ? args.userMessage : "";
|
|
18
|
+
const cleanMessage = stripSiteContextEnvelope(rawMessage);
|
|
19
|
+
const lowerMessage = cleanMessage.toLowerCase();
|
|
20
|
+
const asksIntentPage = /\b(intent|purpose|mission)\b/.test(lowerMessage);
|
|
21
|
+
const asksHero = /\bhero\b/.test(lowerMessage);
|
|
22
|
+
const asksText = /\b(rich[\s-]?text|text(?:\s+section|\s+block)?|copy)\b/.test(lowerMessage);
|
|
23
|
+
const asksCta = /\bcta\b|\bcall to action\b|\baction button\b/.test(lowerMessage);
|
|
24
|
+
const asksFeatures = /\bfeatures?\b/.test(lowerMessage);
|
|
25
|
+
const asksFaq = /\bfaq\b/.test(lowerMessage);
|
|
26
|
+
const asksTestimonials = /\btestimonials?\b/.test(lowerMessage);
|
|
27
|
+
const asksCards = /\bcards?\b/.test(lowerMessage);
|
|
28
|
+
const asksStructuredSections = asksHero || asksText || asksCta || asksFeatures || asksFaq || asksTestimonials || asksCards;
|
|
29
|
+
const sitePurpose = extractSiteContextLineValue(rawMessage, "Site purpose");
|
|
30
|
+
const pageTitle = pageTitleFromSlug(args.requestedSlug);
|
|
31
|
+
// Extract explicit hero heading from message (e.g. "hero titled 'About Us'")
|
|
32
|
+
const heroTitleMatch = cleanMessage.match(/hero\s+(?:titled?|called|named|with\s+(?:the\s+)?(?:title|heading))\s+['"]([^'"]+)['"]/i);
|
|
33
|
+
const heroHeading = heroTitleMatch?.[1] ?? (asksIntentPage ? "Purpose of This Site" : pageTitle);
|
|
34
|
+
const heroSubheading = sitePurpose
|
|
35
|
+
? `This page explains ${sitePurpose}.`
|
|
36
|
+
: asksIntentPage
|
|
37
|
+
? "This page explains what this site is for, who it helps, and the value it provides."
|
|
38
|
+
: `Use this page to introduce ${pageTitle}.`;
|
|
39
|
+
const ctaText = asksIntentPage ? "Explore the site" : "Get started";
|
|
40
|
+
const ctaHref = "/";
|
|
41
|
+
const ctaTitle = asksIntentPage ? "Ready to learn more?" : `Ready to explore ${pageTitle}?`;
|
|
42
|
+
const ctaDescription = sitePurpose
|
|
43
|
+
? `Continue to discover how ${sitePurpose} translates into concrete next steps.`
|
|
44
|
+
: asksIntentPage
|
|
45
|
+
? "Review the key points, then continue to the next step."
|
|
46
|
+
: "Continue to the next step.";
|
|
47
|
+
const richTextBody = sitePurpose
|
|
48
|
+
? `Site intent: ${titleCaseSentence(sitePurpose)}.\n\nUse this section to explain the problem the site solves, who it serves, and what users should do next.`
|
|
49
|
+
: asksIntentPage
|
|
50
|
+
? "This page captures the site intent: why it exists, who it serves, and what outcomes it helps users achieve.\n\nUse this section to add concrete details and examples."
|
|
51
|
+
: `Use this section to describe ${pageTitle} in detail.\n\nAdd context, benefits, and a clear next step for visitors.`;
|
|
52
|
+
const blocks = [
|
|
53
|
+
{
|
|
54
|
+
id: `b_hero_${seed}`,
|
|
55
|
+
type: "Hero",
|
|
56
|
+
props: {
|
|
57
|
+
...defaultPropsForType("Hero"),
|
|
58
|
+
heading: heroHeading,
|
|
59
|
+
subheading: heroSubheading,
|
|
60
|
+
ctaText,
|
|
61
|
+
ctaHref
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
];
|
|
65
|
+
if (asksStructuredSections && asksText) {
|
|
66
|
+
blocks.push({
|
|
67
|
+
id: `b_richtext_${seed}`,
|
|
68
|
+
type: "RichText",
|
|
69
|
+
props: {
|
|
70
|
+
...defaultPropsForType("RichText"),
|
|
71
|
+
body: richTextBody
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
if (asksStructuredSections && asksFeatures) {
|
|
76
|
+
blocks.push({
|
|
77
|
+
id: `b_features_${seed}`,
|
|
78
|
+
type: "FeatureGrid",
|
|
79
|
+
props: defaultPropsForType("FeatureGrid")
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
if (asksStructuredSections && asksTestimonials) {
|
|
83
|
+
blocks.push({
|
|
84
|
+
id: `b_testimonials_${seed}`,
|
|
85
|
+
type: "Testimonials",
|
|
86
|
+
props: defaultPropsForType("Testimonials")
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
if (asksStructuredSections && asksFaq) {
|
|
90
|
+
blocks.push({
|
|
91
|
+
id: `b_faq_${seed}`,
|
|
92
|
+
type: "FAQAccordion",
|
|
93
|
+
props: defaultPropsForType("FAQAccordion")
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
if (asksStructuredSections && asksCards) {
|
|
97
|
+
blocks.push({
|
|
98
|
+
id: `b_cardgrid_${seed}`,
|
|
99
|
+
type: "CardGrid",
|
|
100
|
+
props: defaultPropsForType("CardGrid")
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
if (asksStructuredSections && asksCta) {
|
|
104
|
+
blocks.push({
|
|
105
|
+
id: `b_cta_${seed}`,
|
|
106
|
+
type: "CTA",
|
|
107
|
+
props: {
|
|
108
|
+
...defaultPropsForType("CTA"),
|
|
109
|
+
title: ctaTitle,
|
|
110
|
+
description: ctaDescription,
|
|
111
|
+
ctaText,
|
|
112
|
+
ctaHref
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
return blocks;
|
|
117
|
+
}
|
|
118
|
+
export function buildCreatePagePlan(args) {
|
|
119
|
+
const normalizedRequested = normalizeRouteCandidate(args.requestedSlug);
|
|
120
|
+
if (!normalizedRequested || normalizedRequested === "/")
|
|
121
|
+
return null;
|
|
122
|
+
const draft = getSessionDraft(args.session);
|
|
123
|
+
if (draft.has(normalizedRequested)) {
|
|
124
|
+
return {
|
|
125
|
+
intent: "needs_clarification",
|
|
126
|
+
summary_for_user: `Page ${normalizedRequested} already exists. Provide a different page path.`,
|
|
127
|
+
change_log: args.assumptions ?? [],
|
|
128
|
+
ops: []
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
const now = new Date().toISOString();
|
|
132
|
+
const page = {
|
|
133
|
+
id: pageIdFromSlug(normalizedRequested),
|
|
134
|
+
slug: normalizedRequested,
|
|
135
|
+
title: pageTitleFromSlug(normalizedRequested),
|
|
136
|
+
updatedAt: now,
|
|
137
|
+
blocks: createPageBlocks({ requestedSlug: normalizedRequested, userMessage: args.userMessage })
|
|
138
|
+
};
|
|
139
|
+
return {
|
|
140
|
+
intent: "edit_plan",
|
|
141
|
+
summary_for_user: `Created page ${normalizedRequested}.`,
|
|
142
|
+
change_log: [...(args.assumptions ?? []), `Created new page ${normalizedRequested}.`],
|
|
143
|
+
ops: [{ op: "create_page", page }]
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
export function isPageRouteRenameRequest(message) {
|
|
147
|
+
if (!message)
|
|
148
|
+
return false;
|
|
149
|
+
const lower = message.toLowerCase();
|
|
150
|
+
const mentionsRoute = lower.includes("slug") || lower.includes("path") || lower.includes("route") || /\/[a-z0-9/_-]*/i.test(message);
|
|
151
|
+
const asksRename = lower.includes("rename") ||
|
|
152
|
+
lower.includes("change") ||
|
|
153
|
+
lower.includes("update") ||
|
|
154
|
+
lower.includes("move") ||
|
|
155
|
+
lower.includes("switch");
|
|
156
|
+
const mentionsPage = lower.includes("page") || lower.includes("this page");
|
|
157
|
+
if (mentionsRoute && asksRename && mentionsPage)
|
|
158
|
+
return true;
|
|
159
|
+
// "rename page to Our community" — natural language name after "to"
|
|
160
|
+
// Exclude position words (first/last/top/bottom) which indicate nav move, not rename
|
|
161
|
+
const hasRenameVerb = lower.includes("rename") || lower.includes("change") || lower.includes("switch");
|
|
162
|
+
const hasTarget = /\bto\s+[A-Za-z]/.test(message) &&
|
|
163
|
+
!/\bto\s+(first|last|top|bottom|start|end|beginning)\b/i.test(message);
|
|
164
|
+
if (hasRenameVerb && mentionsPage && hasTarget)
|
|
165
|
+
return true;
|
|
166
|
+
// "rename to Olive oil" — implicit current page (no "page" keyword needed when verb is "rename")
|
|
167
|
+
if (lower.includes("rename") && hasTarget)
|
|
168
|
+
return true;
|
|
169
|
+
return false;
|
|
170
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import type { PageDoc } from "@avocadostudio-ai/shared";
|
|
2
|
+
export declare function extractAudienceTarget(message: string): string;
|
|
3
|
+
export declare function extractAudienceTargets(message: string): string[];
|
|
4
|
+
export declare function titleCaseWords(text: string): string;
|
|
5
|
+
export declare function addAudienceSuffix(value: string, audience: string): string;
|
|
6
|
+
export declare function audiencePatchForBlock(block: PageDoc["blocks"][number], audience: string): Record<string, unknown>;
|
|
7
|
+
export declare function coercePatchForBlock(block: PageDoc["blocks"][number], rawPatch: unknown): Record<string, unknown>;
|
|
8
|
+
export declare function parseIndexedPath(path: string): {
|
|
9
|
+
listKey: string;
|
|
10
|
+
index: number;
|
|
11
|
+
leaf: string;
|
|
12
|
+
} | null;
|
|
13
|
+
export declare function inferSimpleFieldPatchFromMessage(message: string): Record<string, unknown> | null;
|
|
14
|
+
export declare function isRewriteRequest(message: string): boolean;
|
|
15
|
+
export declare function isTranslationRequest(message: string): boolean;
|
|
16
|
+
export declare function shouldKeepRichTextTitleOnTranslate(args: {
|
|
17
|
+
target: PageDoc["blocks"][number];
|
|
18
|
+
activeEditablePath?: string;
|
|
19
|
+
message: string;
|
|
20
|
+
fullPatch: Record<string, unknown>;
|
|
21
|
+
}): boolean;
|
|
22
|
+
export declare function inferFieldHintFromMessage(message: string, allowedKeys: string[]): string | null;
|
|
23
|
+
export declare function rewriteFromExisting(existing: string, message: string): string;
|
|
24
|
+
export declare function coercePatchForEditablePath(block: PageDoc["blocks"][number], editablePath: string | undefined, rawPatch: unknown, message: string): {
|
|
25
|
+
patch: Record<string, unknown>;
|
|
26
|
+
changedKeys: string[];
|
|
27
|
+
rootKey: string;
|
|
28
|
+
} | null;
|
|
29
|
+
export declare function quotedText(message: string): string | undefined;
|
|
30
|
+
/**
|
|
31
|
+
* How many items a list-append request asks for. Only a quantity that directly follows
|
|
32
|
+
* the add verb counts ("add 3 cards", "add two more faqs") — a number elsewhere ("cards
|
|
33
|
+
* about our 5 values") is content, not a count. Recognises digits, number words, and the
|
|
34
|
+
* vague quantifiers "a couple" (2) / "a few" / "several" (3). Clamped to [1, 10] so a
|
|
35
|
+
* typo like "add 500 cards" can't blow up the patch.
|
|
36
|
+
*/
|
|
37
|
+
export declare function parseRequestedCount(message: string): number;
|
|
38
|
+
export declare function buildListAppendPatch(block: PageDoc["blocks"][number], message: string): {
|
|
39
|
+
items: Record<string, unknown>[];
|
|
40
|
+
features?: undefined;
|
|
41
|
+
cards?: undefined;
|
|
42
|
+
tabs?: undefined;
|
|
43
|
+
stats?: undefined;
|
|
44
|
+
rows?: undefined;
|
|
45
|
+
} | {
|
|
46
|
+
features: Record<string, unknown>[];
|
|
47
|
+
items?: undefined;
|
|
48
|
+
cards?: undefined;
|
|
49
|
+
tabs?: undefined;
|
|
50
|
+
stats?: undefined;
|
|
51
|
+
rows?: undefined;
|
|
52
|
+
} | {
|
|
53
|
+
cards: Record<string, unknown>[];
|
|
54
|
+
items?: undefined;
|
|
55
|
+
features?: undefined;
|
|
56
|
+
tabs?: undefined;
|
|
57
|
+
stats?: undefined;
|
|
58
|
+
rows?: undefined;
|
|
59
|
+
} | {
|
|
60
|
+
tabs: Record<string, unknown>[];
|
|
61
|
+
items?: undefined;
|
|
62
|
+
features?: undefined;
|
|
63
|
+
cards?: undefined;
|
|
64
|
+
stats?: undefined;
|
|
65
|
+
rows?: undefined;
|
|
66
|
+
} | {
|
|
67
|
+
stats: Record<string, unknown>[];
|
|
68
|
+
items?: undefined;
|
|
69
|
+
features?: undefined;
|
|
70
|
+
cards?: undefined;
|
|
71
|
+
tabs?: undefined;
|
|
72
|
+
rows?: undefined;
|
|
73
|
+
} | {
|
|
74
|
+
rows: unknown[][];
|
|
75
|
+
items?: undefined;
|
|
76
|
+
features?: undefined;
|
|
77
|
+
cards?: undefined;
|
|
78
|
+
tabs?: undefined;
|
|
79
|
+
stats?: undefined;
|
|
80
|
+
} | null;
|