@mevdragon/vidfarm-devcli 0.17.0 → 0.18.1

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 (55) hide show
  1. package/.agents/skills/vidfarm-media/SKILL.md +43 -6
  2. package/SKILL.director.md +37 -23
  3. package/SKILL.platform.md +6 -6
  4. package/demo/dist/app.js +69 -69
  5. package/demo/dist/favicon.ico +0 -0
  6. package/dist/src/app.js +1832 -213
  7. package/dist/src/cli.js +238 -17
  8. package/dist/src/devcli/clip-store.js +29 -2
  9. package/dist/src/devcli/clips.js +116 -73
  10. package/dist/src/devcli/composition-edit.js +262 -0
  11. package/dist/src/devcli/timeline-edit.js +283 -0
  12. package/dist/src/editor-chat.js +29 -6
  13. package/dist/src/frontend/discover-client.js +130 -0
  14. package/dist/src/frontend/discover-store.js +23 -0
  15. package/dist/src/frontend/file-directory.js +744 -0
  16. package/dist/src/frontend/template-editor-chat.js +22 -19
  17. package/dist/src/landing-page.js +24 -7
  18. package/dist/src/page-shell.js +25 -1
  19. package/dist/src/reskin/agency-page.js +214 -170
  20. package/dist/src/reskin/calendar-page.js +503 -187
  21. package/dist/src/reskin/chat-page.js +739 -299
  22. package/dist/src/reskin/discover-page.js +1450 -279
  23. package/dist/src/reskin/document.js +1392 -16
  24. package/dist/src/reskin/help-page.js +139 -100
  25. package/dist/src/reskin/index-page.js +1 -1
  26. package/dist/src/reskin/inpaint-page.js +547 -0
  27. package/dist/src/reskin/job-runs-page.js +355 -127
  28. package/dist/src/reskin/library-page.js +1188 -317
  29. package/dist/src/reskin/login-page.js +124 -87
  30. package/dist/src/reskin/pricing-page.js +197 -166
  31. package/dist/src/reskin/settings-page.js +566 -187
  32. package/dist/src/reskin/theme.js +434 -17
  33. package/dist/src/services/clip-curation/gemini.js +5 -0
  34. package/dist/src/services/clip-curation/hunt.js +79 -1
  35. package/dist/src/services/clip-curation/index.js +2 -1
  36. package/dist/src/services/clip-curation/local-agent.js +4 -3
  37. package/dist/src/services/clip-curation/media-select.js +85 -0
  38. package/dist/src/services/clip-curation/query.js +5 -1
  39. package/dist/src/services/clip-curation/refine.js +50 -20
  40. package/dist/src/services/clip-curation/scan.js +10 -3
  41. package/dist/src/services/clip-curation/taxonomy.js +3 -1
  42. package/dist/src/services/clip-curation/taxonomy.v1.json +13 -1
  43. package/dist/src/services/clip-records.js +14 -1
  44. package/dist/src/services/clip-search.js +43 -13
  45. package/dist/src/services/file-directory.js +114 -0
  46. package/dist/src/services/storage.js +24 -1
  47. package/dist/src/services/upstream.js +5 -5
  48. package/dist/src/template-editor-shell.js +16 -2
  49. package/package.json +1 -1
  50. package/public/assets/discover-client-app.js +1 -0
  51. package/public/assets/file-directory-app.js +2 -0
  52. package/public/assets/homepage-client-app.js +12 -12
  53. package/public/assets/page-runtime-client-app.js +24 -24
  54. package/src/assets/favicon.ico +0 -0
  55. package/src/assets/logo-vidfarm.png +0 -0
@@ -43,6 +43,7 @@ export function buildTemplateEditorChatSystemPrompt(input) {
43
43
  "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] } }.",
44
44
  "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.",
45
45
  "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.",
46
+ "IMAGE UTILITY PRIMITIVES beyond generate/edit are also first-class here. POST /api/v1/primitives/images/inpaint with body { tracer, payload: { source_image_url, mask_url, instruction } } does a MASKED edit — reach for it when only a region of an image should change and a mask is available. POST /api/v1/primitives/images/render-html with body { tracer, payload: { html, css?, width?, height?, background_color? } } rasterizes HTML/CSS into a still image — use it for title cards, quote cards, data callouts, or any designed graphic you want as a timeline image. POST /api/v1/primitives/images/remove-background with body { tracer, payload: { source_image_url } } returns a transparent-background PNG cut-out of the subject — use it for overlays, product cut-outs, character sprite cards, or caption/subject matting. IMPORTANT: remove-background BILLS THE WALLET (RapidAPI), unlike the BYOK image generate/edit routes, so only call it when a real cut-out is needed.",
46
47
  "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.",
47
48
  "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/remove-captions, /videos/extract-audio, /videos/probe, /videos/extract-frame, /videos/mute, /videos/replace-audio, /videos/concat, /audio/trim, /audio/probe, /audio/concat, and /audio/normalize. If the user asks to remove burned-in captions, subtitles, or on-screen text from a video, call POST /api/v1/primitives/videos/remove-captions with body { tracer, payload: { source_video_url } } — an async job that returns a durable caption-free MP4 and bills about $0.10 per 30 seconds of source video. Caption removal is capped at ~15 minutes of source and must NEVER be used on long-form video: when the user wants short clips out of a long video (a podcast, stream VOD, webinar, or a YouTube/TikTok/IG/X URL), call POST /clips/scan instead with body { source_url | temp_file_id | s3_key, prompt, ranges?: [\"MM:SS-MM:SS\"], target_duration_sec?, aspect?: \"9:16\"|\"16:9\"|\"4:3\"|\"1:1\", avoid_text?, tracer? } — target_duration_sec is a SOFT range (30 → 20-40s), ranges restrict the hunt to those source windows (big cost saver), aspect crops every clip, and avoid_text prefers text-free scenes via scene SELECTION (never caption removal on the source). The hunt is async (202 + scan_id): poll GET /clips/scan/:scanId, then browse GET /clips/feed?source=<source_video_id> or POST /clips/search. Hunts bill AWS compute only — the AI tagging runs on the user's own saved provider keys. To erase captions from a finished HUNTED clip, pass that short mp4 to /videos/remove-captions afterwards.",
48
49
  "SPEECH PRIMITIVES (TTS/STT) are first-class here. VOICEOVER / NARRATION: when the user wants a script, hook, caption, or dub read aloud as AI audio, call POST /api/v1/primitives/audio/speech with body { tracer, payload: { text, voice?, instructions?, provider?, model?, output_format? } } — instructions is a free-form voice-STYLE prompt (tone, pacing, accent, emotion, persona; e.g. 'calm warm bedtime narrator' or 'excited sports announcer, fast paced'), and voice picks a provider preset (OpenAI: alloy/ash/coral/...; Gemini: Kore/Puck/Charon/...). When sending a voice you may omit provider — it is inferred from the voice preset; a voice paired with the WRONG provider (e.g. voice 'Kore' with provider 'openai') is rejected with a 400, so never mix families. The finished job returns a durable audio URL — place it on the timeline with editor_action add_layer (kind=audio, src=that URL), typically starting where the narration should begin. TRANSCRIPTION: when the user wants a transcript, subtitles/captions from speech, a translation source, or to know who says what, call POST /api/v1/primitives/audio/transcribe with body { tracer, payload: { source_url, diarize?, language?, prompt? } } — source_url may be a VIDEO or AUDIO URL (video audio is demuxed automatically; pick the fork's source via GET /api/v1/compositions/:forkId/remove-video-captions first when transcribing the current composition). One job returns BOTH formats: a simple subtitle version (plain transcript text + timed SRT cues, stored as transcript.txt/transcript.srt) and an advanced multi-speaker version (speaker-attributed timed segments in output.segments and transcript.json) for multi-narration sources like podcasts and interviews. diarize defaults to true and speaker attribution needs a Gemini key — openai/openrouter keys degrade to a plain single-speaker transcript. For transcript-grounded caption work on THIS fork, the read-only video_context tool may already contain the decompose-time transcript; prefer it before running a new billable-key transcription, and use /audio/transcribe for other URLs, multi-speaker attribution, or SRT output. REGENERATE / REVOICE: when the user wants the SAME speaker to say DIFFERENT words (reword the narration, fix a line, change an offer or CTA, redub in the original voice), call POST /api/v1/primitives/audio/regenerate-speech with body { tracer, payload: { source_url (video or audio; video audio is demuxed), rewrite_instruction (how to reword the original transcript) OR text (the exact new script — one of the two is required), voice?, instructions?, provider?, model?, language?, output_format? } }. One job listens to the source, profiles the speaker's voice, rewords the script, and returns a durable audio URL spoken by the CLOSEST PRESET voice with a style prompt matched to the speaker (result fields: audioUrl, script, original_transcript, voice_profile, voice_match). Voice profiling needs a saved Gemini key — with only openai/openrouter keys the job degrades to transcript-only rewording with a default or explicitly passed voice and voice_match 'none'. IMPORTANT HONESTY RULE: OpenAI and Gemini APIs cannot clone voice identity (preset voices only), so always tell the user the regenerated audio APPROXIMATES the original voice rather than clones it. To swap the regenerated audio into the composition, mute the original span (/videos/mute or volume/muted on the layer) and place the new audio with editor_action add_layer (kind=audio, src=audioUrl).",
@@ -67,11 +68,13 @@ export function buildTemplateEditorChatSystemPrompt(input) {
67
68
  "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.",
68
69
  "If multiple tracers are available, mention which tracer you are using when it matters.",
69
70
  "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.",
70
- "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, export_composition, set_captions, and set_transitions.",
71
+ "If the user asks to add, remove, retime, reposition, resize, restyle, swap media, tag, note, group, ungroup, duplicate, split, animate, keyframe, nudge, ripple, trim, or restack 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, export_composition, set_captions, set_transitions, set_layer_keyframes, nudge_layers, ripple_edit, trim_layer, and set_layer_zindex.",
72
+ "FINE TIMELINE CONTROL — you have trackpad-level verbs; use them instead of rewriting the whole document for small changes: (1) set_layer_keyframes authors a custom, script-free CSS @keyframes animation on ONE layer that previews AND renders — pass layer_key + keyframes:[{offset(0..1), opacity?, translate_x?, translate_y?, scale?, rotate?}, …] (>=2 stops), optional keyframe_easing (linear/ease/ease-in/ease-out/ease-in-out/cubic-bezier(...)) and keyframe_duration (seconds, default = clip duration). translate_x/translate_y are percentages of the layer's OWN box. Example fly-up-and-fade-in: keyframes=[{offset:0,opacity:0,translate_y:40},{offset:1,opacity:1,translate_y:0}]. This is the durable way to hand-author motion beyond the Ken Burns / transition / caption presets — the JS runtime adapters (anime.js/GSAP/Lottie) are devcli-only. (2) nudge_layers shifts one or more layers by a RELATIVE delta_start (seconds) and/or delta_track (lanes) — pass layer_key or layer_keys; grouped clips move together. (3) ripple_edit inserts (delta_start>0) or closes (delta_start<0) time at at_time and shifts every downstream clip so the rest of the timeline stays in sync — use it to make room before an insert or to close a gap you can see in timeline_gaps. (4) trim_layer moves ONE edge to a time: edge='start' (left; advances the in-point, and for video/audio pushes the media start so the visible frame is unchanged) or edge='end' (right; changes duration), with to_time in composition seconds. (5) set_layer_zindex restacks: z_order='front'|'back'|'forward'|'backward' (stacking == track index; higher draws on top) or an explicit track. All of these auto-resolve track collisions and note it in the summary; read the NEXT editor_context to confirm the new start/duration/track/animation landed.",
71
73
  "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.",
72
74
  "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.",
73
75
  "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.",
74
- "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.",
76
+ "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. Each layer also reports its current declarative MOTION so you can read the timeline's animation state instead of guessing: transition / transition_out (scene entrance/exit presets), transition_duration, ken_burns (still-image pan/zoom preset), and animation (the name of a custom CSS keyframe animation authored via set_layer_keyframes). recent_action_results reflects the REAL outcome of your previous turn's editor_action calls — each entry has ok:true with a summary (the edit landed) or ok:false with an error (it did not); trust it over the tool's echoed args, and if an edit failed, read the error and retry or explain rather than repeating a false success.",
77
+ "READ THE EXACT MARKUP WHEN YOU NEED IT: editor_context is a derived summary, not the raw HTML. When you need the precise current composition markup — to author or fix a replace_composition_html, to inspect exact inline styles, class names, or data-* attributes on a layer, or to copy an existing structure — fetch it with http_request GET /api/v1/compositions/{composition_id}/composition.html (composition_id is in editor_context). Prefer the targeted editor_action verbs for ordinary edits; pull the HTML only when you genuinely need byte-level detail.",
75
78
  "CRITICAL — verify your edits actually landed. An editor_action tool result only echoes the arguments you sent; it is NOT proof the change was applied or saved. The real outcome shows up in the NEXT <editor_context>. Before telling the user an edit succeeded, check that block: (1) if it contains save_status:\"error\" (or is_read_only:true / a save_error message), the composition is NOT being saved — do NOT claim any edit worked; tell the user exactly what save_error says (e.g. the video is read-only from this link and they should open their own copy to edit) and stop making mutating editor_action calls until it clears; (2) if it contains recent_action_results, each entry with ok:false is an edit from your previous turn that FAILED (read its error) — acknowledge the failure and retry or explain it rather than repeating a false success; (3) otherwise confirm the intended change is reflected in the layers (e.g. the new text on the target layer) before saying it's done. Never report a timeline edit as successful without this check.",
76
79
  "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.",
77
80
  "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.",
@@ -93,14 +96,16 @@ export function buildTemplateEditorChatSystemPrompt(input) {
93
96
  "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.",
94
97
  "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/remove-video-captions 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 /remove-video-captions-poll unless the user explicitly asks to advance the subtitle-removal job. To strip burned-in captions from any OTHER video URL (not this fork's decompose flow), use the POST /api/v1/primitives/videos/remove-captions primitive instead.",
95
98
  "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).",
96
- "KNOWLEDGE ON DEMAND (load_skill): deeper authoring knowledge ships as vidfarm skill packs you can read mid-conversation with the load_skill tool — the composition HTML contract (hyperframes-core), motion/animation craft and transition doctrine (hyperframes-animation), seek-safe keyframe patterns (hyperframes-keyframes), creative direction/palettes/typography (hyperframes-creative), caption identities (embedded-captions), media/audio resolution (vidfarm-media), and end-to-end workflow playbooks (product-launch-video, faceless-explainer, website-to-video, general-video, motion-graphics, slideshow, talking-head-recut). Call load_skill with the pack name to read its SKILL.md; it lists reference files you can then load by relative path (e.g. file='references/step-4-vo.md'). Use it when a request needs craft beyond the built-in preset vocabulary — designing a whole composition via replace_composition_html, picking motion or transition language for a style, or following a named workflow. Load only what the task needs, never bulk-load packs, and never paste large skill content back to the user.",
97
- "replace_composition_html is validated before it reaches the editor: when the HTML violates the composition contract (missing data-composition-id, invalid clip timing, same-track overlaps, unknown preset names, media without src) the tool result comes back with rejected=true and lint_errors, and NOTHING is applied — fix the reported issues and call it again. lint_warnings are advisory and the action still applies (e.g. <script> tags survive local renders but are stripped on cloud/web saves — keep web-editor compositions declarative).",
99
+ "KNOWLEDGE ON DEMAND (load_skill): deeper authoring knowledge ships as vidfarm skill packs you can read mid-conversation with the load_skill tool — the composition HTML contract (hyperframes-core), motion/animation craft and transition doctrine (hyperframes-animation), seek-safe keyframe patterns (hyperframes-keyframes), creative direction/palettes/typography (hyperframes-creative), caption identities (embedded-captions), media/audio resolution (vidfarm-media), and end-to-end workflow playbooks (product-launch-video, faceless-explainer, website-to-video, general-video, motion-graphics, slideshow, talking-head-recut). Call load_skill with the pack name to read its SKILL.md; it lists reference files you can then load by relative path (e.g. file='references/step-4-vo.md'). Use it when a request needs craft beyond the built-in preset vocabulary — designing a whole composition via replace_composition_html, picking motion or transition language for a style, or following a named workflow. NOTE: hyperframes-animation and hyperframes-keyframes describe BOTH script-driven adapters (anime.js/GSAP/Lottie/Three.js) AND script-free CSS techniques — in the web editor apply only the CSS @keyframes / declarative-preset half (the JS adapters are stripped on save; see the web-editor motion rule), and reserve the adapter half for local devcli work. Load only what the task needs, never bulk-load packs, and never paste large skill content back to the user.",
100
+ "replace_composition_html is validated before it reaches the editor: when the HTML violates the composition contract (missing data-composition-id, invalid clip timing, same-track overlaps, unknown preset names, media without src) the tool result comes back with rejected=true and lint_errors, and NOTHING is applied — fix the reported issues and call it again. lint_warnings are advisory and the action still applies.",
101
+ "MOTION IN THE WEB EDITOR IS DECLARATIVE / CSS ONLY — no author <script>. This editor strips every <script> tag on save (stored-XSS defense), so replace_composition_html that contains a <script> is REJECTED (rejected=true), not applied. That means the JavaScript runtime animation adapters — anime.js, GSAP, Lottie, Three.js, TypeGPU, WAAPI-driven code — are NOT available here; they are a local-devcli-only capability. Author all motion for the web editor with the durable, script-free vocabulary: (1) built-in preset actions — set_layer_media ken_burns (still-image pan/zoom), set_transitions / transition + transition_out (scene entrances/exits), set_captions / caption_animation (word-by-word captions); and (2) plain CSS — @keyframes rules in a <style> block plus an inline animation on the layer element (the renderer scrubs and bakes CSS @keyframes deterministically into the final MP4, exactly like the preview). Never try to drive motion with anime.js/GSAP/etc. in the web editor, and never claim you added a scripted animation here. If the user explicitly wants a JS-adapter animation (a Lottie file, a GSAP timeline, a Three.js scene), tell them that is a local devcli / hyperframes-CLI workflow and offer the CSS/preset equivalent for the web editor instead.",
98
102
  "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.",
99
- "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=search with a plain-language query to find files by MEANING (keyword + vector match over every file's name, folder, and metadata notes prefer it over paging list when the library is large or the user references an asset vaguely), action=read with a file_id (copied from a prior list/search) to fetch one file, action=write with file_name + content (or source_url) to SAVE a file, and action=annotate with file_id + notes to attach metadata notes to any existing 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 search or 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 or search to discover the real ones first.",
103
+ "FILE DIRECTORY: the user's files live in ONE navigable directory with three canonical roots, each with nested subfolders and a copyable path per file: (1) /files — durable 'My Files' assets they keep and reuse (videos mp4/mov/webm, images png/jpg/jpeg/gif/webp/svg, audio mp3/wav/m4a/aac, documents pdf/md/txt/csv), vector-searchable via metadata notes; (2) /temp scratch space auto-foldered by date (YYYY-MM-DD) and auto-deleted after 30 days; (3) /raws — the reusable clip / source-footage library, foldered by source (e.g. /raws/<source-slug>/). A file's canonical path looks like /raws/product-demos/clip.mp4 or /files/acme-skincare/logo.png. Use the browse_files tool to navigate, search, read, write, and reorganize it: action=list with path (e.g. path='/', '/raws', '/files/brand-assets') enumerates that folder's subfolders + files as canonical items (path, name, kind, view_url); action=search with a plain-language query finds files by MEANING across roots — it combines semantic vector match, substring, and absolute-path match (scope with path='/raws' for footage, '/files' for durable assets, '/' for everything; force one signal with mode=semantic|substring|path). Prefer search over paging list when the library is large or the user references an asset vaguely. When you omit path entirely, list/search now default to '/' (the whole directory) so /raws and /temp are never hidden — but pass path='/raws' to stay in footage. For /raws you can add content_type to filter by EXACT shot kind (talking_head, b_roll, product_shot, screen_recording, demo, reaction, interview, establishing, lifestyle, text_graphic; comma-separate to OR them) — this is an exact tag filter, distinct from the semantic query. For a big folder, list returns next_offset; pass it back as offset to page deeper (with limit up to 500). action=read with a file_id (copied from a prior list/search) fetches one file; action=write with file_name + content (or source_url) SAVES a text/media file into /files; action=annotate with file_id + notes attaches vector-embedded metadata to a /files entry; action=move with file_id + a target path (e.g. path='/raws/demos') reorganizes a raw between /raws folders. When the user references their own footage, brand assets, logos, music, a brief, a script, or raw clips ('use my product photo', 'the video I uploaded', 'my brand logo', 'that clip of the founder'), browse_files search or 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, paths, or view URLs — always list or search to discover the real ones first.",
100
104
  "SAVING CONTEXT TO MY FILES: browse_files action=write persists durable, reusable context the user (and future chats/agents) can read back. write saves text files (md/txt/csv/json/srt/vtt) via content, and IMPORTS media into the library via source_url (a durable URL from a finished primitive job or existing asset) — use source_url to persist a generated asset the user will want again (a character sprite card, a recurring background, a logo variant) instead of leaving it stranded in a job result. 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'. Pass notes on write (or annotate afterwards) for any asset worth finding again: notes describe what the file is, who/what it depicts, and when to use it, and they are vector-embedded so action=search finds the file by meaning in future sessions. Before overwriting an existing file of the same name in the same folder, confirm with the user (writing the same name replaces it).",
101
105
  "CHARACTER CONSISTENCY: when the user has a recurring character — a mascot, spokesperson, avatar, or product character that should look the same across videos — persist the character to My Files so every future generation can reference it, namescoped under the offer's folder (e.g. 'acme-skincare/characters/zara/'): (1) about_character.md — name, role, personality, physical description (face, build, wardrobe, colors), voice/tone, and do/don'ts, saved via write with content; (2) character_sprite_card.png — ONE reference-sheet image showing the character consistently (full body front/side/back plus a face close-up, neutral background). If the user has no sprite card, offer to generate one with POST /api/v1/primitives/images/generate (image gen is cheap; pass their best character photos as prompt_attachments if they exist, and ask the image for a 'character reference sheet / sprite card' layout), then save the finished job's URL into My Files via write with source_url + file_name='character_sprite_card.png'. Annotate BOTH files with notes naming the character so browse_files search finds them from any phrasing ('our mascot', 'the fox character'). From then on, character consistency = always pass the sprite card's view_url as the reference input on generation: prompt_attachments for images/generate and images/edit reference_attachments, input_references for videos/generate (or generate_layer's prompt_attachments/input_references), and pull wording from about_character.md into the prompt. Before generating ANY recurring character, search My Files for an existing sprite card first — never regenerate a character from memory when a reference exists.",
102
106
  "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.",
103
107
  "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.",
108
+ "BILLING MODEL PER PRIMITIVE — know which class a route is in before calling it, and let it drive when-to-use judgment: (a) WALLET routes spend the customer's real credits — images/remove-background and videos/download (RapidAPI), videos/remove-captions (GhostCut, ~$0.10 per 30s of source), video ingest, and /clips/scan (bills AWS compute only; its BYOK AI tagging is deliberately never billed). (b) BYOK routes run on the customer's own saved provider keys with no vidfarm wallet charge — images/generate, images/edit, images/inpaint, videos/generate, audio/speech, audio/transcribe, audio/captions, audio/regenerate-speech, and all brainstorm/* (AI image/video generation still incurs the provider's own usage cost passed through). (c) COMPUTE-ONLY routes are cheap platform ffmpeg jobs — images/render-html, videos/trim, videos/probe, videos/concat, videos/mute, videos/normalize, videos/extract-audio, videos/extract-frame, videos/replace-audio, audio/trim, audio/concat, audio/normalize, audio/probe, media/dedupe, videos/render-slides, and the timeline render. Use COMPUTE and BYOK-image routes freely; before calling a WALLET route or AI video generation, surface the cost and (when a budget matters) confirm with the user first.",
104
109
  "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.",
105
110
  "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.",
106
111
  "If polling a job, poll gently. Prefer about every 30 seconds rather than rapid repeated checks.",
@@ -135,7 +140,7 @@ export function buildTemplateEditorChatSystemPrompt(input) {
135
140
  `Composition description: ${input.templateDescription}`,
136
141
  "",
137
142
  "Documented REST routes:",
138
- routeLines || "- No routes documented."
143
+ routeLines || "- Every /api/v1/primitives/* media route is available from this editor — see the primitive guidance above for the exact payload and when-to-use of each (image generate/edit/inpaint/render-html/remove-background, video generate/download/trim/probe/concat/mute/normalize/extract-audio/extract-frame/replace-audio/render-slides/remove-captions, media dedupe, audio speech/transcribe/captions/regenerate-speech/trim/concat/normalize/probe, timeline render, and brainstorm coldstart/awareness_stages/angles/hooks/product_placement). Call GET /api/v1/primitives to enumerate the live catalog with fields, and GET /api/v1/primitives/jobs/:job_id for status."
139
144
  ].join("\n");
140
145
  }
141
146
  export function buildEditorChatSystemInstruction(input) {
@@ -670,6 +675,24 @@ export function lintReplaceCompositionAction(args) {
670
675
  explanation: args.explanation
671
676
  };
672
677
  }
678
+ // Web-editor scope only: <script> tags are stripped on every cloud save
679
+ // (stored-XSS defense), so script-driven runtime animation (anime.js, GSAP,
680
+ // Lottie, Three.js) would preview once and then silently vanish on the next
681
+ // autosave — a false success. Reject it here rather than let it strip
682
+ // silently, and steer the agent to the durable declarative/CSS vocabulary.
683
+ // The underlying lintCompositionHtml keeps this a WARNING so devcli
684
+ // (`vidfarm lint`, local serve renders scripts) stays permissive — the JS
685
+ // runtime adapters remain a devcli-only capability.
686
+ const scriptWarning = lint.warnings.find((issue) => issue.code === "scripts_stripped_on_save");
687
+ if (scriptWarning) {
688
+ return {
689
+ rejected: true,
690
+ reason: "composition_html contains <script> tags and was NOT applied. The web editor strips all scripts on save (stored-XSS defense), so script-driven runtime animation (anime.js, GSAP, Lottie, Three.js) does not survive here. Re-author the motion with the durable web vocabulary instead: CSS @keyframes + inline animation on the layer, plus the built-in presets (data-kenburns / set_layer_media ken_burns, data-transition / set_transitions, data-caption-animation / set_captions). The JS runtime adapters are a local-devcli-only capability — do not attempt them in the web editor.",
691
+ lint_errors: [scriptWarning],
692
+ lint_warnings: lint.warnings,
693
+ explanation: args.explanation
694
+ };
695
+ }
673
696
  if (lint.warnings.length) {
674
697
  return { ...args, lint_warnings: lint.warnings };
675
698
  }
@@ -0,0 +1,130 @@
1
+ // Client runtime for the reskinned /discover/feed page. It owns the feed's
2
+ // Zustand cache + the stale-while-revalidate fetch policy, and exposes a tiny
3
+ // imperative bridge (window.__vidfarmDiscover) that the page's inline script
4
+ // calls. Kept intentionally framework-free: the reskin page is SSR HTML, so
5
+ // this module just hydrates the store and hands the inline code a loader.
6
+ //
7
+ // Loading contract (what "snappy without redundant SSR load" means here):
8
+ // 1. The server inlines the first page of the default feed into the boot
9
+ // JSON (feedInitial). The inline script paints THAT immediately — zero
10
+ // follow-up fetch on first load.
11
+ // 2. This module seeds the same snapshot into the store so a back/forward
12
+ // navigation (or BFCache restore) is an instant cache hit.
13
+ // 3. Anything older than TTL_MS revalidates in the background; a fresh result
14
+ // replaces the snapshot and re-paints. Search queries always fetch fresh
15
+ // and are never cached.
16
+ import { createDiscoverStore } from "./discover-store.js";
17
+ // How long a cached default-feed snapshot is trusted before a background
18
+ // revalidation. Short on purpose: long enough to make back-navigation feel
19
+ // instant, short enough that an Add New / decompose elsewhere self-heals fast.
20
+ const TTL_MS = 20_000;
21
+ function readBoot() {
22
+ const el = document.querySelector("[data-rk-discover-boot]");
23
+ if (!el?.textContent)
24
+ return {};
25
+ try {
26
+ return JSON.parse(el.textContent) || {};
27
+ }
28
+ catch {
29
+ return {};
30
+ }
31
+ }
32
+ const boot = readBoot();
33
+ const accountId = boot.accountId || "";
34
+ const feedEndpoint = boot.feedEndpoint || "/discover/feed";
35
+ const store = createDiscoverStore(`vidfarm.discover.feed.v1.${accountId || "anon"}`);
36
+ // Seed the store from the SSR snapshot. This runs AFTER persist has rehydrated
37
+ // from sessionStorage, so the freshest source (the server render we're painting
38
+ // right now) wins over any older persisted copy.
39
+ if (boot.feedInitial && Array.isArray(boot.feedInitial)) {
40
+ const parsed = boot.feedFetchedAt ? Date.parse(boot.feedFetchedAt) : Number.NaN;
41
+ store.getState().setDefaultFeed(boot.feedInitial, Number.isFinite(parsed) ? parsed : Date.now());
42
+ }
43
+ function buildUrl(query) {
44
+ try {
45
+ const u = new URL(feedEndpoint, window.location.href);
46
+ if (query)
47
+ u.searchParams.set("q", query);
48
+ if (accountId)
49
+ u.searchParams.set("account", accountId);
50
+ return u.toString();
51
+ }
52
+ catch {
53
+ return feedEndpoint;
54
+ }
55
+ }
56
+ async function fetchFeed(query) {
57
+ const r = await fetch(buildUrl(query), {
58
+ credentials: "same-origin",
59
+ headers: { accept: "application/json" }
60
+ });
61
+ if (!r.ok) {
62
+ throw new Error(r.status === 401
63
+ ? "Sign in to browse the template feed."
64
+ : `Couldn't load templates (${r.status}).`);
65
+ }
66
+ const j = (await r.json());
67
+ return Array.isArray(j?.templates) ? j.templates : [];
68
+ }
69
+ function loadFeed(query, handlers) {
70
+ const q = (query || "").trim();
71
+ // Search: always fresh, never cached.
72
+ if (q) {
73
+ handlers.onLoading?.();
74
+ fetchFeed(q)
75
+ .then((items) => handlers.onResult(items, { fromCache: false, stale: false }))
76
+ .catch((err) => handlers.onError(err instanceof Error ? err : new Error(String(err))));
77
+ return;
78
+ }
79
+ const cached = store.getState().defaultFeed;
80
+ const now = Date.now();
81
+ if (cached && cached.items.length) {
82
+ const stale = now - cached.fetchedAt > TTL_MS;
83
+ handlers.onResult(cached.items, { fromCache: true, stale });
84
+ if (stale) {
85
+ // Background revalidate — keep the painted feed on failure.
86
+ fetchFeed("")
87
+ .then((items) => {
88
+ store.getState().setDefaultFeed(items, Date.now());
89
+ handlers.onResult(items, { fromCache: false, stale: false });
90
+ })
91
+ .catch(() => { });
92
+ }
93
+ return;
94
+ }
95
+ // Cold (no seed, no cache) — foreground fetch.
96
+ handlers.onLoading?.();
97
+ fetchFeed("")
98
+ .then((items) => {
99
+ store.getState().setDefaultFeed(items, Date.now());
100
+ handlers.onResult(items, { fromCache: false, stale: false });
101
+ })
102
+ .catch((err) => handlers.onError(err instanceof Error ? err : new Error(String(err))));
103
+ }
104
+ // BFCache guard: the back/forward cache can restore a minutes-old page without
105
+ // re-running the server render. If the restored snapshot is stale (or the page
106
+ // came straight from BFCache), revalidate and broadcast so the inline script
107
+ // re-paints — but only when the user isn't mid-search.
108
+ window.addEventListener("pageshow", (event) => {
109
+ const cached = store.getState().defaultFeed;
110
+ if (!cached)
111
+ return;
112
+ const restored = event.persisted;
113
+ if (!restored && Date.now() - cached.fetchedAt <= TTL_MS)
114
+ return;
115
+ fetchFeed("")
116
+ .then((items) => {
117
+ store.getState().setDefaultFeed(items, Date.now());
118
+ window.dispatchEvent(new CustomEvent("vidfarm:discover-feed-updated", { detail: { items } }));
119
+ })
120
+ .catch(() => { });
121
+ });
122
+ window.__vidfarmDiscover = {
123
+ store,
124
+ loadFeed,
125
+ invalidate: () => store.getState().clear()
126
+ };
127
+ // Let the inline script know the bridge is live (it also feature-detects the
128
+ // global directly, so this is just a nudge for anything waiting).
129
+ window.dispatchEvent(new CustomEvent("vidfarm:discover-store-ready"));
130
+ //# sourceMappingURL=discover-client.js.map
@@ -0,0 +1,23 @@
1
+ import { createStore } from "zustand/vanilla";
2
+ import { persist, createJSONStorage } from "zustand/middleware";
3
+ /**
4
+ * A vanilla Zustand store (no React needed — the reskin discover page is
5
+ * server-rendered HTML + inline JS) that persists the default feed to
6
+ * sessionStorage. sessionStorage — not localStorage — so the cache dies with
7
+ * the tab and never bleeds a stale feed across sessions.
8
+ *
9
+ * The store is keyed per account (agency multi-account scoping) so switching
10
+ * ?account= never shows another account's cached feed.
11
+ */
12
+ export function createDiscoverStore(sessionKey) {
13
+ return createStore()(persist((set) => ({
14
+ defaultFeed: null,
15
+ setDefaultFeed: (items, fetchedAt) => set({ defaultFeed: { items, fetchedAt } }),
16
+ clear: () => set({ defaultFeed: null })
17
+ }), {
18
+ name: sessionKey,
19
+ version: 1,
20
+ storage: createJSONStorage(() => sessionStorage)
21
+ }));
22
+ }
23
+ //# sourceMappingURL=discover-store.js.map