@slatesvideo/shared 0.4.5 → 0.4.7

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.
@@ -25,7 +25,9 @@ export declare const getWorkspaceState: Operation<{
25
25
  }>;
26
26
  export declare const getMe: Operation<Record<string, never>>;
27
27
  export declare const getCreditBalance: Operation<Record<string, never>>;
28
- export declare const listAvailableModels: Operation<Record<string, never>>;
28
+ export declare const listAvailableModels: Operation<{
29
+ filter?: string;
30
+ }>;
29
31
  export declare const estimateGenerationCost: Operation<{
30
32
  model: string;
31
33
  quantity?: number;
@@ -39,6 +41,9 @@ export declare const getProject: Operation<{
39
41
  }>;
40
42
  export declare const listAssets: Operation<{
41
43
  projectId: string;
44
+ type?: 'image' | 'video' | 'audio';
45
+ search?: string;
46
+ limit?: number;
42
47
  }>;
43
48
  export declare const getAssetImage: Operation<{
44
49
  id: string;
@@ -166,7 +171,7 @@ export declare function videoCostKey(input: {
166
171
  }): string;
167
172
  export declare const generateVideo: Operation<{
168
173
  prompt: string;
169
- model: VideoModel;
174
+ model: string;
170
175
  projectId?: string;
171
176
  aspectRatio?: '1:1' | '16:9' | '9:16' | '4:3' | '3:4' | '21:9' | '9:21' | '4:5' | '5:4' | '2:3' | '3:2';
172
177
  duration?: number;
@@ -216,6 +221,7 @@ export declare const generateMotionTransfer: Operation<{
216
221
  }>;
217
222
  export declare const getGenerationStatus: Operation<{
218
223
  generationId: string;
224
+ waitSeconds?: number;
219
225
  }>;
220
226
  export declare const listGenerations: Operation<{
221
227
  projectId?: string;
@@ -22,7 +22,9 @@ export function defaultContext() {
22
22
  // ── Helpers ─────────────────────────────────────────────────────
23
23
  function ok(data, text) {
24
24
  return {
25
- text: text ?? JSON.stringify(data, null, 2),
25
+ // Compact JSON — pretty-printing (indent 2) cost ~10-15% extra tokens
26
+ // on every tool result the calling LLM reads.
27
+ text: text ?? JSON.stringify(data),
26
28
  data,
27
29
  };
28
30
  }
@@ -77,11 +79,27 @@ export const getCreditBalance = {
77
79
  };
78
80
  export const listAvailableModels = {
79
81
  id: 'slates_list_available_models',
80
- description: 'Full registry of generation models with their per-call credit cost. Use this for cost estimation.',
81
- input: z.object({}).strict(),
82
- async run(_input, ctx) {
82
+ description: 'Registry of generation model cost keys with per-call credit cost in cents, as a compact "key cents" table. Optional `filter` substring (e.g. "kling" or "nano-banana") keeps the result small — prefer it. For a single known model, slates_estimate_generation_cost is cheaper still.',
83
+ input: z.object({
84
+ filter: z.string().optional().describe('Substring match on the model key, e.g. "kling-v3" or "seedance"'),
85
+ }),
86
+ async run(input, ctx) {
83
87
  const r = await ctx.cloud().get('/api/agent/models');
84
- return ok(r);
88
+ let models = r.models;
89
+ if (input.filter) {
90
+ const q = input.filter.toLowerCase();
91
+ models = models.filter((m) => m.model.toLowerCase().includes(q));
92
+ }
93
+ // Compact text table — "key cents" lines are ~5x denser than the raw
94
+ // JSON registry (the full pretty-printed dump was an ~8k-token leak).
95
+ const table = models.map((m) => `${m.model} ${m.cost_cents}`).join('\n');
96
+ return {
97
+ text: `${models.length} COST keys (cents per generation)${input.filter ? ` matching "${input.filter}"` : ''}. ` +
98
+ `NOTE: these are billing keys for cost lookup ONLY — the \`model\` param on slates_generate_video takes a BASE id ` +
99
+ `(kling-v3.0-std | kling-v3.0-pro | kling-v3.0-omni | seedance-2 | veo-3.1-fast | veo-3.1-standard) with duration/videoResolution as separate params:\n` +
100
+ table,
101
+ data: { count: models.length, credit_markup: r.credit_markup },
102
+ };
85
103
  },
86
104
  };
87
105
  export const estimateGenerationCost = {
@@ -138,15 +156,56 @@ export const getProject = {
138
156
  },
139
157
  };
140
158
  // ── Assets ──────────────────────────────────────────────────────
159
+ /** Compact projection of a raw asset row — the full row (prompt, settings,
160
+ * paths, thumbnails) is 20-50x heavier and was the single biggest context
161
+ * leak in early agent sessions (494 assets ≈ 130KB ≈ 33k tokens in ONE
162
+ * tool result). Every op that embeds asset lists uses this. */
163
+ function compactAsset(a) {
164
+ const r = a;
165
+ const prompt = typeof r.prompt === 'string' ? r.prompt : '';
166
+ return {
167
+ id: r.id,
168
+ code: r.code ?? null,
169
+ label: r.label ?? (prompt ? prompt.slice(0, 40) : null),
170
+ type: r.type,
171
+ created_at: r.createdAt ?? r.created_at ?? undefined,
172
+ };
173
+ }
141
174
  export const listAssets = {
142
175
  id: 'slates_list_assets',
143
- description: 'List all assets (images + videos) in a Slates project. Each asset carries its short code (IMG-A12 / VID-V3 / AUD-S1 — the badge the user sees on the gallery card) and label. When the user names an asset by code ("use IMG-A36 as the reference"), call this and match on the code field to resolve the assetId. Always speak about assets by code + label, never by UUID.',
144
- input: z.object({ projectId: z.string().uuid() }),
176
+ description: 'List assets in a Slates project as COMPACT rows (id, code, label, type) newest first, default limit 50. Each asset carries its short code (IMG-A12 / VID-V3 / AUD-S1 — the badge the user sees on the gallery card) and label. When the user names an asset by code ("use IMG-A36 as the reference"), pass it as `search` to resolve the assetId. Always speak about assets by code + label, never by UUID. NOTE: generate_* results already return the new asset ids — do NOT call this to find an asset you just created.',
177
+ input: z.object({
178
+ projectId: z.string().uuid(),
179
+ type: z.enum(['image', 'video', 'audio']).optional().describe('Only this asset type'),
180
+ search: z.string().optional().describe('Case-insensitive match on code or label (e.g. "IMG-A36" or "sunset")'),
181
+ limit: z.number().int().min(1).max(500).optional().describe('Max rows (default 50, newest first)'),
182
+ }),
145
183
  async run(input, ctx) {
146
184
  const r = await ctx.desktop().get('/agent/assets', {
147
185
  projectId: input.projectId,
148
186
  });
149
- return ok(r);
187
+ const all = (r.assets ?? []).map(compactAsset);
188
+ let rows = all;
189
+ if (input.type)
190
+ rows = rows.filter((a) => a.type === input.type);
191
+ if (input.search) {
192
+ const q = input.search.toLowerCase();
193
+ rows = rows.filter((a) => String(a.code ?? '').toLowerCase().includes(q) ||
194
+ String(a.label ?? '').toLowerCase().includes(q));
195
+ }
196
+ // Newest first, then cap. (created_at is ISO — lexicographic sort works.)
197
+ rows = rows
198
+ .slice()
199
+ .sort((a, b) => String(b.created_at ?? '').localeCompare(String(a.created_at ?? '')));
200
+ const limit = input.limit ?? 50;
201
+ const truncated = rows.length > limit;
202
+ rows = rows.slice(0, limit);
203
+ return ok({
204
+ total_matching: truncated ? undefined : rows.length,
205
+ total_in_project: all.length,
206
+ truncated,
207
+ assets: rows.map(({ created_at: _drop, ...rest }) => rest),
208
+ });
150
209
  },
151
210
  };
152
211
  export const getAssetImage = {
@@ -760,7 +819,10 @@ export const generateImage = {
760
819
  resolution,
761
820
  cost_cents: totalCents,
762
821
  cost_dollars: (totalCents / 100).toFixed(2),
763
- assets: assetList,
822
+ // Compact refs only — the full rows (prompt/settings/paths) were a
823
+ // multi-KB leak per generation and everything needed downstream is
824
+ // the id/code/label.
825
+ assets: assetList.map(compactAsset),
764
826
  generationIds: result.generationIds ?? (result.generationId ? [result.generationId] : []),
765
827
  ...(partialFailure
766
828
  ? { partial: true, error: result.error ?? 'unknown error', requested_count: requestedCount }
@@ -1010,6 +1072,60 @@ export function videoCostKey(input) {
1010
1072
  }
1011
1073
  throw new Error(`Unknown video model: ${input.model}`);
1012
1074
  }
1075
+ /**
1076
+ * Forgiving model-id resolver. Agents routinely paste registry COST keys
1077
+ * ("kling-v3-standard-8s", "seedance-2-1080p-8s") into the `model` param —
1078
+ * an observed 15-turn error-retry spiral in a live session. Instead of
1079
+ * rejecting, extract the intent: base model + any duration / resolution /
1080
+ * audio the key encodes. Returns null only when nothing matches.
1081
+ */
1082
+ function resolveVideoModel(raw) {
1083
+ let s = raw.trim().toLowerCase();
1084
+ const out = { model: 'kling-v3.0-std' };
1085
+ const dur = /-(\d+)s\b/.exec(s);
1086
+ if (dur) {
1087
+ out.duration = parseInt(dur[1], 10);
1088
+ s = s.replace(/-(\d+)s\b/, '');
1089
+ }
1090
+ const res = /-(480p|720p|1080p|4k)\b/.exec(s);
1091
+ if (res) {
1092
+ out.videoResolution = res[1];
1093
+ s = s.replace(/-(480p|720p|1080p|4k)\b/, '');
1094
+ }
1095
+ if (/-audio\b/.test(s)) {
1096
+ out.sound = true;
1097
+ s = s.replace(/-audio\b/, '');
1098
+ }
1099
+ if (/-(realface|face)\b/.test(s)) {
1100
+ out.seedanceFace = true;
1101
+ s = s.replace(/-(realface|face)\b/, '');
1102
+ }
1103
+ const direct = VIDEO_MODELS.find((m) => m === s);
1104
+ if (direct) {
1105
+ out.model = direct;
1106
+ return out;
1107
+ }
1108
+ const aliases = {
1109
+ 'kling-v3-standard': 'kling-v3.0-std',
1110
+ 'kling-v3-std': 'kling-v3.0-std',
1111
+ 'kling-v3.0-standard': 'kling-v3.0-std',
1112
+ 'kling-v3': 'kling-v3.0-std',
1113
+ 'kling-v3-pro': 'kling-v3.0-pro',
1114
+ 'kling-v3-omni': 'kling-v3.0-omni',
1115
+ 'kling-v3-omni-pro': 'kling-v3.0-omni',
1116
+ 'kling-v3.0-omni-pro': 'kling-v3.0-omni',
1117
+ 'seedance-2.0': 'seedance-2',
1118
+ 'seedance-2-0': 'seedance-2',
1119
+ seedance: 'seedance-2',
1120
+ 'veo-3.1': 'veo-3.1-fast',
1121
+ 'veo-3': 'veo-3.1-fast',
1122
+ };
1123
+ if (aliases[s]) {
1124
+ out.model = aliases[s];
1125
+ return out;
1126
+ }
1127
+ return null;
1128
+ }
1013
1129
  // Maps a video model id to its bundled prompting skill (frontmatter `name:`),
1014
1130
  // so guidance text points at a skill that actually exists. Deriving the name
1015
1131
  // via model.split('-')[0] produced 'slates-prompting-kling' / '...-veo', which
@@ -1028,7 +1144,7 @@ export const generateVideo = {
1028
1144
  description: 'Generate video via Slates credits. REQUIRED before calling: read slates-model-selection (routing: Kling = default, Seedance = premium/physics, Veo = audio-only niche), the slates-cost-discipline skill, plus the per-model prompting skill (slates-prompting-seedance / slates-prompting-kling-v3 / slates-prompting-veo-3) — video models prompt very differently. Also read slates-content-policy when the scene involves conflict, creatures, crowds, destruction, weapons, or young characters (build it safe-by-construction so the filter doesn\'t reject or degrade it). projectId is REQUIRED for UI integration (the user sees a progress card and the asset lands in the project — without it the call fails). aspectRatio + duration are required (server returns requires_clarification when missing). Cost > $0.50 returns requires_confirm — pass confirm=true after explicit user OK. Veo locks to 16:9 and to 4/6/8s durations (4K only at 8s). Image-to-video via firstFrameAssetId. Frames-to-video via firstFrameAssetId + lastFrameAssetId (Veo / Seedance only). Ingredients via ingredientAssetIds (Kling Omni / Seedance). No skill files installed? Call slates_get_prompting_guide with the per-model guide (\'slates-prompting-veo-3\' / \'slates-prompting-kling-v3\' / \'slates-prompting-seedance\') and \'slates-cost-discipline\' before first use.',
1029
1145
  input: z.object({
1030
1146
  prompt: z.string().min(1).max(4000),
1031
- model: z.enum(VIDEO_MODELS).describe('Route per the slates-model-selection skill. DEFAULT = Kling V3.0 std (general-purpose, cost-effective, strong start-frame adherence; pro = higher polish; omni = multi-char dialogue + audio). Seedance 2 = the PREMIUM tier — pick it whenever physics/effects/scale remotely matter or for hero shots; audio included, first+last frame + up to 9 reference images, full 480p–4K ladder (native 4K) via videoResolution. Veo 3.1 = niche, never the default — only when native synced audio must generate WITH the video in one gen; locks 16:9, 4/6/8s. For exact per-call credit cost, call slates_estimate_generation_cost or slates_list_available_models — never quote prices from memory (they change).'),
1147
+ model: z.string().describe('One of: kling-v3.0-std | kling-v3.0-pro | kling-v3.0-omni | seedance-2 | veo-3.1-fast | veo-3.1-standard. Pass the BASE id — duration and videoResolution are separate params (registry entries like "kling-v3-standard-8s" are COST keys, not model ids; if you pass one it is auto-resolved). Route per the slates-model-selection skill. DEFAULT = Kling V3.0 std (general-purpose, cost-effective, strong start-frame adherence; pro = higher polish; omni = multi-char dialogue + audio). Seedance 2 = the PREMIUM tier — pick it whenever physics/effects/scale remotely matter or for hero shots; audio included, first+last frame + up to 9 reference images, full 480p–4K ladder (native 4K) via videoResolution. Veo 3.1 = niche, never the default — only when native synced audio must generate WITH the video in one gen; locks 16:9, 4/6/8s. For exact per-call credit cost, call slates_estimate_generation_cost — never quote prices from memory (they change).'),
1032
1148
  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.'),
1033
1149
  aspectRatio: z.enum(['1:1', '16:9', '9:16', '4:3', '3:4', '21:9', '9:21', '4:5', '5:4', '2:3', '3:2']).optional().describe('Veo locks to 16:9 — passing anything else will be ignored or fail. Kling/Seedance support all.'),
1034
1150
  duration: z.number().int().min(4).max(15).optional().describe('Seconds. Kling: 5-15. Veo: 4, 6, or 8 only (4K only at 8s). Seedance: 4-15. Default 5 if omitted but always be explicit (cost scales linearly).'),
@@ -1051,6 +1167,25 @@ export const generateVideo = {
1051
1167
  confirm: z.boolean().optional().describe('Set true after explicit user OK to bypass the >$0.50 cost confirm gate (which fires for almost every video gen since they\'re expensive).'),
1052
1168
  }),
1053
1169
  async run(input, ctx) {
1170
+ // Resolve the model FIRST — forgiving normalization (cost keys, alias
1171
+ // spellings) with a teaching error, so a wrong id never costs the agent
1172
+ // a retry spiral. Anything the key encoded (duration/res/audio) fills
1173
+ // params the caller left blank; explicit params always win.
1174
+ const resolved = resolveVideoModel(input.model);
1175
+ if (!resolved) {
1176
+ throw new Error(`Unknown video model "${input.model}". Valid model ids: ${VIDEO_MODELS.join(' | ')}. ` +
1177
+ `Registry entries like "kling-v3-standard-8s" or "seedance-2-1080p-8s" are COST keys, not model ids — ` +
1178
+ `pass the base id and set duration / videoResolution as separate params.`);
1179
+ }
1180
+ input.model = resolved.model;
1181
+ if (input.duration == null && resolved.duration != null)
1182
+ input.duration = resolved.duration;
1183
+ if (input.videoResolution == null && resolved.videoResolution != null)
1184
+ input.videoResolution = resolved.videoResolution;
1185
+ if (input.sound == null && resolved.sound != null)
1186
+ input.sound = resolved.sound;
1187
+ if (input.seedanceFace == null && resolved.seedanceFace != null)
1188
+ input.seedanceFace = resolved.seedanceFace;
1054
1189
  // projectId is required for video — without it there's no UI feedback,
1055
1190
  // no asset to reference later, and a failed gen leaves the user with
1056
1191
  // nothing. The MCP-only headless path that exists for image gen is
@@ -1455,12 +1590,45 @@ export const generateMotionTransfer = {
1455
1590
  // ── Generation status (background mode) ─────────────────────────
1456
1591
  export const getGenerationStatus = {
1457
1592
  id: 'slates_get_generation_status',
1458
- description: 'Poll one generation by id. Returns status (pending/processing/completed/failed/cancelled), the error message on failure, and the finished asset record (with id, code, filePath) on completion. Use after submitting any generate_* op with background=true; poll every 5-15s video generations commonly take 1-5 minutes. Generations survive app restarts (the desktop resumes in-flight provider jobs on boot).',
1459
- input: z.object({ generationId: z.string().uuid() }),
1593
+ description: 'Poll one generation by id. Returns status (pending/processing/completed/failed/cancelled), the error message on failure, and the finished asset record (with id, code, filePath) on completion. Use after submitting any generate_* op with background=true. ALWAYS pass waitSeconds (use 45) — the call long-polls internally and returns early the moment the generation reaches a terminal state, so ONE call replaces a dozen rapid polls (each poll turn re-reads your whole context; rapid polling is the #1 orchestration-cost leak). Video generations commonly take 1-5 minutes: expect 1-4 long-poll calls, never a tight loop. Generations survive app restarts (the desktop resumes in-flight provider jobs on boot).',
1594
+ input: z.object({
1595
+ generationId: z.string().uuid(),
1596
+ waitSeconds: z
1597
+ .number()
1598
+ .int()
1599
+ .min(0)
1600
+ .max(50)
1601
+ .optional()
1602
+ .describe('Long-poll: block up to N seconds, returning early on completion/failure. Use 45.'),
1603
+ }),
1460
1604
  async run(input, ctx) {
1461
1605
  const desktop = ctx.desktop();
1462
1606
  await desktop.requireCapability('background-generation', 'background generation');
1463
- return ok(await desktop.get('/agent/generation/status', { id: input.generationId }));
1607
+ const deadline = Date.now() + Math.min(Math.max(input.waitSeconds ?? 0, 0), 50) * 1000;
1608
+ for (;;) {
1609
+ const r = await desktop.get('/agent/generation/status', { id: input.generationId });
1610
+ const g = r.generation;
1611
+ const status = g?.status ?? r.status;
1612
+ const terminal = status === 'completed' || status === 'failed' || status === 'cancelled';
1613
+ if (terminal || Date.now() >= deadline) {
1614
+ if (!g)
1615
+ return ok(r);
1616
+ // Slim the payload: the raw generation row (full prompt/settings)
1617
+ // and full asset row are context bloat — return the fields the
1618
+ // agent acts on, with the EXACT billed cost front and center.
1619
+ return ok({
1620
+ status,
1621
+ cost_cents: g.cost ?? null,
1622
+ error: g.error ?? null,
1623
+ model: g.model,
1624
+ completed_at: g.completedAt ?? null,
1625
+ asset: r.asset
1626
+ ? { ...compactAsset(r.asset), file_path: r.asset.filePath }
1627
+ : null,
1628
+ });
1629
+ }
1630
+ await new Promise((resolve) => setTimeout(resolve, Math.min(3000, deadline - Date.now())));
1631
+ }
1464
1632
  },
1465
1633
  };
1466
1634
  export const listGenerations = {
@@ -1477,7 +1645,8 @@ export const listGenerations = {
1477
1645
  return ok(await desktop.get('/agent/generation/list', {
1478
1646
  projectId: input.projectId,
1479
1647
  status: input.status,
1480
- limit: input.limit,
1648
+ // Default cap — an unbounded history dump is a context leak.
1649
+ limit: input.limit ?? 20,
1481
1650
  }));
1482
1651
  },
1483
1652
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@slatesvideo/shared",
3
- "version": "0.4.5",
3
+ "version": "0.4.7",
4
4
  "description": "Shared operations layer for the Slates MCP server and CLI: auth, cloud/desktop clients, and the single tool surface both consume. Most users want @slatesvideo/mcp-server or @slatesvideo/cli instead.",
5
5
  "license": "MIT",
6
6
  "type": "module",