@slatesvideo/shared 0.7.2 → 0.7.3

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 (84) hide show
  1. package/dist/clients/cloud.d.ts +4 -0
  2. package/dist/clients/cloud.js +11 -3
  3. package/dist/index.d.ts +1 -0
  4. package/dist/index.js +1 -0
  5. package/dist/manual/content.d.ts +1 -1
  6. package/dist/manual/content.js +1 -1
  7. package/dist/operations/index.d.ts +12 -13
  8. package/dist/operations/index.js +158 -133
  9. package/dist/operations/surface.d.ts +6 -2
  10. package/dist/operations/surface.js +29 -5
  11. package/dist/prompts/agent-doctrine.d.ts +4 -4
  12. package/dist/prompts/agent-doctrine.js +17 -28
  13. package/dist/prompts/guide-discovery.d.ts +23 -0
  14. package/dist/prompts/guide-discovery.js +39 -0
  15. package/dist/prompts/guide-retrieval.js +1 -1
  16. package/dist/prompts/model-capabilities.d.ts +8 -9
  17. package/dist/prompts/model-capabilities.js +11 -51
  18. package/dist/prompts/model-facts.d.ts +2 -2
  19. package/dist/prompts/model-facts.js +15 -26
  20. package/dist/prompts/partials.generated.js +6 -3
  21. package/dist/prompts/prompting-tips.d.ts +1 -1
  22. package/dist/prompts/prompting-tips.js +21 -63
  23. package/dist/prompts/search-terms.d.ts +3 -0
  24. package/dist/prompts/search-terms.js +24 -0
  25. package/dist/skills/content.js +36 -37
  26. package/dist/skills/metadata.d.ts +7 -0
  27. package/dist/skills/metadata.js +29 -0
  28. package/exports/slates-chatgpt-images/generated/SKILL.md +7 -1
  29. package/exports/slates-chatgpt-images/generated/slates-chatgpt-images.skill +0 -0
  30. package/exports/slates-prompt-builder/generated/SKILL.md +28 -16
  31. package/exports/slates-prompt-builder/generated/reference-character.md +12 -13
  32. package/exports/slates-prompt-builder/generated/reference-content-policy.md +2 -2
  33. package/exports/slates-prompt-builder/generated/reference-gpt-image-2-5.md +191 -0
  34. package/exports/slates-prompt-builder/generated/reference-kling.md +32 -11
  35. package/exports/slates-prompt-builder/generated/reference-nano-banana.md +24 -6
  36. package/exports/slates-prompt-builder/generated/reference-omni-flash.md +65 -0
  37. package/exports/slates-prompt-builder/generated/reference-seedance-2-5.md +362 -0
  38. package/exports/slates-prompt-builder/generated/reference-seedance.md +34 -4
  39. package/exports/slates-prompt-builder/generated/slates-prompt-builder-manifest.json +77 -23
  40. package/exports/slates-prompt-builder/generated/slates-prompt-builder.skill +0 -0
  41. package/package.json +2 -1
  42. package/skills/_partials/blender-action-curves.md +24 -0
  43. package/skills/_partials/iteration-diagnosis.md +5 -0
  44. package/skills/_partials/model-routing.md +35 -0
  45. package/skills/_partials/seedance-25-timestamps.md +2 -2
  46. package/skills/_partials/still-gate.md +2 -2
  47. package/skills/_partials/thresholds.md +1 -1
  48. package/skills/slates-blocking-to-prompt.md +15 -13
  49. package/skills/slates-camera-language.md +45 -7
  50. package/skills/slates-character-identity.md +8 -6
  51. package/skills/slates-chatgpt-images.md +7 -1
  52. package/skills/slates-cinematic-look.md +1 -1
  53. package/skills/slates-content-policy.md +4 -6
  54. package/skills/slates-cost-discipline.md +18 -12
  55. package/skills/slates-dialogue-blocking.md +6 -6
  56. package/skills/slates-direct-response-ad.md +1 -1
  57. package/skills/slates-edit-and-iterate.md +12 -4
  58. package/skills/slates-model-selection.md +82 -90
  59. package/skills/slates-one-prompt-film.md +1 -1
  60. package/skills/slates-previs-blocking.md +44 -13
  61. package/skills/slates-project-organization.md +2 -2
  62. package/skills/slates-prompting-elevenlabs.md +4 -4
  63. package/skills/slates-prompting-flux-2-max.md +2 -3
  64. package/skills/slates-prompting-gpt-image-2-5.md +2 -2
  65. package/skills/slates-prompting-inworld-tts.md +174 -174
  66. package/skills/slates-prompting-kling-v3.md +11 -9
  67. package/skills/slates-prompting-lip-sync.md +15 -15
  68. package/skills/slates-prompting-ltx-2-5.md +5 -6
  69. package/skills/slates-prompting-minimax-h3.md +11 -11
  70. package/skills/slates-prompting-motion-transfer.md +8 -8
  71. package/skills/slates-prompting-nano-banana-2.md +8 -4
  72. package/skills/slates-prompting-omni-flash.md +9 -9
  73. package/skills/slates-prompting-seed-audio.md +24 -4
  74. package/skills/slates-prompting-seedance-2-5.md +40 -30
  75. package/skills/slates-prompting-seedance.md +4 -4
  76. package/skills/slates-prompting-seedream-5-lite.md +6 -6
  77. package/skills/slates-restyle-from-blocking.md +2 -2
  78. package/skills/slates-script-craft.md +1 -1
  79. package/skills/slates-shot-variety.md +1 -1
  80. package/skills/slates-storyboard-from-script.md +1 -1
  81. package/skills/slates-style-prompting.md +56 -54
  82. package/skills/slates-ugc-influencer-ad.md +1 -1
  83. package/skills/slates-vision-feedback-loop.md +118 -110
  84. package/skills/slates-prompting-veo-3.md +0 -224
@@ -1,4 +1,5 @@
1
1
  import { MAX_IMAGE_VARIATIONS } from '../prompts/generation-policy.js';
2
+ import { discoverGuides, guideCatalog } from '../prompts/guide-discovery.js';
2
3
  import { retrieveGuide } from '../prompts/guide-retrieval.js';
3
4
  import { MINIMAX_MAX_REFERENCE, minimaxMaxReferenceTokens, MODEL_CAPABILITIES, GPT_QUALITY_TIERS, DEFAULT_GPT_QUALITY, GPT_BACKGROUNDS } from '../prompts/model-capabilities.js';
4
5
  // Operations layer — the ONE place every Slates agent tool is defined.
@@ -13,7 +14,7 @@ import { MINIMAX_MAX_REFERENCE, minimaxMaxReferenceTokens, MODEL_CAPABILITIES, G
13
14
  // - chooses its transport (cloud vs desktop) internally — callers
14
15
  // don't need to know which side a given op talks to
15
16
  import { z } from 'zod';
16
- import { SlatesCloudClient } from '../clients/cloud.js';
17
+ import { SlatesCloudClient, SlatesCloudHttpError } from '../clients/cloud.js';
17
18
  import { SlatesDesktopClient } from '../clients/desktop.js';
18
19
  import { BlenderBridgeClient, BLENDER_SETUP_HINT, RENDER_TIMEOUT_MS } from '../clients/blender.js';
19
20
  import { SKILLS } from '../skills/content.js';
@@ -59,10 +60,12 @@ import { SHOT_SIZE_BUCKETS, CAMERA_MOVE_BUCKETS, SPEECH_RATE, } from '../prompts
59
60
  // Annotations, tiers and the ONE schema renderer. Declared next door so this
60
61
  // module never hand-sets a hint or a tier per op: `annotate()` derives all four
61
62
  // from the id and the lockstep check re-derives them from the transport verbs.
62
- import { annotate, groupFor, tierFor, toolDefinitions, GROUP_SUMMARY, OPERATION_GROUPS, } from './surface.js';
63
+ import { annotate, groupFor, tierFor, toolDefinitions, searchTools, GROUP_SUMMARY, OPERATION_GROUPS, } from './surface.js';
64
+ // One factory for every default context, so per-connection caches can key on it.
65
+ const defaultCloud = () => new SlatesCloudClient();
63
66
  export function defaultContext() {
64
67
  return {
65
- cloud: () => new SlatesCloudClient(),
68
+ cloud: defaultCloud,
66
69
  desktop: () => new SlatesDesktopClient(),
67
70
  };
68
71
  }
@@ -127,7 +130,10 @@ export const DEVIATION_FACTOR = 1.2;
127
130
  * to find three wordings. Byte-stable (a template over a literal), so the
128
131
  * desktop's prompt-cached prefix is unaffected.
129
132
  */
130
- const CONFIRM_GATE_SENTENCE = `Cost above ${CONFIRM_CREDITS} credits returns requires_confirm — pass confirm=true after explicit user OK.`;
133
+ // The consent half rides every generation tool because a host may drop the
134
+ // server instructions: Codex CLI 0.159.1 passed none to the model (probes
135
+ // 2026-09-30 and 2026-10-02), so a small spend had no approval rule in view.
136
+ const CONFIRM_GATE_SENTENCE = `Show the user the estimate and wait for their OK before any generation, however small. Cost above ${CONFIRM_CREDITS} credits (and, on image and video, any attached reference) also returns requires_confirm — pass confirm=true only to relay that OK.`;
131
137
  // Declared HERE, above every op, because `slates_estimate_generation_cost`
132
138
  // renders them into its `duration` description at MODULE LOAD — a const
133
139
  // declared below the first schema that reads it is a temporal-dead-zone
@@ -239,9 +245,9 @@ const folderBody = (folderId) => (folderId === undefined ? {} : { folderId });
239
245
  const IMAGE_INLINE_REVIEW = 'The image is attached to this result — look at it against the brief before you describe it. ' +
240
246
  'Any claim about how it LOOKS must come from pixels you actually received (slates-vision-feedback-loop).';
241
247
  const VIDEO_REVIEW_POINTER = 'You have NOT seen this clip: call slates_get_asset_video_frames on the asset id above before ' +
242
- 'describing how it looks. A quality claim you cannot point to a tool result for is a REAL NUMBERS ONLY violation.';
248
+ 'describing visible composition or identity. Sampled stills do not establish continuous motion, lip sync or sound; use actual playback through a capable host for those, or report them unreviewed.';
243
249
  const BACKGROUND_REVIEW_POINTER = 'When it completes, look at it before you describe it — slates_get_asset_image for images, ' +
244
- 'slates_get_asset_video_frames for video. For audio, audition the saved file; metadata alone does not establish voice similarity or delivery quality.';
250
+ 'slates_get_asset_video_frames for video. For audio, audition the saved file only through a host that can receive/listen to audio; otherwise report it unreviewed. Metadata does not establish voice similarity or delivery quality.';
245
251
  // The image saved, but reading it back off disk failed (best-effort fetch). The
246
252
  // agent has an asset and NO pixels, which is the one state where a quality
247
253
  // claim would be pure invention — so this branch has to say so rather than
@@ -959,8 +965,8 @@ export const VIDEO_MODELS = [
959
965
  'kling-v3.0-std',
960
966
  'kling-v3.0-pro',
961
967
  'kling-v3.0-omni',
962
- 'veo-3.1-fast',
963
- 'veo-3.1-standard',
968
+ // Veo 3.1 Fast and Standard were retired on 2026-10-02 (Eric). The server
969
+ // keeps their keys for older desktops only; nothing here offers them.
964
970
  'seedance-2',
965
971
  // Seedance 2.5 is the DEFAULT video model (Eric, 2026-09-13): 30s takes, 30
966
972
  // image references, audio-only references, up to 1080p (2026-08-24). 2.0 stays
@@ -985,8 +991,8 @@ export const VIDEO_MODELS = [
985
991
  'minimax-h3-max',
986
992
  'minimax-h3-max-turbo',
987
993
  // LTX-2.5, two seats in one family (2026-08-29). Base is the VOLUME seat —
988
- // cheapest native 1080p second we sell, free native audio at every tier, the
989
- // only row reaching 1440p, and the longest clips in the catalogue (20s).
994
+ // cheapest 1080p second with sound included, free native audio at every tier,
995
+ // the only row reaching 1440p, and clips up to 20s.
990
996
  // Pro is the fidelity seat and is NOT a superset: shorter ladder (no
991
997
  // 1440p/4K), shorter clips (6/8/10 only) and ~1/3 dearer.
992
998
  //
@@ -1108,7 +1114,7 @@ function editClipBounds(model) {
1108
1114
  const EDIT_VIDEO_RESOLUTIONS = videoResolutionUnion(['seedance-2.5-edit']);
1109
1115
  export const estimateGenerationCost = {
1110
1116
  id: 'slates_estimate_generation_cost',
1111
- description: 'Quote credits before any generate_* op. Accepts the same base model ids and parameters as generation, or an exact registry cost key. Pairs with the confirm gate.',
1117
+ description: 'Quote credits before any generate_* op. Accepts the generation model ids and parameters (edit seats other than Kling, lip-sync and motion-transfer engines need an exact registry cost key), or an exact registry cost key. Pairs with the confirm gate.',
1112
1118
  input: z.object({
1113
1119
  model: z.string().describe('Base model id as passed to the generate op (e.g. "seedance-2", "kling-v3.0-std", "nano-banana-2") or an exact registry cost key ("nano-banana-2-2k", "seedance-2-1080p-8s")'),
1114
1120
  quantity: z.number().int().min(1).max(10).optional().describe('Number of generations (default 1)'),
@@ -1126,8 +1132,8 @@ export const estimateGenerationCost = {
1126
1132
  videoResolution: zEnum(VIDEO_RESOLUTIONS).optional().describe('Video only. Omitted, each model quotes at its own default. Per-model ladders: see slates_generate_video\'s videoResolution.'),
1127
1133
  resolution: z.enum(['1k', '2k', '3k', '4k']).optional().describe('Image only. Omit for the model default; pass the same value to generation.'),
1128
1134
  quality: z.enum(GPT_QUALITY_TIERS).optional().describe(`GPT Image tier; default ${DEFAULT_GPT_QUALITY}.`),
1129
- aspectRatio: z.string().optional().describe('Image only. 1:1/4:3/3:4 cost more than 16:9.'),
1130
- sound: z.boolean().optional().describe('Veo only — audio flag changes the cost key.'),
1135
+ aspectRatio: z.string().optional().describe('GPT Image only: 1:1, 4:3 and 3:4 cost more than 16:9.'),
1136
+ sound: z.boolean().optional().describe('Kling 3.0: the audio flag changes the cost key. Omitted means sound on, as generation bills it; pass false to price a silent take. Kling 4K keys include audio.'),
1131
1137
  seedanceFace: z.boolean().optional().describe('Seedance AI-face route (pricier key).'),
1132
1138
  seedanceRealFace: z.boolean().optional().describe('Seedance consented real-face route (premium key).'),
1133
1139
  videoRefSeconds: z.number().nonnegative().optional().describe('Combined reference-video seconds, measured from the clips.'),
@@ -1514,7 +1520,7 @@ export const getAssetsBatch = {
1514
1520
  };
1515
1521
  export const getAssetVideoFrames = {
1516
1522
  id: 'slates_get_asset_video_frames',
1517
- description: 'Extract N evenly-spaced keyframes from a video asset and return them inline as base64 JPEGs. This is the "see the video" path — LLMs can\'t consume video natively, so frames are the next best thing. Default 3 frames (start / middle / end). Bump to 5-8 for longer clips or when motion is the whole story. Use this before writing a motion-transfer prompt, a lip-sync refinement, or any iteration on a video clip. Response carries the asset\'s code (e.g. VID-V3) + label — name them when discussing the clip with the user.',
1523
+ description: 'Extract evenly-spaced sampled still frames from a video asset and return them inline as JPEGs. Inspect visible appearance, composition and identity before revising a prompt. Stills do not verify continuous motion, timing, lip sync or sound; actual playback through a capable host is needed for those claims. Default 3 samples; count can request more. The response includes the asset code and label; use them when discussing the clip.',
1518
1524
  input: z.object({
1519
1525
  id: z.string().uuid(),
1520
1526
  count: z.number().int().min(1).max(8).optional().describe('Number of frames to extract. Default 3.'),
@@ -2190,19 +2196,21 @@ const LEGACY_DESKTOP_IMAGE_BATCH = 4;
2190
2196
  export const generateImage = {
2191
2197
  id: 'slates_generate_image',
2192
2198
  billable: true,
2193
- description: 'Generate an image via Slates credits.\n' +
2194
- // GENERATED from MODEL_FACTS — the hand-typed model list that stood here
2195
- // was a third copy of the routing doctrine, and it had already gone stale
2196
- // (it still described nano-banana-2-lite by a capability the param owns).
2197
- `${describeRouting('image')}\n` +
2198
- 'Full table: the slates-model-selection skill. ' +
2199
- 'Pass projectId to save into a Slates project (asset appears live in the desktop UI). All models except nano-banana-2 REQUIRE projectId (no headless path). REQUIRED before calling: read the slates-cost-discipline skill (and the model\'s slates-prompting-* skill). You MUST pass aspectRatio and resolution explicitly (the server returns requires_clarification when missing — defaults waste credits). ' +
2199
+ description:
2200
+ // Rules first: Claude Code keeps only the first 2,048 characters of a tool
2201
+ // description, and the generated roster below runs past that cut.
2202
+ 'Generate an image via Slates credits. Pass projectId to save into a Slates project (asset appears live in the desktop UI). All models except nano-banana-2 REQUIRE projectId (no headless path). You MUST pass aspectRatio (the server returns requires_clarification when missing); resolution defaults to the model\'s own. ' +
2200
2203
  CONFIRM_GATE_SENTENCE +
2201
- ' MCP/CLI generation always charges credits. No skills installed? Call slates_get_prompting_guide with the model\'s topic and \'slates-cost-discipline\' first. ' +
2204
+ ' MCP/CLI generation always charges credits. The estimate returns the model\'s prompting card; load its slates-prompting-* guide with slates_get_prompting_guide for anything the card leaves out. ' +
2202
2205
  // GENERATED from the skill file's own never-use list -- the one piece of
2203
2206
  // prompting doctrine that is ALWAYS in context, because the agent has
2204
2207
  // demonstrably skipped the call that would have taught it.
2205
- describeBannedTokens('image'),
2208
+ describeBannedTokens('image') +
2209
+ // GENERATED from MODEL_FACTS — the hand-typed model list that stood here
2210
+ // was a third copy of the routing doctrine, and it had already gone stale
2211
+ // (it still described nano-banana-2-lite by a capability the param owns).
2212
+ `\n${describeRouting('image')}\n` +
2213
+ 'Full table: the slates-model-selection skill.',
2206
2214
  input: z.object({
2207
2215
  prompt: z.string().min(1).max(4000),
2208
2216
  model: zEnum(IMAGE_MODELS).optional().describe(`Image model. Omitted: ${DEFAULT_IMAGE_MODEL} with projectId, nano-banana-2 (the only headless seat) without. Routing: slates-model-selection skill.`),
@@ -2477,7 +2485,7 @@ export const generateImage = {
2477
2485
  };
2478
2486
  }
2479
2487
  // /proxy/generate kicks off a credit-aware job and returns a jobId
2480
- // for fal/Veo (async providers). We poll /proxy/jobs/{jobId} until
2488
+ // for fal (async providers). We poll /proxy/jobs/{jobId} until
2481
2489
  // the status is `completed` or `failed`, then fetch each image URL
2482
2490
  // and inline as base64 so the calling LLM sees the pixels.
2483
2491
  // The fal endpoint differs by mode: bare model id for text-to-image,
@@ -2590,7 +2598,7 @@ const LEGACY_EDIT_REFERENCE_MODELS = ['nano-banana-2', 'nano-banana-2-lite', 'na
2590
2598
  export const editImage = {
2591
2599
  id: 'slates_edit_image',
2592
2600
  billable: true,
2593
- description: 'Surgically edit an image asset with a text instruction (e.g. \'make the jacket red\') instead of regenerating from scratch — use when ~90% of the image is already right. The result is a NEW asset (prompt prefixed \'[Edit]\'); the source is untouched. Default model ' + toolModelFor('image-edit') + ' (the image-edit tool seat, the model the app\'s Edit box opens on). Every model also takes referenceAssetIds, up to its reference cap less one: the source is image 1 of the request. Before first use call slates_get_prompting_guide with topic \'slates-edit-and-iterate\'.',
2601
+ description: 'Surgically edit an image asset with a text instruction (e.g. \'make the jacket red\') instead of regenerating from scratch — use when ~90% of the image is already right. The result is a NEW asset (prompt prefixed \'[Edit]\'); the source is untouched. Default model ' + toolModelFor('image-edit') + ' (the image-edit tool seat, the model the app\'s Edit box opens on). Every model also takes referenceAssetIds, up to its reference cap less one: the source is image 1 of the request. Use slates-edit-and-iterate for missing edit craft; reuse current guidance already in context.',
2594
2602
  input: z.object({
2595
2603
  projectId: z.string().uuid(),
2596
2604
  sourceAssetId: z.string().uuid().describe('Image asset to edit. Must exist in the project.'),
@@ -2834,10 +2842,9 @@ export const extractGridCells = {
2834
2842
  * same teaching `requires_clarification` shape as every other gate in this op,
2835
2843
  * rather than a raw Zod error the agent has to guess its way out of.
2836
2844
  *
2837
- * `promptMode` matters: Veo's reference-to-video endpoint is 8s only, declared
2838
- * as `duration.modeOverrides.ingredients`. Free reference images with no
2839
- * first/last frame IS ingredients mode — the same condition
2840
- * `buildFalVeoRequest` uses to pick the ref2v endpoint.
2845
+ * `promptMode` matters for any row that declares `duration.modeOverrides.ingredients`
2846
+ * (retired Veo's reference-to-video endpoint was 8s only). Free reference images
2847
+ * with no first/last frame IS ingredients mode.
2841
2848
  */
2842
2849
  function assertVideoCapabilities(input) {
2843
2850
  const freeRefs = (input.ingredientAssetIds?.length ?? 0) +
@@ -2866,7 +2873,6 @@ function assertVideoCapabilities(input) {
2866
2873
  // shape (verified against /api/agent/models):
2867
2874
  // Kling: kling-v3-{standard|pro|omni}-{N}s — note the user-facing
2868
2875
  // model id `kling-v3.0-std` maps to registry key `kling-v3-standard`.
2869
- // Veo: veo-3.1-{fast|standard}[-4k]-{N}s[-audio]
2870
2876
  // Seedance: seedance-2-{res}-{N}s (BytePlus ModelArk, sole provider). The
2871
2877
  // cost key encodes resolution (480p/720p/1080p/4k) — price scales with
2872
2878
  // resolution, so the key MUST carry it or the pre-flight quote is wrong.
@@ -2932,29 +2938,21 @@ export function videoCostKey(input) {
2932
2938
  }
2933
2939
  return `${input.model}${face}-${res}-${input.duration}s`;
2934
2940
  }
2935
- if (input.model.startsWith('veo')) {
2936
- const is4k = input.videoResolution === '4k';
2937
- const audio = input.sound !== false; // default audio on for Veo
2938
- const parts = [input.model];
2939
- if (is4k)
2940
- parts.push('4k');
2941
- parts.push(`${input.duration}s`);
2942
- if (audio)
2943
- parts.push('audio');
2944
- return parts.join('-');
2945
- }
2946
2941
  if (input.model.startsWith('kling-v3.0')) {
2947
2942
  // Mirrors klingCreditKey() in slate/src/shared/pricing.ts. Kling native 4K
2948
2943
  // bills flat-rate keys: std/pro/omni all get a `-4k` tier key, and omni-pro
2949
2944
  // shares kling-v3-omni-4k (the o3/4k endpoint has one flat rate, audio
2950
2945
  // included). At 1080p AUDIO IS A KEY DIMENSION (credits = COGS × markup,
2951
- // locked 2026-07-05): sound → `-audio` variant. Kling sound defaults OFF.
2946
+ // locked 2026-07-05): sound → `-audio` variant. An OMITTED sound is ON: the
2947
+ // desktop agent route sends `sound ?? true` and bills the audio key, so a
2948
+ // quote that read omitted as silent under-quoted every default Kling take
2949
+ // (21 credits quoted, 32 billed at std 5s; found 2026-10-02).
2952
2950
  const tier = KLING_TIER_MAP[input.model] ?? input.model;
2953
2951
  if (input.videoResolution === '4k') {
2954
2952
  const tier4k = tier === 'kling-v3-omni-pro' ? 'kling-v3-omni' : tier;
2955
2953
  return `${tier4k}-4k-${input.duration}s`;
2956
2954
  }
2957
- return `${tier}-${input.duration}s${input.sound === true ? '-audio' : ''}`;
2955
+ return `${tier}-${input.duration}s${input.sound !== false ? '-audio' : ''}`;
2958
2956
  }
2959
2957
  if (input.model === 'omni-flash') {
2960
2958
  // Mirrors omniFlashCreditKey() in slate/src/shared/pricing.ts — flat 720p
@@ -3079,13 +3077,19 @@ function resolveVideoModel(raw) {
3079
3077
  // so a pasted `minimax-h3-768p-10s-ref2` resolves instead of erroring. The
3080
3078
  // number it carries is K (images PAST the free five), so it is converted back
3081
3079
  // to a TOTAL before anything can re-surcharge it.
3080
+ // The free allowance differs per row (H3 five, H3 Max four), so the total is
3081
+ // computed once the model is known, in `withRefTotal`; reading `out.model`
3082
+ // here read the placeholder and gave every row five.
3082
3083
  const ref = /-ref(\d+)\b/.exec(s);
3083
- if (ref) {
3084
- out.referenceImages =
3085
- (MINIMAX_FREE_REF_IMAGES_BY_MODEL[out.model] ?? MINIMAX_FREE_REF_IMAGES) +
3086
- parseInt(ref[1], 10);
3084
+ const paidRefs = ref ? parseInt(ref[1], 10) : null;
3085
+ if (ref)
3087
3086
  s = s.replace(/-ref(\d+)\b/, '');
3088
- }
3087
+ const withRefTotal = (resolved) => {
3088
+ if (paidRefs !== null) {
3089
+ resolved.referenceImages = (MINIMAX_FREE_REF_IMAGES_BY_MODEL[resolved.model] ?? MINIMAX_FREE_REF_IMAGES) + paidRefs;
3090
+ }
3091
+ return resolved;
3092
+ };
3089
3093
  // The RESOLUTION vocabulary is GENERATED from MODEL_CAPABILITIES — the
3090
3094
  // hand-typed list that stood here went stale the day 768p and 2k shipped.
3091
3095
  const resRe = new RegExp(`-(${VIDEO_RESOLUTION_VOCAB.join('|')})\\b`);
@@ -3111,7 +3115,7 @@ function resolveVideoModel(raw) {
3111
3115
  const direct = VIDEO_MODELS.find((m) => m === s);
3112
3116
  if (direct) {
3113
3117
  out.model = direct;
3114
- return out;
3118
+ return withRefTotal(out);
3115
3119
  }
3116
3120
  const aliases = {
3117
3121
  'kling-v3-standard': 'kling-v3.0-std',
@@ -3133,8 +3137,6 @@ function resolveVideoModel(raw) {
3133
3137
  'seedance-2-5': 'seedance-2.5',
3134
3138
  'seedance2.5': 'seedance-2.5',
3135
3139
  seedance: 'seedance-2',
3136
- 'veo-3.1': 'veo-3.1-fast',
3137
- 'veo-3': 'veo-3.1-fast',
3138
3140
  'gemini-omni-flash': 'omni-flash',
3139
3141
  'gemini-omni-flash-preview': 'omni-flash',
3140
3142
  'omni-flash-preview': 'omni-flash',
@@ -3159,7 +3161,7 @@ function resolveVideoModel(raw) {
3159
3161
  };
3160
3162
  if (aliases[s]) {
3161
3163
  out.model = aliases[s];
3162
- return out;
3164
+ return withRefTotal(out);
3163
3165
  }
3164
3166
  return null;
3165
3167
  }
@@ -3215,11 +3217,11 @@ const VIDEO_MODEL_GUIDES = [
3215
3217
  export const generateVideo = {
3216
3218
  id: 'slates_generate_video',
3217
3219
  billable: true,
3218
- description: 'Generate video via Slates credits. REQUIRED before calling: read slates-model-selection (the routing doctrine), slates-cost-discipline, and the matching per-model prompting skill (' +
3220
+ description: 'Generate video via Slates credits. Choose the model with slates-model-selection. Video models prompt very differently: the estimate returns the chosen model\'s prompting card, and its full guide (' +
3219
3221
  VIDEO_MODEL_GUIDES +
3220
- ') — video models prompt very differently; load them via slates_get_prompting_guide if no skill files are installed. Read slates-content-policy when the scene involves conflict, creatures, crowds, destruction, weapons, or young characters. projectId, aspectRatio, and duration are required (requires_clarification otherwise). ' +
3222
+ ') covers modes the card leaves out, via slates_get_prompting_guide. Read slates-content-policy when the scene involves conflict, creatures, crowds, destruction, weapons, or young characters. projectId, aspectRatio, and duration are required (requires_clarification otherwise). ' +
3221
3223
  CONFIRM_GATE_SENTENCE +
3222
- ' Image-to-video via firstFrameAssetId; first+last frames = Veo/Seedance only; ingredients via ingredientAssetIds (Kling Omni / Seedance). Asset params take UUIDs or badge codes ("IMG-A8"). ' +
3224
+ ' Image-to-video via firstFrameAssetId; first+last frames on every model except Omni Flash; ingredients via ingredientAssetIds (Kling Omni, Seedance, Omni Flash, H3 and H3 Max; Kling Std/Pro only with a first frame). Asset params take UUIDs or badge codes ("IMG-A8"). ' +
3223
3225
  // GENERATED from the skill's own slop-token list. Always in context on both
3224
3226
  // surfaces, so it survives an agent that skips slates_get_prompting_guide.
3225
3227
  describeBannedTokens('video'),
@@ -3239,7 +3241,7 @@ export const generateVideo = {
3239
3241
  `Full table: the slates-model-selection skill. Each model's legal aspect ratios, durations and ` +
3240
3242
  `resolutions are in those params' own descriptions — read them there, not from memory. ` +
3241
3243
  `For per-call cost, call slates_estimate_generation_cost.`),
3242
- projectId: z.string().uuid().optional().describe('Save into this Slates project. Strongly recommended — the desktop UI shows a progress card live and the asset appears when complete.'),
3244
+ projectId: z.string().uuid().optional().describe('Save into this Slates project. Required — the desktop UI shows a progress card live and the asset appears when complete.'),
3243
3245
  // 🚨 THESE THREE DESCRIPTIONS ARE GENERATED FROM `MODEL_CAPABILITIES`.
3244
3246
  // Never hand-write a ratio, resolution or duration into them again — every
3245
3247
  // one of the hand-written claims that stood here had drifted, and an
@@ -3256,7 +3258,7 @@ export const generateVideo = {
3256
3258
  // demand, by the one session that needs it. If you are about to explain
3257
3259
  // WHY here, you are writing the skill in the wrong file.
3258
3260
  firstFrameAssetId: z.string().optional().describe('Starting frame for image-to-video (UUID or badge code, resolved at call time).'),
3259
- lastFrameAssetId: z.string().optional().describe('Ending frame. Veo and Seedance only; pairs with firstFrameAssetId.'),
3261
+ lastFrameAssetId: z.string().optional().describe('Ending frame; every model except Omni Flash. Pairs with firstFrameAssetId.'),
3260
3262
  ingredientAssetIds: z.array(z.string()).max(30).optional().describe(
3261
3263
  // Caps DERIVED from MODEL_CAPABILITIES — the hand-typed list omitted
3262
3264
  // kling-v3.0-omni-pro entirely and read as if 7 were an Omni Flash-only rule.
@@ -3271,15 +3273,15 @@ export const generateVideo = {
3271
3273
  // The capacity sentences are DERIVED from MODEL_FACTS (see
3272
3274
  // multimodalRefSummary) rather than hand-typed, so a cap change in one
3273
3275
  // place cannot leave a stale number in a description an LLM reads.
3274
- videoReferenceAssetIds: z.array(z.string()).optional().describe(`Reference VIDEOS, cited in the prompt as "video 1", "video 2"… in the order given. ${multimodalRefModels().join(' / ')} only; per-model caps are on audioReferenceAssetIds. Billing switches to the vref key (input+output seconds) — pass videoReferenceSecondsEach. Over the cap is REFUSED, never trimmed.`),
3276
+ videoReferenceAssetIds: z.array(z.string()).optional().describe(`Reference VIDEOS, cited in the prompt as "video 1", "video 2"… in the order given. ${multimodalRefModels().join(' / ')} only; per-model caps are on audioReferenceAssetIds. On Seedance, billing switches to the vref key (input+output seconds) — pass videoReferenceSecondsEach. Over the cap is REFUSED, never trimmed.`),
3275
3277
  videoReferenceSecondsEach: z.array(z.number()).optional().describe('REQUIRED with videoReferenceAssetIds, same order and length: each clip\'s duration in seconds. Feeds the vref cost key; the server re-probes and corrects an understated value upward.'),
3276
- audioReferenceAssetIds: z.array(z.string()).optional().describe(`Reference AUDIO, cited as "audio 1", "audio 2"… in the order given. ${multimodalRefModels().join(' / ')} only. No billing surcharge. ${multimodalRefModels().map(multimodalRefSummary).join(' ')}`),
3278
+ audioReferenceAssetIds: z.array(z.string()).optional().describe(`Reference AUDIO, cited as "audio 1", "audio 2"… in the order given. ${multimodalRefModels().join(' / ')} only. No surcharge on Seedance; on H3 Max, audio counts toward the reference-token pool. ${multimodalRefModels().map(multimodalRefSummary).join(' ')}`),
3277
3279
  audioReferenceSpokenText: z.array(z.string()).optional().describe('The exact words in each reference clip — same order and length as audioReferenceAssetIds, "" for a clip with no speech. The model RE-TRANSCRIBES a take rather than using it verbatim, so audio decides voice/accent/timing and only this decides the WORDS. Omit it and the words are a guess.'),
3278
- sound: z.boolean().optional().describe('Kling Omni / Veo / Seedance: Sound on (true) or Silent (false). Default true.'),
3279
- audioLanguage: z.enum(['EN', 'ZH', 'JA', 'KO', 'ES']).optional().describe('Kling Omni only — language for dialogue.'),
3280
- generateMusic: z.boolean().optional().describe('Kling Omni only — auto-generate background music.'),
3280
+ sound: z.boolean().optional().describe('Kling (every tier) and LTX: sound on (default) or silent (false); Kling bills audio as its own key. Seedance, Omni Flash and H3 always generate audio.'),
3281
+ audioLanguage: z.enum(['EN', 'ZH', 'JA', 'KO', 'ES']).optional().describe('Any Kling model with sound on — language for dialogue.'),
3282
+ generateMusic: z.boolean().optional().describe('Any Kling model with sound on — auto-generate background music.'),
3281
3283
  seedanceFace: z.boolean().optional().describe('Seedance ONLY: a reference shows an AI CHARACTER\'s face. Faces are blocked on the default route, so this reroutes to a face-capable provider at ~45% more. A REAL person fails here with [REAL_FACE_DETECTED] — see seedanceRealFace.'),
3282
- seedanceRealFace: z.boolean().optional().describe('Seedance ONLY: a reference shows a REAL person. Real-person route, roughly 2x the AI-face price — quote it first. REQUIRES realFaceConsent=true.'),
3284
+ seedanceRealFace: z.boolean().optional().describe('Seedance ONLY: a reference shows a REAL person. Real-person route, about 1.4x the AI-face price (about 2x faceless) — quote it first. REQUIRES realFaceConsent=true.'),
3283
3285
  realFaceConsent: z.boolean().optional().describe('MANDATORY with seedanceRealFace: true ONLY after the user has explicitly confirmed they hold rights/consent to this likeness and it does not impersonate or misrepresent them. Refused without it; public figures fail on every route.'),
3284
3286
  negativePrompt: z.string().optional(),
3285
3287
  background: z.boolean().optional().describe(BACKGROUND_DESCRIBE),
@@ -3369,7 +3371,7 @@ export const generateVideo = {
3369
3371
  return ok({
3370
3372
  requires_clarification: true,
3371
3373
  missing: [],
3372
- message: `Omni Flash takes a prompt, an optional start frame, and up to ${omniFlashRefCap} reference IMAGES — last frames and video/audio references are not supported. Drop those, or switch to seedance-2 (video/audio refs, last frame) or veo (last frame).`,
3374
+ message: `Omni Flash takes a prompt, an optional start frame, and up to ${omniFlashRefCap} reference IMAGES — last frames and video/audio references are not supported. Drop those, or switch to seedance-2.5 (video/audio refs, last frame).`,
3373
3375
  });
3374
3376
  }
3375
3377
  const refCount = (input.ingredientAssetIds?.length ?? 0) +
@@ -3847,10 +3849,9 @@ export const generateAudio = {
3847
3849
  id: 'slates_generate_audio',
3848
3850
  billable: true,
3849
3851
  description: `Generate project audio using credits. Choose the surface via the model routing below. ` +
3850
- 'Read slates-cost-discipline and the matching prompting skill first (slates-prompting-seed-audio | slates-prompting-elevenlabs | slates-prompting-inworld-tts). ' +
3852
+ 'The estimate returns the chosen surface\'s prompting card; its full guide (slates-prompting-seed-audio | slates-prompting-elevenlabs | slates-prompting-inworld-tts) covers what the card leaves out. ' +
3851
3853
  'Seed Audio bills the requested duration, which is appended to the prompt regardless of output length. Kling "SFX:" / "Ambient noise:" syntax does not transfer. ' +
3852
- CONFIRM_GATE_SENTENCE +
3853
- ' No skill files installed? Call slates_get_prompting_guide first.',
3854
+ CONFIRM_GATE_SENTENCE,
3854
3855
  input: z.object({
3855
3856
  projectId: z.string().uuid().describe('Slates project the audio asset lands in. Required — the renderer refreshes live.'),
3856
3857
  model: z
@@ -4102,7 +4103,8 @@ async function quoteToolBlocks(ctx, stem, seconds, voiceStep) {
4102
4103
  export const generateLipSync = {
4103
4104
  id: 'slates_generate_lip_sync',
4104
4105
  billable: true,
4105
- description: 'Lip-sync a still image (avatar) or a video clip to audio. KLING-ONLY — this tool wraps Kling\'s dedicated lip-sync endpoints and nothing else: sourceType=video re-syncs a clip, sourceType=image animates a still avatar (avatar-standard, or avatar-pro for the premium seat). Quote each with slates_estimate_generation_cost rather than from memory. Audio from TTS (ttsText + ttsVoice) or an uploaded file. Billed per 5s of the output (the clip, or on a still the voice track). For a Seedance version, do NOT look for an engine switch here — run a normal slates_generate_video on seedance-2 with the clip attached as a video reference and the dialogue written into the prompt; that is the same call, with the prompt visible and editable. REQUIRED before calling: slates-cost-discipline + slates-prompting-lip-sync skills. projectId is REQUIRED.',
4106
+ description: 'Lip-sync a still image (avatar) or a video clip to audio. KLING-ONLY — this tool wraps Kling\'s dedicated lip-sync endpoints and nothing else: sourceType=video re-syncs a clip, sourceType=image animates a still avatar (avatar-standard, or avatar-pro for the premium seat). Quote each with slates_estimate_generation_cost rather than from memory. Audio from TTS (ttsText + ttsVoice) or an uploaded file. Billed per 5s of the output (the clip, or on a still the voice track). For a Seedance version, do NOT look for an engine switch here — run a normal slates_generate_video on seedance-2 with the clip attached as a video reference and the dialogue written into the prompt; that is the same call, with the prompt visible and editable. The craft is slates-prompting-lip-sync; the confirm response shows the exact cost before anything is charged. projectId is REQUIRED. ' +
4107
+ CONFIRM_GATE_SENTENCE,
4106
4108
  input: z.object({
4107
4109
  projectId: z.string().uuid().describe('Slates project the source asset lives in. The new lip-synced video lands here.'),
4108
4110
  sourceAssetId: z.string().uuid().describe('Asset id of the still image (avatar flow) or video clip (lip-sync flow). Must already exist in the project — use slates_upload_reference_image or slates_generate_image / slates_generate_video first if needed.'),
@@ -4225,11 +4227,11 @@ export const generateLipSync = {
4225
4227
  export const generateMotionTransfer = {
4226
4228
  id: 'slates_generate_motion_transfer',
4227
4229
  billable: true,
4228
- description: 'Transfer the motion from a reference video onto a target image character. KLING-ONLY — this tool wraps Kling Motion Control and nothing else: kling-mc-std or kling-mc-pro, structured skeleton/depth retargeting, billed per 5s of the driving clip. For a Seedance version, do NOT look for an engine switch here — run a normal slates_generate_video on seedance-2 with the driving clip attached as a video reference and the motion described in the prompt ("the character from image 1 performs the exact motion from video 1"); that is the same call, with the prompt visible and editable. REQUIRED before calling: slates-cost-discipline + slates-prompting-motion-transfer skills. projectId is REQUIRED — both assets must exist in the project. ' +
4230
+ description: 'Transfer the motion from a reference video onto a target image character. KLING-ONLY — this tool wraps Kling Motion Control and nothing else: kling-mc-std or kling-mc-pro, structured skeleton/depth retargeting, billed per 5s of the driving clip. For a Seedance version, do NOT look for an engine switch here — run a normal slates_generate_video on seedance-2 with the driving clip attached as a video reference and the motion described in the prompt ("the character from image 1 performs the exact motion from video 1"); that is the same call, with the prompt visible and editable. The craft is slates-prompting-motion-transfer; the confirm response shows the exact cost before anything is charged. projectId is REQUIRED — both assets must exist in the project. ' +
4229
4231
  CONFIRM_GATE_SENTENCE,
4230
4232
  input: z.object({
4231
4233
  projectId: z.string().uuid().describe('Slates project. Both source and target assets must live here.'),
4232
- sourceVideoAssetId: z.string().uuid().describe('Asset id of the reference video — its motion will be retargeted onto the target image. Must already exist in the project. Up to 30s.'),
4234
+ sourceVideoAssetId: z.string().uuid().describe('Asset id of the reference video — its motion will be retargeted onto the target image. Must already exist in the project. Up to 30s with characterOrientation video, 10s with image; billed per 5s block.'),
4233
4235
  targetImageAssetId: z.string().uuid().describe('Asset id of the target image (the character that will perform the motion). Must already exist in the project.'),
4234
4236
  motionModel: z.enum(['kling-mc-std', 'kling-mc-pro']).optional().describe('kling-mc-std general motion; kling-mc-pro cleaner anatomy — default. Quote both with slates_estimate_generation_cost.'),
4235
4237
  characterOrientation: z.enum(['video', 'image']).optional().describe('"video" = use the source video\'s framing. "image" = use the target image\'s framing. Default video.'),
@@ -4336,7 +4338,7 @@ export const editVideo = {
4336
4338
  // point to in a tool result) and go stale on the next rate change; the
4337
4339
  // windows are owned by MODEL_CAPABILITIES and are generated below into the
4338
4340
  // params that enforce them.
4339
- 'Edit an EXISTING video clip with one instruction — character swap, environment change, style transfer — in one pass, no masking. Original motion, camera, and audio are preserved; only what the prompt names changes. Use when a clip is ~90% right (fix it, don\'t re-roll it) or to AI-edit the user\'s own footage. Engines: Kling O3 edit (default — subject/style refs via elements), omni-flash-edit (PROMPT-ONLY, no refs, the cheapest seat), or seedance-2.5-edit (the only engine that takes a clip over 15s; seedanceFace:true for AI-character faces). Clip-length and resolution windows per engine are on the `model` param. Cost is per second of OUTPUT (≈ clip length, rounded up), and an edit bills input + output seconds on every provider — read the quote from the confirm gate or slates_estimate_generation_cost, never from memory. Subjects to swap IN go as characterAssetIds (frontal + angle images become Kling elements — Kling models only); style refs as styleAssetIds; max 4 combined. The edited clip saves as a NEW asset linked to its parent (chain edits freely). Routing: Kling edit is the default (element lock + audio intact); omni-flash-edit for cheap prompt-only footage-synced swaps; Seedance edit/relocate for style-transfer-heavy jobs — see slates-model-selection. Prompting: slates-prompting-kling-v3 §Edit / slates-prompting-omni-flash.',
4341
+ 'Edit an EXISTING video clip with one instruction — character swap, environment change, style transfer — in one pass, no masking. Original motion, camera, and audio are preserved; only what the prompt names changes. Use when a clip is ~90% right (fix it, don\'t re-roll it) or to AI-edit the user\'s own footage. Engines: Kling O3 edit (default — subject/style refs via elements), omni-flash-edit (PROMPT-ONLY, no refs, priced level with Kling O3 Edit Standard; the first pick in the routing guide for footage-synced edits), or seedance-2.5-edit (the only engine that takes a clip over 15s; seedanceFace:true for AI-character faces). Clip-length and resolution windows per engine are on the `model` param. Cost is per second of OUTPUT (≈ clip length, rounded up), and an edit bills input + output seconds on Seedance and output seconds on Kling and Omni Flash — read the quote from the confirm gate or slates_estimate_generation_cost, never from memory. Subjects to swap IN go as characterAssetIds (frontal + angle images become Kling elements — Kling models only); style refs as styleAssetIds; max 4 combined. The edited clip saves as a NEW asset linked to its parent (chain edits freely). Routing: Kling edit is the default (element lock + audio intact); omni-flash-edit for cheap prompt-only footage-synced swaps; Seedance edit for style-transfer-heavy jobs — see slates-model-selection. Prompting: slates-prompting-kling-v3 §Edit / slates-prompting-omni-flash.',
4340
4342
  input: z.object({
4341
4343
  projectId: z.string().uuid().describe('Project the source clip lives in.'),
4342
4344
  sourceVideoAssetId: z.string().describe('The VIDEO asset to edit — UUID or badge code ("VID-V3", bare "V3"); codes resolve against the project at call time. Kling: 3–15s clips; omni-flash-edit: 3–10s.'),
@@ -4353,7 +4355,7 @@ export const editVideo = {
4353
4355
  styleAssetIds: z.array(z.string()).max(4).optional().describe('Style/appearance reference images (@ImageN). Max 4 combined with characterAssetIds. KLING MODELS ONLY — rejected on omni-flash-edit.'),
4354
4356
  keepAudio: z.boolean().optional().describe('Preserve the original audio track (default true; Kling models only — omni-flash-edit and seedance-2.5-edit output carry their own audio).'),
4355
4357
  videoResolution: zEnum(EDIT_VIDEO_RESOLUTIONS).optional().describe(`seedance-2.5-edit ONLY (default ${defaultVideoResolutionFor('seedance-2.5-edit')}). Ignored by the Kling and Omni Flash engines, whose output follows the source clip.`),
4356
- seedanceFace: z.boolean().optional().describe('seedance-2.5-edit ONLY: set true when a CHARACTER FACE is visible in the clip. Faceless edits run on BytePlus; faces are blocked there and must route to the relaxed provider, which costs ~35% more. A face edit submitted without this flag is rejected by the provider, not silently downgraded.'),
4358
+ seedanceFace: z.boolean().optional().describe('seedance-2.5-edit ONLY: set true when a CHARACTER FACE is visible in the clip. Faceless edits run on BytePlus; faces are blocked there and must route to the relaxed provider, which costs about 40-50% more. A face edit submitted without this flag is rejected by the provider, not silently downgraded.'),
4357
4359
  background: z.boolean().optional().describe(BACKGROUND_DESCRIBE),
4358
4360
  confirm: z.boolean().optional().describe('Set true to bypass the cost confirm gate after the user OKs the spend.'),
4359
4361
  folderId: folderIdField,
@@ -4503,7 +4505,7 @@ export const editVideo = {
4503
4505
  // ── Trim a video to an exact window (fit-to-model primitive) ────
4504
4506
  export const trimVideo = {
4505
4507
  id: 'slates_trim_video',
4506
- description: 'Trim a video asset to an exact [inSec, outSec] window and save the result as a NEW clip linked to the original (the original is untouched). This is the fit-to-model primitive: an 11s clip will not run on omni-flash-edit (3–10s) or Kling edit (3–15s), and a Seedance video reference must be 2–15s — trim it first, then edit/relocate the trimmed clip. EXACT re-encode (not a keyframe-snapped cut) so the result honors hard duration caps to the frame; any phone rotation flag is baked into the pixels in the same pass. The new clip lands with correct duration/width/height immediately. inSec defaults to 0. Pass pieces to cut the window into SEVERAL clips in one call, as the Trim & split dialog does (split points, or Auto-split by longest piece, with seconds shared between neighbours); every new clip comes back.',
4508
+ description: 'Trim a video asset to an exact [inSec, outSec] window and save the result as a NEW clip linked to the original (the original is untouched). This is the fit-to-model primitive: an 11s clip will not run on omni-flash-edit (3–10s), a 16s one not on Kling edit (3–15s), and a Seedance 2.0 video reference must be 2–15s (2.5 takes up to 30s combined) — trim it first, then edit the trimmed clip. EXACT re-encode (not a keyframe-snapped cut) so the result honors hard duration caps to the frame; any phone rotation flag is baked into the pixels in the same pass. The new clip lands with correct duration/width/height immediately. inSec defaults to 0. Pass pieces to cut the window into SEVERAL clips in one call, as the Trim & split dialog does (split points, or Auto-split by longest piece, with seconds shared between neighbours); every new clip comes back.',
4507
4509
  input: z.object({
4508
4510
  projectId: z.string().uuid().describe('Project the clip lives in.'),
4509
4511
  assetId: z
@@ -4859,7 +4861,7 @@ export const exportVideo = {
4859
4861
  };
4860
4862
  export const exportTimelineXml = {
4861
4863
  id: 'slates_export_timeline_xml',
4862
- description: "Export the project's timeline as FCP7/XMEML XML — the file DaVinci Resolve imports directly (File → Import → Timeline) to recreate the edit with references to the original clip media on disk. This is the 'Export for DaVinci, Premiere or Final Cut' path. Pass an absolute outputPath ending in .xml; fails if the file exists unless overwrite=true.",
4864
+ description: "Export the project's timeline as FCP7/XMEML XML — the file DaVinci Resolve imports directly (File → Import → Timeline) to recreate the edit with references to the original clip media on disk. Use this for DaVinci Resolve or Premiere; current Final Cut Pro requires FCPXML, which this tool does not export. Pass an absolute outputPath ending in .xml; fails if the file exists unless overwrite=true.",
4863
4865
  input: z
4864
4866
  .object({
4865
4867
  projectId: z.string().uuid().optional(),
@@ -6576,7 +6578,7 @@ export const editCut = {
6576
6578
  };
6577
6579
  export function resolveGuideTopic(topic) {
6578
6580
  const t = topic.trim().toLowerCase();
6579
- if (SKILLS[t])
6581
+ if (Object.hasOwn(SKILLS, t))
6580
6582
  return t;
6581
6583
  if (t === 'slates-character-turnaround' || t === 'character-turnaround') {
6582
6584
  return 'slates-character-identity';
@@ -6650,8 +6652,6 @@ export function resolveGuideTopic(topic) {
6650
6652
  return 'slates-prompting-flux-2-max';
6651
6653
  if (t.startsWith('seedream'))
6652
6654
  return 'slates-prompting-seedream-5-lite';
6653
- if (t.startsWith('veo'))
6654
- return 'slates-prompting-veo-3';
6655
6655
  if (t.startsWith('omni-flash') || t.startsWith('gemini-omni') || t === 'omni flash')
6656
6656
  return 'slates-prompting-omni-flash';
6657
6657
  // MiniMax H3 — both seats share one skill. Placed BEFORE the seed/seedance
@@ -6672,7 +6672,7 @@ export function resolveGuideTopic(topic) {
6672
6672
  if (t.startsWith('kling-mc'))
6673
6673
  return 'slates-prompting-motion-transfer';
6674
6674
  if (t === 'edit-video' || t === 'video-edit' || t === 'edit video' || t === 'video edit')
6675
- return 'slates-prompting-kling-v3';
6675
+ return resolveGuideTopic(defaultModelFor('video', 'edit'));
6676
6676
  if (t.startsWith('kling-v3'))
6677
6677
  return 'slates-prompting-kling-v3';
6678
6678
  // Audio — the TTS seat FIRST, then seed-audio, then eleven-sfx.
@@ -6733,69 +6733,96 @@ export function resolveGuideTopic(topic) {
6733
6733
  }
6734
6734
  return null;
6735
6735
  }
6736
- /**
6737
- * The guide index, GENERATED from SKILLS.
6738
- *
6739
- * The list here was hand-typed and had drifted to 25 of 32 names — the prompting
6740
- * guides for GPT Image, MiniMax H3, LTX-2.5, Seedance 2.5 and Omni Flash were
6741
- * all missing, so an agent reading this description could not learn they exist.
6742
- * A hand-typed index of a generated corpus is a stale index; it is only a matter
6743
- * of when.
6744
- *
6745
- * Per-model guides are listed as BARE NAMES: the name is the description, and
6746
- * `resolveGuideTopic()` resolves a model id to the right one anyway.
6747
- */
6748
- function describeGuideTopics() {
6749
- const names = Object.keys(SKILLS).sort();
6750
- const perModel = names.filter((n) => n.startsWith('slates-prompting-'));
6751
- const rest = names.filter((n) => !n.startsWith('slates-prompting-'));
6752
- return (`Workflow and cross-cutting guides: ${rest.join(', ')}. ` +
6753
- `Per-model prompting guides (or just pass the model id, which resolves to one of these): ` +
6754
- `${perModel.join(', ')}. ` +
6755
- `Style names (photoreal, anime, painterly, 3d-render) resolve to slates-style-prompting.`);
6736
+ // Discovery is the first call of most briefs: a slow or unentitled member check
6737
+ // must not cost every call a round trip, or hang free craft behind a 30s read.
6738
+ const MEMBER_CATALOG_TTL_MS = 60_000;
6739
+ const MEMBER_CATALOG_TIMEOUT_MS = 5_000;
6740
+ const memberCatalogCache = new WeakMap();
6741
+ /** Private bodies stay on the entitled member feed, never in public packages. */
6742
+ function memberGuideCatalog(ctx) {
6743
+ const cached = memberCatalogCache.get(ctx.cloud);
6744
+ if (cached && Date.now() - cached.at < MEMBER_CATALOG_TTL_MS)
6745
+ return cached.value;
6746
+ const value = fetchMemberGuideCatalog(ctx);
6747
+ memberCatalogCache.set(ctx.cloud, { at: Date.now(), value });
6748
+ return value;
6749
+ }
6750
+ async function fetchMemberGuideCatalog(ctx) {
6751
+ try {
6752
+ // Request first: a missing token throws here, before any timer exists.
6753
+ const request = ctx.cloud().get('/members/manifest.json');
6754
+ let timer;
6755
+ const timeout = new Promise((_, reject) => { timer = setTimeout(() => reject(new Error('Member guide check timed out')), MEMBER_CATALOG_TIMEOUT_MS); });
6756
+ const manifest = await Promise.race([request, timeout]).finally(() => clearTimeout(timer));
6757
+ if (!Array.isArray(manifest?.skills))
6758
+ throw new Error('Invalid member guide manifest');
6759
+ // A malformed entry is skipped; it never hides the account's other playbooks.
6760
+ const entries = manifest.skills
6761
+ .filter(entry => entry.tier === 'paid' && typeof entry.name === 'string' && /^slates-[a-z0-9]+(?:-[a-z0-9]+)*$/.test(entry.name) && entry.name.length <= 64 && typeof entry.description === 'string' && entry.description.length <= 1024)
6762
+ .map(entry => ({ name: entry.name, description: entry.description, tier: 'paid' }));
6763
+ return { entries, access: 'available' };
6764
+ }
6765
+ catch (error) {
6766
+ if (error.code === 'CLOUD_TOKEN_MISSING')
6767
+ return { entries: [], access: 'not connected; bundled guides available' };
6768
+ if (error instanceof SlatesCloudHttpError && error.status === 402)
6769
+ return { entries: [], access: 'no active skills entitlement; bundled guides available' };
6770
+ if (error instanceof SlatesCloudHttpError && error.status === 404)
6771
+ return { entries: [], access: 'member feed unavailable on this API version; bundled guides available' };
6772
+ // Keep free craft usable during a cloud outage, but expose the failed access check.
6773
+ return { entries: [], access: error instanceof SlatesCloudHttpError && error.status === 401 ? 'reconnect Slates to access member guides; bundled guides available' : 'member access check failed; retry for private guides; bundled guides available' };
6774
+ }
6756
6775
  }
6757
6776
  export const getPromptingGuide = {
6758
6777
  id: 'slates_get_prompting_guide',
6759
- description: 'For app help and exact UI instructions use topic "app-manual" with the words of the question (query "export an mp4", "where is the timeline"): it returns the ONE best section of the canonical product manual and the headings of related ones to ask for next. No query returns the map of surfaces; a surface asked for by name (query "THE TIMELINE (CUT) AND EXPORT") returns its opening and its section names. depth "full" is the whole manual and large; avoid it. ' +
6760
- // 🚨 NO "ALWAYS READ THIS FIRST" SENTENCE. It stood here for months and was
6761
- // MEASURED at 13% compliance before and after the enforcement work — pointer
6762
- // prose is the shape that does not move the agent. What replaced it is
6763
- // structural: the never-use list rides the generate ops' descriptions and
6764
- // the craft card rides the estimate result, so the facts arrive whether or
6765
- // not this op is ever called.
6766
- "Return a bundled Slates prompting/workflow guide. MCP-only clients (Claude Desktop, Smithery) don't get the CLI-installed skill files — call this instead. Accepts a guide name or a model id ('veo-3.1-fast', 'kling-v3.0-pro', 'seedance-2', 'nano-banana-2'), which maps to the right guide. Reach for it when a card is not enough: the failure modes, the worked examples and the sources are only in the full text.",
6778
+ description: 'Find production craft from the user vision: pass query alone with the brief (for example "two friends talking in a rainy diner") for keyword-ranked guides with matching sections, followed by every other guide\'s description so you choose by judgment. Omit topic and query, or use topic "catalog", for the whole catalog. Then request a guide name, model id or style name with card/index/section/full depth. Cards are short; query with a topic selects one section or cinematic technique; full returns worked examples, failure modes and sources. Reuse current guidance already in context. Entitled member playbooks are fetched privately through the connected Slates account. For exact buttons and UI paths use topic "app-manual" with relevant question keywords; no query returns its surface map. Users supply the vision; you find the guides.',
6767
6779
  input: z.object({
6768
- query: z.string().max(200).optional().describe('Keywords, section heading, or exact cinematic technique ID. Returns only the matching section or technique.'),
6769
- topic: z
6770
- .string()
6771
- .min(1)
6772
- .describe(`Guide name, model id, or style name. ${describeGuideTopics()}`),
6773
- depth: z.enum(['card', 'index', 'section', 'full']).optional().describe('Default "card" is a short overview. "index" lists sections; query selects one section or technique; "full" explicitly returns the complete guide.'),
6780
+ query: z.string().min(1).max(1000).optional().describe('Without topic: creative brief or craft need to discover guides. With topic: section keywords, heading or technique ID.'),
6781
+ topic: z.string().min(1).optional().describe('Optional guide name, model id, style name, catalog, or app-manual. Omit to search by intent or browse.'),
6782
+ depth: z.enum(['card', 'index', 'section', 'full']).optional().describe('Default card is a short overview; index lists sections; query selects a section; full returns the complete guide.'),
6783
+ limit: z.number().int().min(1).max(20).optional().describe('Page size: a search returns 8 ranked matches by default, browsing the whole catalog. Does not change guide bodies.'),
6784
+ offset: z.number().int().min(0).optional().describe('Catalog/search offset from nextOffset in the preceding result.'),
6774
6785
  }),
6775
- async run(input) {
6776
- if (input.topic.trim().toLowerCase() === 'app-manual') {
6777
- const content = input.query || input.depth === 'full'
6778
- ? appManualSections(input.query)
6779
- : appManualIndex();
6786
+ async run(input, ctx) {
6787
+ const topic = input.topic?.trim().toLowerCase();
6788
+ if (topic === 'app-manual') {
6789
+ const content = input.query || input.depth === 'full' ? appManualSections(input.query) : appManualIndex();
6780
6790
  return { text: content, data: { topic: 'app-manual', bytes: Buffer.byteLength(content, 'utf8'), guide: content } };
6781
6791
  }
6782
- const resolved = resolveGuideTopic(input.topic);
6783
- const content = resolved ? SKILLS[resolved] : undefined;
6784
- if (!resolved || content === undefined) {
6785
- throw new Error(`Unknown guide topic: ${input.topic}. Valid topics: ${Object.keys(SKILLS).sort().join(', ')}`);
6792
+ const resolved = topic ? resolveGuideTopic(topic) : null;
6793
+ if (resolved) {
6794
+ const depth = input.depth ?? 'card';
6795
+ const guide = retrieveGuide(resolved, SKILLS[resolved], depth, input.query);
6796
+ return { text: guide, data: { topic: resolved, tier: 'free', depth, bytes: Buffer.byteLength(guide, 'utf8'), guide } };
6797
+ }
6798
+ const member = await memberGuideCatalog(ctx);
6799
+ const privateEntry = member.entries.find(entry => entry.name === topic);
6800
+ if (privateEntry) {
6801
+ // The feed's 402/404 bodies are purchase pages; the agent needs the fact, not the page.
6802
+ const result = await ctx.cloud().get(`/members/skills/${encodeURIComponent(privateEntry.name)}.md?format=json`).catch((error) => {
6803
+ if (error instanceof SlatesCloudHttpError && error.status === 402)
6804
+ throw new SlatesCloudHttpError(`${privateEntry.name} needs an active skills entitlement; bundled guides remain available.`, 402);
6805
+ if (error instanceof SlatesCloudHttpError && error.status === 404)
6806
+ throw new SlatesCloudHttpError(`${privateEntry.name} is no longer in the member feed.`, 404);
6807
+ throw error;
6808
+ });
6809
+ if (typeof result?.markdown !== 'string')
6810
+ throw new Error('Invalid member guide body');
6811
+ const depth = input.depth ?? 'card';
6812
+ const guide = retrieveGuide(privateEntry.name, result.markdown, depth, input.query);
6813
+ return { text: guide, data: { topic: privateEntry.name, tier: 'paid', depth, bytes: Buffer.byteLength(guide, 'utf8'), guide } };
6786
6814
  }
6787
- const depth = input.depth ?? 'card';
6788
- const guide = retrieveGuide(resolved, content, depth, input.query);
6789
- // Both fields carry the body: some native clients expose structured data only.
6790
- return { text: guide, data: { topic: resolved, depth, bytes: Buffer.byteLength(guide, 'utf8'), guide } };
6815
+ const catalog = [...guideCatalog(SKILLS), ...member.entries].sort((a, b) => a.name.localeCompare(b.name));
6816
+ const result = discoverGuides(catalog, SKILLS, input.query ?? (topic && topic !== 'catalog' && topic !== 'index' ? input.topic : undefined), input.limit, input.offset);
6817
+ const guide = result.guides.map(entry => `${entry.name} (${entry.tier}): ${entry.description}${entry.sections.length ? `\n Matching sections: ${entry.sections.join('; ')}` : ''}`).join('\n\n');
6818
+ const rest = result.rest.length ? `\n\nEvery other guide (keyword hints above only see shared words; choose by the brief):\n${result.rest.map(entry => `- ${entry.name} (${entry.tier}): ${entry.description}`).join('\n')}` : '';
6819
+ const text = `${result.fallback ? 'No keyword match; choose relevant craft from the catalog.\n\n' : ''}${guide}${rest}\n\n${result.total} ${result.query && !result.fallback ? 'keyword matches' : 'guides'}; nextOffset: ${result.nextOffset ?? 'none'}. Member guides: ${member.access}. Retrieve a name with depth card/index or query for a section. The user does not need to choose guides.`;
6820
+ return { text, data: { ...result, memberAccess: member.access, bytes: Buffer.byteLength(text, 'utf8') } };
6791
6821
  },
6792
6822
  };
6793
6823
  /**
6794
- * The one op that changes what OTHER ops are visible.
6795
- *
6796
- * 🚨 IT EXISTS BECAUSE THE SURFACE IS 112 KB AND EVERY TURN PAYS FOR ALL OF IT.
6797
- * Both surfaces start with the shared core set. Search returns compact metadata;
6798
- * names/group returns exact schemas and replaces the optional selection.
6824
+ * Task discovery and exact schemas share one operation. The desktop uses
6825
+ * names/group to replace its optional selection; MCP keeps its full list fixed.
6799
6826
  */
6800
6827
  export const loadTools = {
6801
6828
  id: 'slates_load_tools',
@@ -6810,9 +6837,7 @@ export const loadTools = {
6810
6837
  }).refine((v) => [v.group, v.query, v.names].filter(Boolean).length === 1, 'Pass exactly one of query, names, or group.'),
6811
6838
  async run(input) {
6812
6839
  if (input.query) {
6813
- const words = input.query.toLowerCase().split(/[^a-z0-9]+/).filter(Boolean);
6814
- const ranked = ALL_OPERATIONS.map((op) => ({ op, score: words.reduce((n, w) => n + (op.id.includes(w) ? 4 : op.description.toLowerCase().includes(w) ? 1 : 0), 0) }))
6815
- .filter((x) => x.score > 0).sort((a, b) => b.score - a.score).slice(0, 10);
6840
+ const ranked = searchTools(ALL_OPERATIONS, input.query);
6816
6841
  const matches = ranked.map(({ op }) => ({ name: op.id, description: op.description.split(/(?<=\.)\s/)[0], billable: !!op.billable, annotations: op.annotations }));
6817
6842
  return ok({ matches }, matches.map((o) => `${o.name}: ${o.description}`).join('\n') || 'No matching tools. Try another task description.');
6818
6843
  }