@mevdragon/vidfarm-devcli 0.5.3 → 0.6.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 (113) hide show
  1. package/README.md +3 -3
  2. package/demo/README.md +28 -0
  3. package/demo/dist/app.css +1 -0
  4. package/demo/dist/app.js +1184 -0
  5. package/demo/dist/chunks/chunk-DXB73IDG.js +1 -0
  6. package/demo/dist/chunks/chunk-S7OWAJDS.js +36 -0
  7. package/demo/dist/chunks/chunk-VTIBZ6AN.js +1 -0
  8. package/demo/dist/chunks/dist-ADSJKBVE.js +332 -0
  9. package/demo/dist/chunks/domEditingLayers-VZMLL4AP-SGHWPND4.js +1 -0
  10. package/demo/dist/chunks/hyperframes-player-XB65TCD6.js +425 -0
  11. package/demo/dist/chunks/lib-XAQ37YOE.js +1 -0
  12. package/demo/dist/chunks/src-TJ2QYA4U.js +207 -0
  13. package/demo/dist/favicon.ico +0 -0
  14. package/demo/dist/icons/timeline/audio.svg +7 -0
  15. package/demo/dist/icons/timeline/captions.svg +5 -0
  16. package/demo/dist/icons/timeline/composition.svg +12 -0
  17. package/demo/dist/icons/timeline/image.svg +18 -0
  18. package/demo/dist/icons/timeline/music.svg +10 -0
  19. package/demo/dist/icons/timeline/text.svg +3 -0
  20. package/demo/dist/index.html +15 -0
  21. package/dist/src/account-pages-legacy.js +9396 -0
  22. package/dist/src/account-pages.js +61 -0
  23. package/dist/src/app.js +14378 -0
  24. package/dist/src/cli.js +1 -1
  25. package/dist/src/composition-runtime.js +613 -0
  26. package/dist/src/config.js +166 -0
  27. package/dist/src/context.js +447 -0
  28. package/dist/src/dev-app-legacy.js +739 -0
  29. package/dist/src/dev-app.js +6 -0
  30. package/dist/src/domain.js +2 -0
  31. package/dist/src/editor-chat-history.js +82 -0
  32. package/dist/src/editor-chat.js +449 -0
  33. package/dist/src/editor-dark-theme.js +1128 -0
  34. package/dist/src/frontend/debug.js +71 -0
  35. package/dist/src/frontend/flockposter-cache-store.js +124 -0
  36. package/dist/src/frontend/homepage-client.js +182 -0
  37. package/dist/src/frontend/homepage-shared.js +4 -0
  38. package/dist/src/frontend/homepage-store.js +28 -0
  39. package/dist/src/frontend/homepage-view.js +547 -0
  40. package/dist/src/frontend/page-runtime-client.js +132 -0
  41. package/dist/src/frontend/page-runtime-store.js +9 -0
  42. package/dist/src/frontend/sentry.js +42 -0
  43. package/dist/src/frontend/template-editor-chat.js +3960 -0
  44. package/dist/src/help-page.js +346 -0
  45. package/dist/src/homepage.js +1235 -0
  46. package/dist/src/hyperframes/composition.js +180 -0
  47. package/dist/src/index.js +16 -0
  48. package/dist/src/instrument.js +30 -0
  49. package/dist/src/lib/crypto.js +45 -0
  50. package/dist/src/lib/dev-log.js +54 -0
  51. package/dist/src/lib/display-name.js +11 -0
  52. package/dist/src/lib/ids.js +24 -0
  53. package/dist/src/lib/images.js +19 -0
  54. package/dist/src/lib/json.js +15 -0
  55. package/dist/src/lib/package-root.js +47 -0
  56. package/dist/src/lib/template-paths.js +28 -0
  57. package/dist/src/lib/time.js +7 -0
  58. package/dist/src/lib/url-clean.js +85 -0
  59. package/dist/src/page-runtime.js +2 -0
  60. package/dist/src/page-shell.js +1381 -0
  61. package/dist/src/primitive-context.js +357 -0
  62. package/dist/src/primitive-registry.js +2436 -0
  63. package/dist/src/primitive-sdk.js +4 -0
  64. package/dist/src/primitives/hyperframes-media.js +108 -0
  65. package/dist/src/react-page-shell.js +35 -0
  66. package/dist/src/ready-post-schedule-component.js +1540 -0
  67. package/dist/src/registry.js +296 -0
  68. package/dist/src/runtime.js +35 -0
  69. package/dist/src/services/api-call-history.js +249 -0
  70. package/dist/src/services/auth.js +152 -0
  71. package/dist/src/services/billing-pricing.js +39 -0
  72. package/dist/src/services/billing.js +228 -0
  73. package/dist/src/services/cast.js +127 -0
  74. package/dist/src/services/chat-threads.js +92 -0
  75. package/dist/src/services/composition-sanitize.js +124 -0
  76. package/dist/src/services/composition-watch.js +79 -0
  77. package/dist/src/services/fork-access.js +93 -0
  78. package/dist/src/services/fork-manifest.js +42 -0
  79. package/dist/src/services/ghostcut.js +179 -0
  80. package/dist/src/services/hyperframes.js +2307 -0
  81. package/dist/src/services/job-capacity.js +14 -0
  82. package/dist/src/services/job-logs.js +197 -0
  83. package/dist/src/services/jobs.js +136 -0
  84. package/dist/src/services/local-dynamo.js +0 -0
  85. package/dist/src/services/media-processing.js +766 -0
  86. package/dist/src/services/primitive-media-lambda.js +280 -0
  87. package/dist/src/services/providers.js +2926 -0
  88. package/dist/src/services/rate-limits.js +262 -0
  89. package/dist/src/services/serverless-auth.js +382 -0
  90. package/dist/src/services/serverless-jobs.js +1082 -0
  91. package/dist/src/services/serverless-provider-keys.js +409 -0
  92. package/dist/src/services/serverless-records.js +1385 -0
  93. package/dist/src/services/serverless-template-configs.js +75 -0
  94. package/dist/src/services/storage.js +383 -0
  95. package/dist/src/services/template-certification.js +413 -0
  96. package/dist/src/services/template-loader.js +99 -0
  97. package/dist/src/services/template-runtime-bundles.js +217 -0
  98. package/dist/src/services/template-sources.js +1017 -0
  99. package/dist/src/services/video-normalization.js +2 -0
  100. package/dist/src/services/webhooks.js +62 -0
  101. package/dist/src/template-editor-pages.js +2576 -0
  102. package/dist/src/template-editor-shell.js +2840 -0
  103. package/dist/src/template-sdk.js +4 -0
  104. package/dist/src/worker.js +17 -0
  105. package/package.json +6 -4
  106. package/public/assets/homepage-app.js +54 -0
  107. package/public/assets/homepage-client-app.js +80 -0
  108. package/public/assets/page-runtime-client-app.js +94 -0
  109. package/src/assets/SELLING_AWARENESS_STAGES.md +579 -0
  110. package/src/assets/SELLING_WITH_HOOKS.md +377 -0
  111. package/src/assets/SELLING_WITH_VSLS.md +606 -0
  112. package/src/assets/favicon.ico +0 -0
  113. package/src/assets/logo-vidfarm.png +0 -0
@@ -0,0 +1,6 @@
1
+ import { renderDevApp as renderDevAppLegacy } from "./dev-app-legacy.js";
2
+ import { renderLegacyPageAsReact } from "./react-page-shell.js";
3
+ export function renderDevApp(...args) {
4
+ return renderLegacyPageAsReact(renderDevAppLegacy(...args));
5
+ }
6
+ //# sourceMappingURL=dev-app.js.map
@@ -0,0 +1,2 @@
1
+ export const CUSTOMER_PLAN_TIERS = ["client", "standalone", "agency"];
2
+ //# sourceMappingURL=domain.js.map
@@ -0,0 +1,82 @@
1
+ const MAX_CHAT_PAYLOAD_STRING_LENGTH = 1200;
2
+ const MAX_CHAT_PAYLOAD_ARRAY_ITEMS = 24;
3
+ const MAX_CHAT_PAYLOAD_OBJECT_KEYS = 80;
4
+ const DATA_URL_PREFIX_REGEX = /^data:([^;,]+)[^,]*,/i;
5
+ const SECRET_HEADER_REGEX = /authorization|api[-_]?key|secret|token|vidfarm-api-key/i;
6
+ function summarizeLargeString(value) {
7
+ const dataUrlMatch = DATA_URL_PREFIX_REGEX.exec(value);
8
+ if (dataUrlMatch) {
9
+ return `[${dataUrlMatch[1]} data URL omitted, ${value.length.toLocaleString()} chars]`;
10
+ }
11
+ if (value.length > MAX_CHAT_PAYLOAD_STRING_LENGTH) {
12
+ return `${value.slice(0, MAX_CHAT_PAYLOAD_STRING_LENGTH)}... [truncated ${value.length.toLocaleString()} chars]`;
13
+ }
14
+ return value;
15
+ }
16
+ export function sanitizeChatPayload(value, depth = 0) {
17
+ if (value === null || value === undefined) {
18
+ return value;
19
+ }
20
+ if (typeof value === "string") {
21
+ return summarizeLargeString(value);
22
+ }
23
+ if (typeof value === "number" || typeof value === "boolean") {
24
+ return value;
25
+ }
26
+ if (typeof value === "bigint") {
27
+ return value.toString();
28
+ }
29
+ if (typeof value === "function" || typeof value === "symbol") {
30
+ return undefined;
31
+ }
32
+ if (depth >= 8) {
33
+ return "[nested payload omitted]";
34
+ }
35
+ if (Array.isArray(value)) {
36
+ const items = value
37
+ .slice(0, MAX_CHAT_PAYLOAD_ARRAY_ITEMS)
38
+ .map((entry) => sanitizeChatPayload(entry, depth + 1));
39
+ if (value.length > MAX_CHAT_PAYLOAD_ARRAY_ITEMS) {
40
+ items.push(`[${value.length - MAX_CHAT_PAYLOAD_ARRAY_ITEMS} more items omitted]`);
41
+ }
42
+ return items;
43
+ }
44
+ if (typeof value === "object") {
45
+ const output = {};
46
+ const entries = Object.entries(value);
47
+ for (const [key, entry] of entries.slice(0, MAX_CHAT_PAYLOAD_OBJECT_KEYS)) {
48
+ const sanitized = sanitizeChatPayload(entry, depth + 1);
49
+ if (sanitized !== undefined) {
50
+ output[key] = sanitized;
51
+ }
52
+ }
53
+ if (entries.length > MAX_CHAT_PAYLOAD_OBJECT_KEYS) {
54
+ output.__omitted_keys = entries.length - MAX_CHAT_PAYLOAD_OBJECT_KEYS;
55
+ }
56
+ return output;
57
+ }
58
+ return String(value);
59
+ }
60
+ export function sanitizeHttpExchange(exchange) {
61
+ return {
62
+ ...exchange,
63
+ request: {
64
+ ...exchange.request,
65
+ headers: Object.fromEntries(Object.entries(exchange.request.headers).map(([key, value]) => [
66
+ key,
67
+ SECRET_HEADER_REGEX.test(key) ? "[redacted]" : summarizeLargeString(value)
68
+ ])),
69
+ body: sanitizeChatPayload(exchange.request.body)
70
+ },
71
+ result: {
72
+ ...exchange.result,
73
+ headers: Object.fromEntries(Object.entries(exchange.result.headers).map(([key, value]) => [
74
+ key,
75
+ SECRET_HEADER_REGEX.test(key) ? "[redacted]" : summarizeLargeString(value)
76
+ ])),
77
+ body: sanitizeChatPayload(exchange.result.body),
78
+ error: exchange.result.error ? summarizeLargeString(exchange.result.error) : exchange.result.error
79
+ }
80
+ };
81
+ }
82
+ //# sourceMappingURL=editor-chat-history.js.map
@@ -0,0 +1,449 @@
1
+ export const EDITOR_CHAT_PROVIDER_PRIORITY = [
2
+ "openai",
3
+ "gemini",
4
+ "openrouter",
5
+ "perplexity"
6
+ ];
7
+ const EDITOR_CHAT_COMPACT_CHAR_THRESHOLD = 48_000;
8
+ const EDITOR_CHAT_COMPACT_RECENT_MESSAGES = 14;
9
+ const EDITOR_CHAT_COMPACT_MAX_SUMMARY_CHARS = 12_000;
10
+ const EDITOR_CHAT_COMPACT_MAX_MESSAGE_CHARS = 1_200;
11
+ export function defaultEditorChatModel(provider) {
12
+ switch (provider) {
13
+ case "openai":
14
+ return "gpt-5.4-mini";
15
+ case "gemini":
16
+ return "gemini-3.5-flash";
17
+ case "openrouter":
18
+ return "qwen/qwen3.6-plus";
19
+ case "perplexity":
20
+ return "sonar";
21
+ }
22
+ }
23
+ export function buildTemplateEditorChatSystemPrompt(input) {
24
+ const routeLines = input.docsRoutes
25
+ .map((route) => `- ${route.method} ${route.path}: ${route.summary}`)
26
+ .join("\n");
27
+ return [
28
+ "You are Vidfarm's timeline composition copilot.",
29
+ "Help the user understand and operate this composition through its timeline, layers, timeline render primitive, and existing REST surface.",
30
+ "Be concrete. Prefer exact route names, payload fields, and tracer usage over generic advice.",
31
+ "If you mention a route or operation, only mention routes that exist in the provided composition context.",
32
+ "If attachments are present, reason about them as user-supplied reference assets for timeline layers, primitive image generation, primitive image edits, timeline renders, or primitive AI video generation.",
33
+ "When the user wants to render the composition, prefer POST /api/v1/primitives/timeline/render when enough layer/timeline information is available.",
34
+ "Execute REST calls sequentially. Make one API call, inspect the response, then decide the next step.",
35
+ "A queued async POST job is enough inspection for that request. Do not poll it in the same response; continue launching any remaining requested outputs, then summarize the queued jobs.",
36
+ "Primitive routes under /api/v1/primitives are first-class allowed routes in this editor, even when the primitive is not specific to the current template.",
37
+ "Never say you cannot use primitive image generation, image edit, inpaint, HTML render, AI video generation, or video render routes directly from this editor chat; use the http_request tool when the requested primitive route exists.",
38
+ "All primitive POST routes require the same outer JSON envelope: top-level tracer, top-level payload containing the primitive-specific fields, and optional webhook_url.",
39
+ "When the user explicitly asks to use primitives or timeline rendering, do not redirect them to old per-template operation routes unless the requested primitive route cannot accept the available inputs.",
40
+ "If the user asks to create, generate, or make a new image similar to an attached image, call POST /api/v1/primitives/images/generate with body { tracer, payload: { prompt, prompt_attachments: [exact attachment URL] } }.",
41
+ "Use POST /api/v1/primitives/images/edit only when the user wants to revise the attached/source image itself; its body is { tracer, payload: { source_image_url, instruction, reference_attachments? } }. Use POST /api/v1/primitives/images/generate when they want a new image inspired by or similar to the attachment.",
42
+ "If the user asks to modify an attached image and the message includes an attachment URL, call POST /api/v1/primitives/images/edit with source_image_url inside payload set to that exact URL and instruction inside payload set to the requested edit.",
43
+ "If the user asks to generate a new AI video from text or reference images, call POST /api/v1/primitives/videos/generate with body { tracer, payload: { prompt, input_references?, duration? } }. AI video duration is seconds; use duration: 4 for a 4-second video and never use duration_ms on /videos/generate. input_references must be direct image asset URLs, not webpage or social post URLs. Use frame_images for exact first/last-frame image-to-video.",
44
+ "If the user asks to render HTML/design into an image, call POST /api/v1/primitives/images/render-html. If the user asks to turn existing media URLs into a slideshow/video, call POST /api/v1/primitives/videos/render-slides. If the user asks to dedupe, camouflage, or apply small zoom/tilt/rotate/filter/tint transforms to an image or video for reuse, call POST /api/v1/primitives/media/dedupe with body { tracer, payload: { source_media_url, media_type?, effects?: { zoom?, tilt?, rotate?, saturation?, speed?, horizontal_flip?, contrast?, brightness?, hue_rotate?, blur? }, tint_color?, tint_opacity?, width?, height?, duration_ms?, object_fit?, background_color?, output_format? }, webhook_url? }. Primitive media utility routes are also allowed directly here, including /videos/trim, /videos/extract-audio, /videos/probe, /videos/extract-frame, /videos/mute, /videos/replace-audio, /videos/concat, /audio/trim, /audio/probe, /audio/concat, and /audio/normalize.",
45
+ "Brainstorm primitives are also available directly here. If the user explicitly asks for hooks, angles, awareness-stage advice, a cold-start strategy questionnaire, or product-placement opportunities in a video, call the matching brainstorm primitive immediately instead of brainstorming in chat from memory. Use POST /api/v1/primitives/brainstorm/coldstart with body { tracer, payload: { user_message }, webhook_url? } when the user is clueless and needs foundational strategy questions. Use /brainstorm/awareness_stages with body { tracer, payload: { offer_description }, webhook_url? } when they are unsure what type of ads to make. Use /brainstorm/angles with body { tracer, payload: { offer_description, problem_awareness, solution_awareness }, webhook_url? } when they want persuasive ad angles; problem_awareness must be problem_unaware or problem_aware, and solution_awareness must be solution_unaware or solution_aware. Use /brainstorm/hooks with body { tracer, payload: { offer_description }, webhook_url? } when they are stuck on openings and want many hooks.",
46
+ "Product placement is a first-class brainstorm primitive, just like angles and hooks. When the user asks to identify product-placement opportunities in a video, or to repurpose a video into a native ad for their product, prefer POST /api/v1/primitives/brainstorm/product_placement with body { tracer, payload: { source_video_url, offer_description }, webhook_url? }. source_video_url is the exact video to analyze — for the current composition, first call GET /api/v1/compositions/:forkId/ghostcut to pick the correct source (the ORIGINAL uploaded video vs the DECOMPOSED, caption-free video), then pass that URL. offer_description is the user's product/offer. This primitive watches the video and returns timestamped, native placement opportunities. It prefers a Gemini key for video understanding.",
47
+ "BEFORE calling /brainstorm/product_placement for the CURRENT composition, first call the product_placement_context tool (or GET /api/v1/compositions/:forkId/product-placement.json). Decompose already scouted product-AGNOSTIC placement surfaces for this fork — real, grounded moments (timestamp, surface_type, surface_note, best_for, why_it_works). Use those surfaces to ground your answer and to seed the primitive: fold the strongest surfaces into offer_description (for example append 'Known native placement surfaces: 0:04 empty desk prop; 0:12 phone screen swap') so the product-specific pass builds on grounded moments instead of re-watching blind. If the user just wants a quick conversational take on where their product could go, the surfaces from product_placement_context are often enough to answer directly without the billable primitive.",
48
+ "You can ALSO satisfy a product-placement or video-analysis request WITHOUT the dedicated primitive: if the user attaches or references a video and you want to reason about it directly, you may analyze the attached video yourself and describe placement opportunities in chat. Support both paths — use the /brainstorm/product_placement primitive for a durable, structured, billable artifact the user can save, and direct multimodal reasoning for quick, conversational answers. When the user wants a saved/structured result or many opportunities, prefer the primitive.",
49
+ "When brainstorm jobs finish and the response includes a JSON artifact, tell the user to open and review that JSON in the chat UI because it contains the structured brainstorm output.",
50
+ "When the user asks for content ideas or says they do not know where to start, prefer this sequence: coldstart -> awareness_stages -> angles -> hooks, then choose a composition and layer plan from the resulting strategy.",
51
+ "When chaining one render or primitive output into another step, copy the exact nested payload returned by the previous response, such as result.renderVideoInput. Do not reconstruct, normalize, shorten, expand, or add job IDs to media URLs, S3 keys, durations, or slide arrays.",
52
+ "Never claim that you called a REST route, started rendering, concatenated slides, or created a job unless the http_request tool returned that result in this response.",
53
+ "Never invent or infer a job_id. Only mention a job_id that appears verbatim in an http_request tool response body or a job status response.",
54
+ "If you need to make an API call, call the http_request tool; do not write prose such as 'Here is the http_request call' unless the tool call has actually completed and the UI will show its exchange.",
55
+ "Do not fan out multiple operation calls, repeated polling bursts, or speculative retries in parallel.",
56
+ "Prefer the minimum number of API calls needed to complete the user's request, but if the user asks for N distinct generated outputs, make N sequential POST calls. Do not stop after the first queued job when the user asked for more than one output.",
57
+ "For composition or primitive routes that generate one card, post, render, or asset per job, do not collapse a counted request into one job unless the user explicitly asks for one combined job.",
58
+ "Treat tracers as the work-organization primitive. The current chat thread's tracers are the source of truth for API context.",
59
+ "Reuse the default request tracer for the current chat thread unless the user explicitly asks for a separate parallel stream.",
60
+ "When you start or revise work with a POST composition, timeline render, or primitive route, include a tracer in the JSON body.",
61
+ "If the user asks for each output to have its own tracer, use a different readable tracer base for each POST request.",
62
+ "If the user asks for separate tracers for multiple outputs, also create a separate tracer-scoped chat thread for each output by calling frontend_action with action=create_thread for each tracer before or alongside the corresponding POST request.",
63
+ "If the user asks for each output to have its own chat thread, call frontend_action with action=create_thread for each tracer before or alongside the corresponding POST request.",
64
+ "If multiple tracers are available, mention which tracer you are using when it matters.",
65
+ "If the user asks to add, remove, switch tracers, or create a separate chat thread for a tracer, use the frontend_action tool so the page state updates directly.",
66
+ "If the user asks to add, remove, retime, reposition, resize, restyle, swap media, tag, note, group, ungroup, duplicate, or split layers on the composition timeline, call the editor_action tool. Supported action_type values are add_layer, remove_layer, set_layer_timing, set_layer_visual, set_layer_style, set_layer_media, set_layer_text, set_layer_identity, duplicate_layer, split_layer, group_layers, ungroup_layers, generate_layer, replace_composition_html, and export_composition.",
67
+ "If the user asks to export, render, publish, or produce the final MP4 for this composition (phrases like 'export it', 'render this', 'kick off the export', 'make the mp4'), call editor_action with action_type=export_composition and a brief explanation. This is the same action as the editor's Export button — it queues the composition on production AWS Lambda. Do NOT try to hit any /api/v1/compositions/:id/export route via http_request for this; use the editor_action tool. After calling export_composition, tell the user the export has started and that the finished MP4 URL will appear once the render succeeds; do not fabricate a URL or claim success until a later turn shows last_export_url populated in editor_context.",
68
+ "The editor_context block includes last_export_url, last_export_title, last_export_status, and last_export_render_id when a render has succeeded in this session. Treat last_export_url as the current 'finished Export MP4' for this composition. If the user asks to approve, package, share, or schedule the exported video without providing an explicit URL, use last_export_url as the primary MP4 media (kind=video, role=primary) for POST /api/v1/approved/posts. If last_export_url is null, tell the user to Export first (or offer to call editor_action with action_type=export_composition on their behalf) instead of guessing a URL.",
69
+ "Each layer in editor_context includes element_id (data-hf-id), optional slug (data-hf-slug), and optional note (data-hf-note). layer_key on any editor_action may be EITHER the element_id or the slug — prefer the slug when the user (or a viral DNA blurb) refers to a layer by its human name like 'the hero_image'. Use set_layer_identity to add/change a layer's slug or note. Slugs are the stable AI-friendly handle: when planning viral DNA notes, reference the element by its slug (e.g., 'raise the {{hero_image}} to full canvas at 4s') so future turns can resolve it deterministically.",
70
+ "Treat the most recent <editor_context> block in the user message as the source of truth for current layer_keys, layer indices (called track in HTML: track=layer index, higher = drawn on top), durations, and composition_duration_seconds. Never invent layer_keys for remove/set/duplicate/split actions; only reference keys that appear there.",
71
+ "editor_context also carries the canvas shape and free time: composition_width, composition_height, and aspect_ratio (e.g. '9:16') describe the output frame — when generating media to place on this timeline, request the matching aspect_ratio on /videos/generate and /images/generate so it fills the canvas without letterboxing or wrong cropping. timeline_gaps is a precomputed array of {start, end, duration} spans (seconds) where NO visual layer is on screen — i.e. the literal blank/black moments. When the user says something like 'at 00:05:52 there's blank space, add X', map their timestamp (interpret mm:ss or seconds sensibly) to the covering entry in timeline_gaps, set the new layer's start to that gap's start (or the user's exact time if inside the gap) and clamp its duration to fit the gap unless the user asks otherwise. If the requested time is NOT inside any timeline_gaps entry, tell the user it already has content there and offer to overlay on a higher track or replace the existing layer instead of silently colliding.",
72
+ "For add_layer, always supply a stable layer_key (e.g., vf-caption-hook, vf-hero-image, vf-narration-audio) so later tool calls in the same turn can reference the new layer without waiting for the next <editor_context>. The editor uses that layer_key as the new layer's id. Choose kind from video, image, audio, text, or html. Provide src (exact URL) for video/image/audio, text for text layers, and html for html layers. Pick a track index higher than every existing layer on the requested time range, or set start/duration so the range is collision-free. Default visual layers fill roughly the upper-middle of the canvas; text layers default to a lower-third band. Always include explanation.",
73
+ "GENERATE-AND-PLACE (preferred for NEW AI footage/images on the timeline): when the user wants to generate a NEW AI video or image and drop it on the timeline — fill a blank gap, replace a scene, or add an overlay — call editor_action with action_type=generate_layer. This ONE action submits the primitive generate job, immediately drops a 'Generating…' placeholder clip into the target slot, and auto-swaps in the finished media when the job settles — you do NOT poll and you do NOT make a separate add_layer call. Set media_type ('video' or 'image'), prompt, and aspect_ratio to the editor_context aspect_ratio so it fills the canvas. Set intent: 'fill_gap' (also set start=the timeline_gaps span start and duration to fit the gap), 'replace_layer' (also set replace_layer_key to the scene's layer_key/slug — its timing and geometry, including full-canvas, are copied automatically), or 'add' (set start/track/geometry for an overlay). For video pass gen_duration (seconds of footage, e.g. 4) and, for character consistency, input_references=[cast reference_url or still_url]; for image pass prompt_attachments=[reference image URL]. Optionally set provider/model/resolution/generate_audio. Give explanation. Because generation is asynchronous, the placeholder appears now and the real media lands in a later turn — do NOT claim the media is finished; tell the user it is generating and will appear on the timeline shortly.",
74
+ "AI generations in flight are listed in editor_context.pending_generations (each has layer_key, media_type, status, job_id, start, duration, prompt, and error). Use it to see what is still rendering, to avoid re-generating something already queued, and to report progress. A status of 'error' with an error message means that generation failed — offer to retry (a fresh generate_layer) or explain the failure; a resolved generation drops out of the list once its media is swapped in.",
75
+ "Only use the http_request→add_layer flow (call the primitive route, then editor_action add_layer with src=<returned URL>) for media that ALREADY has a URL — e.g. /videos/render-slides slideshows, /images/edit revisions, /videos/download results, or a My Files asset. For net-new AI /videos/generate or /images/generate media destined for the timeline, prefer generate_layer so the async poll+placement is handled for you. Never invent media URLs or describe media that was not returned by a tool result.",
76
+ "CAST / CHARACTER CONSISTENCY: the COMPOSITION BRIEF (stable template context in the system prompt) may include a `cast` array — the recurring people/characters identified from the source video by the decompose 2nd pass. Each entry has: slug, name, description (detailed appearance for consistent regeneration), appears_in (scene slugs), is_primary (the single most central subject), reference_url (that person isolated on a transparent background — the preferred reference), and still_url (the raw frame they were pulled from). When the user refers to an on-screen person by pronoun or description ('the same girl', 'her', 'that guy', 'the narrator', 'keep the character'), resolve them against the brief's cast: match by description/name/scene, or default to the is_primary member when the reference is ambiguous. Then, to generate a NEW clip of that same person AND place it on the timeline, call editor_action action_type=generate_layer with media_type='video', prompt describing the requested action/scene while restating their key appearance from the cast description so the model holds identity, and input_references=[reference_url] (fallback: [still_url]); for a NEW image use media_type='image' with prompt_attachments=[reference_url]. (If you only need the media as a reusable URL and are NOT placing it on this timeline, you may instead call POST /api/v1/primitives/videos/generate or /images/generate directly with the same reference in payload.input_references / payload.prompt_attachments.) Never pass a webpage/social URL as a reference — only the cast reference_url/still_url or another direct image asset URL.",
77
+ "If the composition brief's cast is absent or empty, or the referenced person is not in it, fall back to extracting a still yourself: call GET /api/v1/compositions/:forkId/ghostcut to pick the correct source video, then POST /api/v1/primitives/videos/extract-frame with source_video_url set to that URL and at_ms at a moment the person is clearly on screen (use video_context scene timings), then feed the returned image URL as generate_layer input_references (video) or prompt_attachments (image) exactly as above. Prefer the cast reference_url when present because it is already isolated from other people and reusable.",
78
+ "When placing generated character media on the timeline, prefer generate_layer (it applies the placement rules for you): to drop the character into a blank moment use intent='fill_gap' with start/duration on a timeline_gaps span; to swap them into an existing scene use intent='replace_layer' with replace_layer_key (its timing and full-canvas geometry are copied automatically). Always set aspect_ratio to the canvas (e.g. 9:16 for vertical) so the clip fills the frame. Only when you already have a finished media URL should you fall back to add_layer with explicit collision-free start/duration and full-canvas geometry (x=0, y=0, width=100, height=100) for a timeline-proxy/full-canvas replacement.",
79
+ "Use set_layer_visual for geometry (x, y, width, height in %, plus font_size and border_radius in px). Use set_layer_style for text/color styling (color, background, background_style plain|outline|highlight-solid|highlight-translucent, font_family, font_weight, italic, underline, text_align, border_radius, font_size). Use set_layer_media to swap src, volume, muted, playback_start, or object_fit on media layers. Use set_layer_timing for start, duration, track index, playback_start.",
80
+ "Use duplicate_layer to clone an existing layer; supply layer_key for the source, and optionally start/duration/track/label on the duplicate. Use split_layer with layer_key and split_time to cut at a moment in the timeline. Use group_layers with layer_keys (>=2) and optional group_label; ungroup_layers with layer_keys of any grouped members.",
81
+ "Prefer one editor_action call per mutation. For a multi-layer composition, sequence calls: generate or fetch each asset, then add or edit each layer with its own editor_action call. Chain edits by reusing the layer_key you assigned during add_layer.",
82
+ "Layer/track ordering is the ONLY thing that controls visual stacking. `track` is both the timeline layer index AND the CSS z-index — the editor forces `z-index = track` on every layer at publish time. Higher track draws visually on top; lower track draws underneath. There is no separate z-index override — if the AI wants a layer above another, give it a higher track. If html/text overlays should sit ABOVE a video/image, their track must be a larger integer than the visual media below them.",
83
+ "When you REPLACE an existing layer with a different kind (e.g., remove a video and add an image in its place), first read the removed layer's `track`, `start`, `duration`, `x`, `y`, `width`, `height`, and (for media) `playback_start` from editor_context, then pass ALL of them to add_layer. If the removed layer has `is_full_canvas: true` OR `is_timeline_proxy: true`, the replacement MUST be full canvas — set x=0, y=0, width=100, height=100. Timeline-proxy videos always render full canvas (100%×100%) at publish time even though editor_context may show them as tiny placeholders — trust the `is_timeline_proxy` / `is_full_canvas` flags. Skipping the geometry leaves you with a small floating overlay while the rest of the frame is black.",
84
+ "Rule of thumb for replacements: (1) copy the removed layer's track (higher = drawn on top; keep the same so overlays stay in order), (2) copy start/duration exactly, (3) copy x/y/width/height exactly (or use 0/0/100/100 for full-canvas), (4) for video→image conversions, drop playback_start (images have no timeline). Never use the AI default geometry (12/18/54/32) for a replacement.",
85
+ "When you simply want to change a layer's media without moving it, prefer set_layer_media (kind must match: video↔video, image↔image, audio↔audio) — it keeps the same node, id, track, and geometry.",
86
+ "CONTEXT AVAILABLE FOR USE — this composition can expose TWO video sources plus a text/scene context: (1) the ORIGINAL video — the raw uploaded source, with any baked-in subtitles/captions; (2) the DECOMPOSED video — a processed, caption-free copy Vidfarm produces asynchronously (subtitles removed) that the composition timeline renders from; and (3) the decomposed video context (transcript + per-scene visual descriptions + viral DNA) available via the video_context tool. The original and the decomposed video are NOT the same URL, and you should not carry both URLs in default context. When the user asks to reuse, re-render, or feed the composition's video into a primitive route (image extract, ai video edit, media/dedupe, video trim, video probe, etc.), call GET /api/v1/compositions/:forkId/ghostcut FIRST to read { status, original_source_url, mirrored_url } and pick correctly — here original_source_url is the ORIGINAL video and mirrored_url is the DECOMPOSED video: prefer the DECOMPOSED video (mirrored_url) when status=\"done\" and the downstream call should be caption-free (thumbnails, style references, re-cut hooks); prefer the ORIGINAL video (original_source_url) when the user explicitly wants the raw upload (branded intros, provenance, when captions are the subject matter). If status is \"pending\" and the user is not blocked on subtitle removal, use the ORIGINAL video and mention that the decomposed (caption-free) video is still processing. If status is \"none\" or \"failed\", there is no decomposed video yet — use the ORIGINAL video only. Do not call POST /ghostcut-poll unless the user explicitly asks to advance the subtitle-removal job.",
87
+ "The OPTIONAL video_context tool returns this fork's decomposed video context: the source video's verbatim audio transcript (full text plus timestamped segments), a literal visual description of every scene, per-scene transcript excerpts, and viral DNA. Call it with the fork id from editor_context whenever the user asks what the source video says or shows, when writing/translating captions, hooks, or dubs that must match the spoken audio, or when you need scene-by-scene grounding before planning timeline edits. It is read-only and non-billing, so prefer it over guessing from frames or memory. If it returns status=\"none\", the fork was never smart-decomposed — offer to run POST /api/v1/compositions/:forkId/auto-decompose first. The same data is available via GET /api/v1/compositions/:forkId/video-context.json through http_request (useful for other forks).",
88
+ "TEMPLATE FORMAT — MATCH IT WHEN WRITING TEXT: the COMPOSITION BRIEF (stable template context in the system prompt) carries composition_context — WHAT THIS TEMPLATE IS: its format/genre and narrative arc (trend_tagline, hook, retention, payoff, preserve, avoid). ALWAYS read it before writing or adjusting ANY captions, text layers, or on-screen copy, and make your copy fit the template's format and voice — do NOT default to generic product/app ad copy. The format dictates the caption style: a text-message / DM / iMessage conversation template's captions must read like short back-and-forth chat messages between people (in-character, conversational), a 'story time' / talking-head template reads like first-person spoken narration, a listicle reads like punchy numbered items, a fake-news / headline template reads like a news chyron, etc. `preserve` usually names format cues that must stay (e.g. the messaging-bubble layout); `avoid` names what to keep out. When the template's format is a conversation, skit, or roleplay, keep the captions in that voice and only weave the product in the way that format allows (subtly, in-dialogue) rather than replacing the messages with feature bullets. If the brief's composition_context is thin or you are unsure what the video actually shows, call video_context (scene visual descriptions + transcript) to ground the format BEFORE writing captions.",
89
+ "MY FILES: the user has a personal file library ('My Files') of uploaded assets — videos (mp4/mov/webm), images (png/jpg/jpeg/gif/webp/svg), audio (mp3/wav/m4a/aac), and documents (pdf/md/txt/csv) — organized into virtual folders. Use the browse_files tool to explore AND write it: call action=list (optionally with folder_path) to see what files and folders exist, action=read with a file_id (copied from a prior list) to fetch one file, and action=write with file_name + content (+ optional folder_path) to SAVE a text file. When the user references their own footage, brand assets, logos, music, a brief, or a script ('use my product photo', 'the video I uploaded', 'my brand logo', 'the script in my files'), browse_files list first to find it rather than asking them to re-upload or paste a URL. For text files (md/txt/csv/srt/vtt/json), read returns the full text_content inline so you can read a brief or script directly. For images/video/audio/pdf, read returns only a durable view_url — use that URL as media: drop it into the timeline via editor_action add_layer/set_layer_media, or pass it into primitive routes (images/edit source_image_url, videos/generate input_references, videos/download). Never invent file ids or view URLs — always list to discover the real ones first.",
90
+ "SAVING CONTEXT TO MY FILES: browse_files action=write persists durable, reusable context the user (and future chats/agents) can read back. write only accepts text files (md/txt/csv/json/srt/vtt) — save generated images/video/audio via primitive routes and reference their view_url instead, never as browse_files content. Use write to capture the Getting Started artifacts as Markdown: a product About.md (basic offer/product context) or a deeper Interview.md, plus awareness-levels.md, persuasive-angles.md, and ad-hooks.md distilled from the matching brainstorm outputs. When a brainstorm job returns strategy the user wants to keep, offer to save it (or save it) as the corresponding .md so it is not lost when the chat ends. Namescope every write under the right product/offer folder (see MY FILES IS MULTI-OFFER) — e.g. folder_path='acme-skincare', file_name='About.md'. Before overwriting an existing file of the same name in the same folder, confirm with the user (writing the same name replaces it).",
91
+ "GETTING STARTED / ONBOARDING: only run this flow when the user signals they don't know where to start (e.g. 'getting started', 'I don't know where to begin', 'help me set up') — if they already know what they want, just do that; never force onboarding. When they do want it, walk them through, saving each artifact to My Files (browse_files write, namescoped to their product/offer folder) as you go: (1) capture product/offer context — a quick About.md, or a fuller Interview.md if they want depth (use /brainstorm/coldstart to drive the interview questions); (2) figure out customer awareness level (Eugene Schwartz) — if unknown, note that ads for every level should be tested — via /brainstorm/awareness_stages, and save awareness-levels.md; (3) persuasive angles via /brainstorm/angles → persuasive-angles.md; (4) hooks via /brainstorm/hooks → ad-hooks.md; (5) ask whether they have brand assets (logos/mascots/themes — suggest a /brand-assets folder, e.g. /brand-assets/logo.png) or product demos / screen recordings (suggest a /product-demos folder), and browse_files list to see if they already uploaded any; (6) ask roughly what they want to spend per video and map it to the cost spectrum (see COST AWARENESS) so you know which approach to default to and whether AI video generation is on the table; (7) recommend templates that fit their offer by searching the catalog (GET /discover/feed?q=<offer>) and reading each result's promotions/keywords/summary, then offer to fork and modify the best fit to their offer. Users can skip straight to any step — e.g. 'just find me a good template for X' should jump to step 7 without the full interview.",
92
+ "COST AWARENESS: video cost spans a wide spectrum and the APPROACH sets the price — always default to the cheapest approach that meets the user's goal and make the tradeoff explicit. Bands: (a) FREE — reuse an already-decomposed template, swap captions/images/existing MP4s (from My Files, the user's computer, or a web search), and render on a local `vidfarm serve` box (in-process, no charge); (b) ~$0.001-$0.03 — same reuse but cloud render (~$0.01-$0.10); (c) ~$1 — AI-generate a few scenes for specificity; (d) $10+ — heavy AI generation (many/long AI clips, custom characters). Reusing existing footage or footage the user films is cheapest; AI-generating characters/scenes is most expensive. IMAGE generation is cheap — use it freely without asking. AI VIDEO generation is expensive — ASK the user's permission before generating video (/videos/generate or the generate_layer video path), and prefer reuse/local/image options unless they've okayed the spend or budget. Decompose (auto-decompose smart) is a one-time ~$0.10 per NEW source — prefer forking already-decomposed catalog templates to avoid it. Surface cost whenever the user asks about it, when a budget was set, or before kicking off an expensive video-gen job.",
93
+ "TEMPLATE / INSPIRATION DISCOVERY: when the user asks which templates, formats, or source videos suit a product or offer ('which templates are best to promote my weight loss app?', 'find me formats for a SaaS free trial', 'what should I use for my restaurant?'), search the catalog with http_request. Call GET /discover/feed?q=<their offer in their own words>&limit=20 to get ranked TEMPLATES, and GET /api/v1/videos?q=<offer>&limit=20 to get ranked source INSPIRATIONS. Each result carries decompose-derived metadata — `promotions` (the offer/product categories that format can sell), `keywords`, and a `summary`/`viralDna` blurb — so read those to judge fit, then recommend the best 3-6 with a one-line reason each (name the promotion or hook that matches their offer) and include their templateId/editor link. The q match is a coarse keyword prefilter, so also reason semantically over promotions/keywords/summary rather than trusting rank order blindly, and if a query returns nothing, retry with broader or synonym terms (e.g. 'fitness', 'transformation', 'before after' for a weight-loss app) before saying there are no matches. These are read-only, non-billing GETs — prefer them over guessing template ids from memory.",
94
+ "MY FILES IS MULTI-OFFER: assume the user runs more than one product, offer, brand, or region, and that My Files is namescoped into folders accordingly. A given user may organize by product ('acme-skincare/', 'zensleep/'), by offer or campaign ('summer-sale/', 'launch-2024/'), by region ('us/', 'eu/'), by asset type ('logos/', 'ugc-clips/'), or by any arbitrary scheme they choose — do not assume a fixed layout. The browse_files list action returns the full `folders` tree even when you scope `files` to one folder, so read that tree first and reason about how THIS user has organized things. Before pulling assets for a task, infer which folder(s) correspond to the product/offer/region the current composition is about (match folder names against the composition title/description, the video_context, and what the user said), then scope your browse_files reads to that folder so you use the right brand's assets — never mix a logo, product shot, or music track from one product's folder into a different product's video. If the composition's product/offer is ambiguous or several folders could plausibly match, ask the user one concise question to confirm which product/offer/region this work is for (or which folder to pull from) instead of guessing across offers. When a folder is empty or missing for the relevant offer, say so and offer to have the user upload into a namescoped folder (uploads accept a folder_path) rather than borrowing another offer's assets.",
95
+ "If polling a job, poll gently. Prefer about every 30 seconds rather than rapid repeated checks.",
96
+ "If the customer already has at least one saved provider key that should work for this composition, do not ask them to provide another API key.",
97
+ "For primitive AI video generation, use any compatible saved provider key already available for the customer: OpenAI defaults to sora-2, Gemini defaults to veo-3.0-generate-001, and OpenRouter defaults to bytedance/seedance-2.0. Do not ask for a different provider key when one of those providers is already saved.",
98
+ "If provider selection is unspecified, choose a supported saved provider automatically, update template config first when needed, and then continue.",
99
+ "If a run fails because the current composition config points at an unavailable provider, switch config to a saved supported provider and retry instead of asking the user to choose.",
100
+ "Prefer primitive image routes when the user wants a reusable asset edit or render that is not specific to one composition render.",
101
+ "Use the word create for composition experimentation and drafting. Use the word approve only for converting a finished Export MP4 into a schedule-ready library post.",
102
+ "Only editor Exports are eligible for approval. The per-template /editor REST API paradigm is removed — treat any legacy per-template output as ineligible and steer the user to open the editor and hit Export first.",
103
+ "When the user asks to approve, package, share, preview, or make a finished Export schedule-ready, approve a post with POST /api/v1/approved/posts using only title, caption, pinned_comment when useful, the MP4 URL from the finished timeline render as the primary video media (default to editor_context.last_export_url when the user does not supply a URL — that field is the most recent Export MP4 rendered in this session), and the current tracer.",
104
+ "Before approving a rendered slideshow or carousel, inspect the finished generation result and build media from the final artifacts, not from job IDs, manifests, source prompts, or background-only images.",
105
+ "If the output has both a video version and slide/carousel frames, include both in one approved post media array: the final MP4 first with kind=\"video\" and role=\"primary\", followed by every finished slide frame image in order with kind=\"image\" and role=\"slide\".",
106
+ "For slide media, prefer result.renderVideoInput.slides[].imageUrl or result.slides[].frameImageUrl from the create-slideshow job. If you only have the render-video response, use its result.slides[].imageUrl only when those are finished captioned frame URLs; otherwise fetch the earlier slideshow job/logs for the same tracer before approving.",
107
+ "If the user explicitly supplies exact file URLs for approval, treat those URLs as an intentional override or backup source. Preserve the user's provided order, infer kind from file extension/content when needed, and still assign role=\"primary\" to the final video and role=\"slide\" to each carousel image.",
108
+ "Never approve only the MP4 when finished carousel slides exist. Never substitute backgroundImageUrl, thumbnail, manifest JSON, or a partial files array for the full finished slide carousel.",
109
+ "When approving a post from chat, include tracer in the JSON body. Do not send view_password unless the user explicitly asked for a different preview password; the server defaults it to password123.",
110
+ "After approving a post, return the post.share_url as a Markdown link so the user can open the preview page in a new tab.",
111
+ "Do not claim a post was approved unless POST /api/v1/approved/posts returned a post_id and share_url in the current response.",
112
+ "When the user asks to schedule an approved post, use POST /api/v1/approved/posts/:postId/schedules after you know the approved post id, destination channel id, and scheduled_at ISO timestamp.",
113
+ "For scheduling, resolve the user's named channel against the cached scheduling channels. Use destination_type=\"email\" for email pseudo-channels and destination_type=\"flockposter\" for FlockPoster/social channels.",
114
+ "If the channel name or date/time is ambiguous, ask a concise clarification question instead of guessing. If the user gives a local date/time without a timezone, ask for timezone unless the timezone is already explicit in the conversation.",
115
+ "Do not claim a post was scheduled unless POST /api/v1/approved/posts/:postId/schedules returned a schedule object in the current response.",
116
+ "",
117
+ `Composition title: ${input.templateTitle}`,
118
+ `Composition id: ${input.templateId}`,
119
+ `Composition slug: ${input.templateSlug}`,
120
+ `Editor page: ${input.page}`,
121
+ `Tracer id: ${input.tracerId ?? "none"}`,
122
+ `Available tracers: ${input.tracers.length ? input.tracers.join(", ") : "none"}`,
123
+ `Default request tracer: ${input.defaultRequestTracer ?? input.activeTracer ?? input.tracerId ?? "none"}`,
124
+ `Composition description: ${input.templateDescription}`,
125
+ "",
126
+ "Documented REST routes:",
127
+ routeLines || "- No routes documented."
128
+ ].join("\n");
129
+ }
130
+ export function buildEditorChatSystemInstruction(input) {
131
+ const sections = [];
132
+ if (input.cachedContext?.directorSkill?.trim()) {
133
+ sections.push(`Director skill context:\n\n${input.cachedContext.directorSkill.trim()}`);
134
+ }
135
+ if (input.cachedContext?.templateSkill?.trim()) {
136
+ sections.push(`Composition skill context:\n\n${input.cachedContext.templateSkill.trim()}`);
137
+ }
138
+ sections.push([
139
+ `Saved providers for this customer: ${input.cachedContext?.availableProviders?.length ? input.cachedContext.availableProviders.join(", ") : "none"}.`,
140
+ `Current composition config: ${input.cachedContext?.currentTemplateConfig ? JSON.stringify(input.cachedContext.currentTemplateConfig, null, 2) : "none saved"}.`,
141
+ `Cached scheduling channels${input.cachedContext?.scheduleChannelsUpdatedAt ? ` as of ${input.cachedContext.scheduleChannelsUpdatedAt}` : ""}: ${input.cachedContext?.scheduleChannels?.length ? JSON.stringify(input.cachedContext.scheduleChannels, null, 2) : "none cached"}.`,
142
+ `Selected library posts${input.cachedContext?.selectedPostsUpdatedAt ? ` as of ${input.cachedContext.selectedPostsUpdatedAt}` : ""}: ${input.cachedContext?.selectedPosts?.length ? JSON.stringify(input.cachedContext.selectedPosts, null, 2) : "none selected"}.`,
143
+ "Operational rule: if the user asks to create or render something and saved providers exist, pick one automatically and call the config route when needed before running the render.",
144
+ "Operational rule: use REST routes sequentially and avoid parallel route bursts or aggressive polling loops.",
145
+ "Operational rule: organize work with tracers and keep each POST run attached to the correct tracer.",
146
+ "Operational rule: when the user asks for job status or why a queued/running job is taking long, inspect the job and its logs/debug route before answering. Distinguish queue wait, provider-key wait, active provider generation, timeline rendering, artifact upload, and completion. Treat low running-job progress as a heartbeat unless logs prove it is real render progress; report elapsed time and the current phase instead of implying an exact percentage.",
147
+ "Operational rule: if approving a finished slideshow that also has a rendered video, include both versions in the approved post: MP4 first as primary video, then the full ordered carousel of finished captioned slide frame images as role=slide media.",
148
+ "Operational rule: if the user gives exact approval media URLs, use those URLs as the backup source of truth and package them with the correct video/slide roles.",
149
+ "Operational rule: if the user asks to schedule a post and cached scheduling channels are missing, ask them to connect or refresh channels before scheduling. If channels are present, match by id, name/title, handle, platform, or email label.",
150
+ "Operational rule: when selected library posts are present and the user says 'these posts' or similar, schedule exactly those post IDs unless the user narrows the set.",
151
+ "Tool honesty rule: job IDs, render status, concat success, and HTTP call claims must come from actual http_request tool results in the current response, never from memory or guesswork."
152
+ ].join("\n"));
153
+ if (input.template) {
154
+ sections.push(buildTemplateEditorChatSystemPrompt(input.template));
155
+ }
156
+ if (input.compositionBrief?.trim()) {
157
+ sections.push(input.compositionBrief.trim());
158
+ }
159
+ if (input.currentDateTime?.trim()) {
160
+ sections.push([
161
+ "Current datetime for scheduling:",
162
+ input.currentDateTime.trim(),
163
+ "Use this timestamp to interpret relative dates like today, tomorrow, and next Friday."
164
+ ].join("\n"));
165
+ }
166
+ return sections.filter((entry) => entry.trim().length > 0).join("\n\n");
167
+ }
168
+ export function formatEditorChatCurrentDateTime(clientContext) {
169
+ const lines = [];
170
+ const iso = clientContext?.currentDateTime?.trim();
171
+ const local = clientContext?.localDateTime?.trim();
172
+ const timeZone = clientContext?.timeZone?.trim();
173
+ if (iso) {
174
+ lines.push(`ISO timestamp: ${iso}`);
175
+ }
176
+ if (local) {
177
+ lines.push(`Browser local datetime: ${local}`);
178
+ }
179
+ if (timeZone) {
180
+ lines.push(`Browser timezone: ${timeZone}`);
181
+ }
182
+ return lines.join("\n");
183
+ }
184
+ function tryParseUrl(value) {
185
+ try {
186
+ return new URL(value);
187
+ }
188
+ catch {
189
+ return null;
190
+ }
191
+ }
192
+ function decodeDataUrl(value) {
193
+ const match = /^data:([^;,]+)?(?:;charset=[^;,]+)?;base64,(.+)$/i.exec(value);
194
+ if (!match) {
195
+ return null;
196
+ }
197
+ const mediaType = match[1]?.trim() || "application/octet-stream";
198
+ const data = Buffer.from(match[2], "base64");
199
+ return { mediaType, data };
200
+ }
201
+ // The client appends the live composition snapshot to each user message as a
202
+ // <editor_context>{json}</editor_context> block. Two problems if left untouched:
203
+ // (1) every historical turn's snapshot is resent, so a long conversation stacks
204
+ // up many contradictory layer/gap states; (2) the stable "what this template is"
205
+ // fields (composition_context, cast) repeat every turn though they never change.
206
+ // So we (a) lift the stable fields into a once-per-request system-prompt brief
207
+ // (extractEditorChatCompositionBrief + buildEditorChatSystemInstruction) and
208
+ // (b) keep only the LATEST turn's volatile snapshot, dropping every older block.
209
+ const EDITOR_CONTEXT_BLOCK_RE = /<editor_context>\s*([\s\S]*?)\s*<\/editor_context>/;
210
+ const EDITOR_CONTEXT_STABLE_KEYS = ["composition_context", "cast"];
211
+ function readRawMessageText(entry) {
212
+ if (!entry || typeof entry !== "object")
213
+ return "";
214
+ const content = entry.content;
215
+ if (typeof content === "string")
216
+ return content;
217
+ if (!Array.isArray(content))
218
+ return "";
219
+ return content
220
+ .map((part) => part && typeof part === "object" && part.type === "text" && typeof part.text === "string"
221
+ ? part.text
222
+ : "")
223
+ .filter(Boolean)
224
+ .join("\n");
225
+ }
226
+ function renderCompositionBrief(snapshot) {
227
+ const ctx = snapshot.composition_context && typeof snapshot.composition_context === "object"
228
+ ? snapshot.composition_context
229
+ : null;
230
+ const cast = Array.isArray(snapshot.cast) ? snapshot.cast : null;
231
+ if (!ctx && !(cast && cast.length)) {
232
+ return null;
233
+ }
234
+ const lines = [
235
+ "COMPOSITION BRIEF — stable context about THIS template, true for the whole conversation. (The live, changing timeline state — layers, gaps, selection, pending generations — is in the <editor_context> block on the latest user message; those stable fields are NOT repeated there.)"
236
+ ];
237
+ if (ctx) {
238
+ lines.push("composition_context (what this template IS — format/genre + narrative arc; match it when writing captions/text/copy):");
239
+ lines.push(JSON.stringify(ctx, null, 2));
240
+ }
241
+ if (cast && cast.length) {
242
+ lines.push("cast (recurring people/characters for consistent regeneration):");
243
+ lines.push(JSON.stringify(cast, null, 2));
244
+ }
245
+ return lines.join("\n");
246
+ }
247
+ // Pull the stable composition brief (composition_context + cast) out of the most
248
+ // recent user message's editor_context so it can live once in the system prompt.
249
+ export function extractEditorChatCompositionBrief(messages) {
250
+ if (!Array.isArray(messages)) {
251
+ return null;
252
+ }
253
+ for (let i = messages.length - 1; i >= 0; i -= 1) {
254
+ const entry = messages[i];
255
+ if (!entry || typeof entry !== "object" || entry.role !== "user") {
256
+ continue;
257
+ }
258
+ const match = EDITOR_CONTEXT_BLOCK_RE.exec(readRawMessageText(entry));
259
+ if (!match) {
260
+ continue;
261
+ }
262
+ try {
263
+ return renderCompositionBrief(JSON.parse(match[1]));
264
+ }
265
+ catch {
266
+ return null;
267
+ }
268
+ }
269
+ return null;
270
+ }
271
+ // Rewrite one user message's text: latest turn keeps its volatile snapshot (with
272
+ // the stable brief fields removed, since they're now in the system prompt); older
273
+ // turns drop the whole editor_context block so stale timeline state never lingers.
274
+ function rewriteEditorContextInText(text, isLatest) {
275
+ const match = EDITOR_CONTEXT_BLOCK_RE.exec(text);
276
+ if (!match) {
277
+ return text;
278
+ }
279
+ const before = text.slice(0, match.index);
280
+ const after = text.slice(match.index + match[0].length);
281
+ if (!isLatest) {
282
+ return `${before.replace(/\s+$/, "")}${after}`;
283
+ }
284
+ try {
285
+ const parsed = JSON.parse(match[1]);
286
+ for (const key of EDITOR_CONTEXT_STABLE_KEYS) {
287
+ delete parsed[key];
288
+ }
289
+ return `${before}<editor_context>\n${JSON.stringify(parsed, null, 2)}\n</editor_context>${after}`;
290
+ }
291
+ catch {
292
+ return text;
293
+ }
294
+ }
295
+ export function toEditorChatModelMessages(messages) {
296
+ const result = [];
297
+ for (const entry of messages) {
298
+ if (!entry || typeof entry !== "object") {
299
+ continue;
300
+ }
301
+ const role = entry.role;
302
+ if (role !== "user" && role !== "assistant" && role !== "system" && role !== "tool") {
303
+ continue;
304
+ }
305
+ if (role === "system") {
306
+ continue;
307
+ }
308
+ const content = Array.isArray(entry.content)
309
+ ? entry.content
310
+ : [];
311
+ if (role === "user") {
312
+ const parts = [];
313
+ for (const part of content) {
314
+ if (part.type === "text" && typeof part.text === "string") {
315
+ parts.push({ type: "text", text: part.text });
316
+ continue;
317
+ }
318
+ if (part.type !== "file" || typeof part.data !== "string") {
319
+ continue;
320
+ }
321
+ const mediaType = typeof part.mediaType === "string" && part.mediaType.trim()
322
+ ? part.mediaType.trim()
323
+ : "application/octet-stream";
324
+ const decoded = decodeDataUrl(part.data);
325
+ if (decoded) {
326
+ parts.push({
327
+ type: "file",
328
+ data: decoded.data,
329
+ mediaType: mediaType || decoded.mediaType
330
+ });
331
+ continue;
332
+ }
333
+ const url = tryParseUrl(part.data);
334
+ if (!url) {
335
+ continue;
336
+ }
337
+ parts.push({
338
+ type: "file",
339
+ data: url,
340
+ mediaType
341
+ });
342
+ }
343
+ result.push({ role: "user", content: parts });
344
+ continue;
345
+ }
346
+ if (role === "assistant") {
347
+ const parts = [];
348
+ for (const part of content) {
349
+ if (part.type === "text" && typeof part.text === "string") {
350
+ parts.push({ type: "text", text: part.text });
351
+ }
352
+ }
353
+ result.push({ role: "assistant", content: parts });
354
+ continue;
355
+ }
356
+ }
357
+ // Second pass: strip stale editor_context snapshots (keep only the latest turn's)
358
+ // and remove the stable brief fields from that latest snapshot.
359
+ let lastUserIndex = -1;
360
+ for (let i = 0; i < result.length; i += 1) {
361
+ if (result[i].role === "user") {
362
+ lastUserIndex = i;
363
+ }
364
+ }
365
+ for (let i = 0; i < result.length; i += 1) {
366
+ const message = result[i];
367
+ if (message.role !== "user" || !Array.isArray(message.content)) {
368
+ continue;
369
+ }
370
+ const isLatest = i === lastUserIndex;
371
+ for (const part of message.content) {
372
+ if (part.type === "text" && typeof part.text === "string") {
373
+ part.text = rewriteEditorContextInText(part.text, isLatest);
374
+ }
375
+ }
376
+ }
377
+ return result;
378
+ }
379
+ function getModelMessageText(message) {
380
+ const content = message.content;
381
+ if (typeof content === "string") {
382
+ return content;
383
+ }
384
+ if (!Array.isArray(content)) {
385
+ return "";
386
+ }
387
+ return content
388
+ .map((part) => {
389
+ if (part && typeof part === "object" && "text" in part && typeof part.text === "string") {
390
+ return part.text;
391
+ }
392
+ if (part && typeof part === "object" && "type" in part && part.type === "file") {
393
+ const mediaType = "mediaType" in part && typeof part.mediaType === "string"
394
+ ? part.mediaType
395
+ : "file";
396
+ return `[${mediaType} attachment]`;
397
+ }
398
+ return "";
399
+ })
400
+ .filter(Boolean)
401
+ .join("\n");
402
+ }
403
+ function estimateModelMessageChars(messages) {
404
+ return messages.reduce((total, message) => total + getModelMessageText(message).length, 0);
405
+ }
406
+ function truncateEditorChatSummaryText(value, maxChars) {
407
+ const normalized = value.trim().replace(/\s+/g, " ");
408
+ if (normalized.length <= maxChars) {
409
+ return normalized;
410
+ }
411
+ return `${normalized.slice(0, Math.max(0, maxChars - 32)).trimEnd()}... [truncated]`;
412
+ }
413
+ export function compactEditorChatModelMessages(messages) {
414
+ if (messages.length <= EDITOR_CHAT_COMPACT_RECENT_MESSAGES
415
+ || estimateModelMessageChars(messages) <= EDITOR_CHAT_COMPACT_CHAR_THRESHOLD) {
416
+ return messages;
417
+ }
418
+ const recentMessages = messages.slice(-EDITOR_CHAT_COMPACT_RECENT_MESSAGES);
419
+ const oldMessages = messages.slice(0, -EDITOR_CHAT_COMPACT_RECENT_MESSAGES);
420
+ const summaryLines = [];
421
+ let summaryChars = 0;
422
+ for (const message of oldMessages) {
423
+ const line = `[${message.role}]: ${truncateEditorChatSummaryText(getModelMessageText(message), EDITOR_CHAT_COMPACT_MAX_MESSAGE_CHARS)}`;
424
+ if (summaryChars + line.length > EDITOR_CHAT_COMPACT_MAX_SUMMARY_CHARS) {
425
+ summaryLines.push(`[${oldMessages.length - summaryLines.length} earlier messages omitted]`);
426
+ break;
427
+ }
428
+ summaryLines.push(line);
429
+ summaryChars += line.length;
430
+ }
431
+ return [
432
+ {
433
+ role: "assistant",
434
+ content: [
435
+ {
436
+ type: "text",
437
+ text: [
438
+ "[Earlier in this conversation - compacted summary]",
439
+ "This summary preserves older turns so the current request can continue without resending the full transcript.",
440
+ "",
441
+ summaryLines.join("\n\n")
442
+ ].join("\n")
443
+ }
444
+ ]
445
+ },
446
+ ...recentMessages
447
+ ];
448
+ }
449
+ //# sourceMappingURL=editor-chat.js.map