@slatesvideo/shared 0.6.2 → 0.6.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api-url.d.ts +9 -0
- package/dist/api-url.js +9 -0
- package/dist/auth.d.ts +13 -1
- package/dist/auth.js +9 -5
- package/dist/clients/cloud.d.ts +3 -0
- package/dist/clients/cloud.js +34 -3
- package/dist/clients/desktop.js +3 -0
- package/dist/index.d.ts +8 -2
- package/dist/index.js +44 -1
- package/dist/operations/index.d.ts +243 -31
- package/dist/operations/index.js +1483 -154
- package/dist/operations/surface.d.ts +69 -0
- package/dist/operations/surface.js +227 -0
- package/dist/prompts/agent-doctrine.d.ts +36 -0
- package/dist/prompts/agent-doctrine.js +201 -0
- package/dist/prompts/asset-label.d.ts +23 -0
- package/dist/prompts/asset-label.js +70 -0
- package/dist/prompts/banned-tokens.d.ts +40 -0
- package/dist/prompts/banned-tokens.js +219 -0
- package/dist/prompts/character-sheet.d.ts +0 -2
- package/dist/prompts/character-sheet.js +0 -2
- package/dist/prompts/craft-cards.d.ts +20 -0
- package/dist/prompts/craft-cards.js +82 -0
- package/dist/prompts/environment-sheet.js +16 -0
- package/dist/prompts/index.d.ts +1 -0
- package/dist/prompts/index.js +4 -0
- package/dist/prompts/model-capabilities.d.ts +65 -1
- package/dist/prompts/model-capabilities.js +139 -2
- package/dist/prompts/model-facts.d.ts +20 -4
- package/dist/prompts/model-facts.js +95 -27
- package/dist/prompts/partials.generated.js +2 -1
- package/dist/prompts/prompting-tips.d.ts +1 -1
- package/dist/prompts/prompting-tips.js +123 -0
- package/dist/prompts/reference-composer.d.ts +36 -7
- package/dist/prompts/reference-composer.js +75 -20
- package/dist/prompts/reference-rules.d.ts +15 -26
- package/dist/prompts/reference-rules.js +15 -93
- package/dist/prompts/shot-grammar.d.ts +154 -0
- package/dist/prompts/shot-grammar.js +184 -0
- package/dist/prompts/shot-spec.d.ts +265 -0
- package/dist/prompts/shot-spec.js +303 -0
- package/dist/skills/content.js +25 -22
- package/exports/slates-prompt-builder/generated/SKILL.md +3 -3
- package/exports/slates-prompt-builder/generated/reference-content-policy.md +6 -0
- package/exports/slates-prompt-builder/generated/reference-kling.md +22 -0
- package/exports/slates-prompt-builder/generated/reference-nano-banana.md +17 -0
- package/exports/slates-prompt-builder/generated/reference-seedance.md +19 -1
- package/exports/slates-prompt-builder/generated/slates-prompt-builder-manifest.json +17 -17
- package/exports/slates-prompt-builder/generated/slates-prompt-builder.skill +0 -0
- package/package.json +83 -73
- package/skills/_partials/decision-log.md +5 -4
- package/skills/_partials/thresholds.md +19 -0
- package/skills/slates-content-policy.md +15 -1
- package/skills/slates-cost-discipline.md +26 -4
- package/skills/slates-model-selection.md +2 -2
- package/skills/slates-one-prompt-film.md +20 -12
- package/skills/slates-project-organization.md +1 -1
- package/skills/slates-prompting-elevenlabs.md +61 -2
- package/skills/slates-prompting-flux-2-max.md +39 -0
- package/skills/slates-prompting-gpt-image-2.md +109 -70
- package/skills/slates-prompting-inworld-tts.md +166 -0
- package/skills/slates-prompting-kling-v3.md +39 -0
- package/skills/slates-prompting-lip-sync.md +38 -0
- package/skills/slates-prompting-ltx-2-5.md +218 -0
- package/skills/slates-prompting-minimax-h3.md +39 -0
- package/skills/slates-prompting-motion-transfer.md +38 -0
- package/skills/slates-prompting-nano-banana-2.md +36 -0
- package/skills/slates-prompting-omni-flash.md +41 -0
- package/skills/slates-prompting-seed-audio.md +38 -0
- package/skills/slates-prompting-seedance-2-5.md +38 -0
- package/skills/slates-prompting-seedance.md +36 -1
- package/skills/slates-prompting-seedream-5-lite.md +38 -0
- package/skills/slates-prompting-veo-3.md +39 -0
- package/skills/slates-shot-variety.md +53 -0
- package/skills/slates-storyboard-from-script.md +31 -15
- package/skills/slates-style-prompting.md +1 -1
- package/skills/slates-vision-feedback-loop.md +1 -1
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { z } from 'zod';
|
|
2
|
+
import type { OperationAnnotations, OperationGroup, OperationTier } from './index.js';
|
|
3
|
+
/** Structural shape of an op, declared locally so this module never imports a
|
|
4
|
+
* VALUE from the registry (which imports this one). */
|
|
5
|
+
export interface SurfaceOp {
|
|
6
|
+
id: string;
|
|
7
|
+
description: string;
|
|
8
|
+
input: z.ZodType<unknown>;
|
|
9
|
+
billable?: boolean;
|
|
10
|
+
tier?: OperationTier;
|
|
11
|
+
group?: OperationGroup;
|
|
12
|
+
annotations?: OperationAnnotations;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Derive an op's four hints.
|
|
16
|
+
*
|
|
17
|
+
* `openWorldHint` means "reaches something outside this system": every billable
|
|
18
|
+
* op hits a provider, and every Blender op hits a separate process over a
|
|
19
|
+
* socket. Everything else touches only the user's own workspace.
|
|
20
|
+
*/
|
|
21
|
+
export declare function annotate(id: string, billable: boolean | undefined): OperationAnnotations;
|
|
22
|
+
/**
|
|
23
|
+
* The deferred groups, by op id. Everything NOT listed here is `core`.
|
|
24
|
+
*
|
|
25
|
+
* The cut is "what a session needs to do the work" versus "what it needs once,
|
|
26
|
+
* when the user asks for that thing". Creating, generating, reviewing and
|
|
27
|
+
* shot-listing are core; tidying the library, cutting a timeline, renaming and
|
|
28
|
+
* deleting, and driving Blender are not — and each is a coherent thing a user
|
|
29
|
+
* asks for by name, which is what makes a group loadable in one call.
|
|
30
|
+
*
|
|
31
|
+
* 🚨 A GROUP IS A BUDGET, NOT A LAW. If `final_state` falls on timeline or
|
|
32
|
+
* library tasks after this shipped, move those ops back to core. The point is
|
|
33
|
+
* the per-turn byte count, and a deferral that costs a turn every time is not
|
|
34
|
+
* paying for itself.
|
|
35
|
+
*/
|
|
36
|
+
export declare const OPERATION_GROUPS: Record<OperationGroup, readonly string[]>;
|
|
37
|
+
export declare function groupFor(id: string): OperationGroup | undefined;
|
|
38
|
+
/** `extended` iff the op names a group. Absent from every group ⇒ `core`, so a
|
|
39
|
+
* new op is visible until someone deliberately defers it. */
|
|
40
|
+
export declare function tierFor(id: string): OperationTier;
|
|
41
|
+
/** One-line summary of each group, for `slates_load_tools`' own description. */
|
|
42
|
+
export declare const GROUP_SUMMARY: Record<OperationGroup, string>;
|
|
43
|
+
export interface ToolDefinition {
|
|
44
|
+
name: string;
|
|
45
|
+
description: string;
|
|
46
|
+
/** JSON Schema. Both surfaces read the same object under their own key name. */
|
|
47
|
+
inputSchema: Record<string, unknown>;
|
|
48
|
+
annotations: OperationAnnotations;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Render one op's tool definition.
|
|
52
|
+
*
|
|
53
|
+
* `$refStrategy: 'none'` is the shape both surfaces now emit: inlined, so no
|
|
54
|
+
* client has to resolve a `$ref`, and byte-identical between them — which is
|
|
55
|
+
* what makes "the two surfaces expose the same tools" checkable rather than
|
|
56
|
+
* merely asserted about the id set.
|
|
57
|
+
*/
|
|
58
|
+
export declare function toolDefinition(op: SurfaceOp): ToolDefinition;
|
|
59
|
+
/**
|
|
60
|
+
* Render a tool surface.
|
|
61
|
+
*
|
|
62
|
+
* `desktop` sends `core` plus whatever groups have been loaded this run; `mcp`
|
|
63
|
+
* sends everything, because a stdio server has no run to append to.
|
|
64
|
+
*/
|
|
65
|
+
export declare function toolDefinitions(ops: readonly SurfaceOp[], opts: {
|
|
66
|
+
surface: 'desktop' | 'mcp';
|
|
67
|
+
groups?: readonly OperationGroup[];
|
|
68
|
+
}): ToolDefinition[];
|
|
69
|
+
//# sourceMappingURL=surface.d.ts.map
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// THE TOOL SURFACE: annotations, tiers, and the ONE schema renderer.
|
|
3
|
+
//
|
|
4
|
+
// Three jobs that all answer "what does a client see, and when":
|
|
5
|
+
//
|
|
6
|
+
// 1. ANNOTATIONS (MCP spec 2025-06-18). `readOnlyHint` / `destructiveHint` /
|
|
7
|
+
// `idempotentHint` / `openWorldHint`, so a host can auto-approve a read
|
|
8
|
+
// and warn on a delete. Without them `slates_list_assets` prompts exactly
|
|
9
|
+
// like `slates_delete_project`, every prompt looks the same, and the user
|
|
10
|
+
// learns to click through all of them.
|
|
11
|
+
//
|
|
12
|
+
// 2. TIERS. 90 ops is 112 KB of descriptions and JSON schemas on EVERY
|
|
13
|
+
// desktop Studio Agent turn. `core` is what a session needs to work;
|
|
14
|
+
// `extended` is deferred behind `slates_load_tools` and appended to the
|
|
15
|
+
// run's tool list once a group loads. The MCP server still registers
|
|
16
|
+
// everything — a stdio server has no run to append to, and Claude Code
|
|
17
|
+
// already defers stdio tool schemas through its own tool search.
|
|
18
|
+
//
|
|
19
|
+
// 3. ONE SCHEMA RENDERER. The desktop rendered `$refStrategy: 'none'` and
|
|
20
|
+
// the MCP server rendered `target: 'openApi3'`, so "the two surfaces
|
|
21
|
+
// expose the same tools" was true of the ID SET and unproven of the
|
|
22
|
+
// BYTES. `toolDefinitions()` is now the only renderer either one calls.
|
|
23
|
+
//
|
|
24
|
+
// 🚨 NOTHING HERE IS HAND-SET PER OP. Annotations are derived from the op id
|
|
25
|
+
// by the rules below, and `scripts/agent-surface-lockstep-check.mjs` re-derives
|
|
26
|
+
// them INDEPENDENTLY from each op's own transport verbs — a `readOnlyHint` on
|
|
27
|
+
// an op whose `run` body posts is a lie that lets a host auto-approve a
|
|
28
|
+
// mutation, so the check has to be able to catch it, and it is mutation-tested.
|
|
29
|
+
// ============================================================
|
|
30
|
+
import { zodToJsonSchema } from 'zod-to-json-schema';
|
|
31
|
+
// ── 1. Annotations ──────────────────────────────────────────────────────────
|
|
32
|
+
/** Ops that only read. Prefixes, because the verb IS the first word of the id. */
|
|
33
|
+
const READ_ONLY_PREFIXES = [
|
|
34
|
+
'slates_get_',
|
|
35
|
+
'slates_list_',
|
|
36
|
+
'slates_estimate_',
|
|
37
|
+
'slates_blender_status',
|
|
38
|
+
'slates_blender_scene',
|
|
39
|
+
'slates_blender_docs',
|
|
40
|
+
'slates_blender_search_docs',
|
|
41
|
+
];
|
|
42
|
+
/**
|
|
43
|
+
* Ops whose effect a user cannot undo from inside Slates.
|
|
44
|
+
*
|
|
45
|
+
* Moves are here on purpose: `slates_move_assets_to_project` relocates files on
|
|
46
|
+
* disk, and an agent that guessed the wrong project has no "put it back"
|
|
47
|
+
* without knowing where they came from. A rename is recoverable; a move across
|
|
48
|
+
* projects, in practice, is not.
|
|
49
|
+
*/
|
|
50
|
+
const DESTRUCTIVE_IDS = new Set([
|
|
51
|
+
'slates_delete_project',
|
|
52
|
+
'slates_delete_asset',
|
|
53
|
+
'slates_delete_folder',
|
|
54
|
+
'slates_delete_character',
|
|
55
|
+
'slates_delete_environment',
|
|
56
|
+
'slates_delete_style',
|
|
57
|
+
'slates_delete_storyboard',
|
|
58
|
+
'slates_delete_scene',
|
|
59
|
+
'slates_delete_frame',
|
|
60
|
+
'slates_move_assets_to_folder',
|
|
61
|
+
'slates_move_assets_to_project',
|
|
62
|
+
'slates_move_entity_to_project',
|
|
63
|
+
'slates_merge_shots',
|
|
64
|
+
'slates_split_shot',
|
|
65
|
+
]);
|
|
66
|
+
/** Same input, same end state — `update`/`set`/`reorder`/`rename` all overwrite
|
|
67
|
+
* rather than append, so re-running one is a no-op rather than a second edit. */
|
|
68
|
+
const IDEMPOTENT_PREFIXES = [
|
|
69
|
+
'slates_update_',
|
|
70
|
+
'slates_set_',
|
|
71
|
+
'slates_reorder_',
|
|
72
|
+
'slates_rename_',
|
|
73
|
+
'slates_batch_update_',
|
|
74
|
+
];
|
|
75
|
+
const startsWithAny = (id, prefixes) => prefixes.some((p) => id.startsWith(p));
|
|
76
|
+
/**
|
|
77
|
+
* Derive an op's four hints.
|
|
78
|
+
*
|
|
79
|
+
* `openWorldHint` means "reaches something outside this system": every billable
|
|
80
|
+
* op hits a provider, and every Blender op hits a separate process over a
|
|
81
|
+
* socket. Everything else touches only the user's own workspace.
|
|
82
|
+
*/
|
|
83
|
+
export function annotate(id, billable) {
|
|
84
|
+
const readOnlyHint = startsWithAny(id, READ_ONLY_PREFIXES);
|
|
85
|
+
return {
|
|
86
|
+
readOnlyHint,
|
|
87
|
+
// A read is never destructive, whatever the id looks like.
|
|
88
|
+
destructiveHint: !readOnlyHint && DESTRUCTIVE_IDS.has(id),
|
|
89
|
+
idempotentHint: readOnlyHint || startsWithAny(id, IDEMPOTENT_PREFIXES),
|
|
90
|
+
openWorldHint: !!billable || id.startsWith('slates_blender_'),
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
// ── 2. Tiers ────────────────────────────────────────────────────────────────
|
|
94
|
+
/**
|
|
95
|
+
* The deferred groups, by op id. Everything NOT listed here is `core`.
|
|
96
|
+
*
|
|
97
|
+
* The cut is "what a session needs to do the work" versus "what it needs once,
|
|
98
|
+
* when the user asks for that thing". Creating, generating, reviewing and
|
|
99
|
+
* shot-listing are core; tidying the library, cutting a timeline, renaming and
|
|
100
|
+
* deleting, and driving Blender are not — and each is a coherent thing a user
|
|
101
|
+
* asks for by name, which is what makes a group loadable in one call.
|
|
102
|
+
*
|
|
103
|
+
* 🚨 A GROUP IS A BUDGET, NOT A LAW. If `final_state` falls on timeline or
|
|
104
|
+
* library tasks after this shipped, move those ops back to core. The point is
|
|
105
|
+
* the per-turn byte count, and a deferral that costs a turn every time is not
|
|
106
|
+
* paying for itself.
|
|
107
|
+
*/
|
|
108
|
+
export const OPERATION_GROUPS = {
|
|
109
|
+
// Folders, styles, and moving assets around. Reached when the user says
|
|
110
|
+
// "tidy this up", never while making the thing.
|
|
111
|
+
library: [
|
|
112
|
+
'slates_list_folders',
|
|
113
|
+
'slates_create_folder',
|
|
114
|
+
'slates_rename_folder',
|
|
115
|
+
'slates_delete_folder',
|
|
116
|
+
'slates_set_folder_cover',
|
|
117
|
+
'slates_move_assets_to_folder',
|
|
118
|
+
'slates_move_assets_to_project',
|
|
119
|
+
'slates_copy_assets_to_project',
|
|
120
|
+
'slates_move_entity_to_project',
|
|
121
|
+
'slates_list_styles',
|
|
122
|
+
'slates_create_style',
|
|
123
|
+
'slates_update_style',
|
|
124
|
+
'slates_delete_style',
|
|
125
|
+
'slates_get_project_directory',
|
|
126
|
+
'slates_reveal_file',
|
|
127
|
+
],
|
|
128
|
+
// The cut and the export. A generation session never touches these.
|
|
129
|
+
timeline: [
|
|
130
|
+
'slates_get_timeline',
|
|
131
|
+
'slates_add_clip_to_timeline',
|
|
132
|
+
'slates_reorder_clips',
|
|
133
|
+
'slates_remove_clip',
|
|
134
|
+
'slates_add_timeline_track',
|
|
135
|
+
'slates_update_timeline_track',
|
|
136
|
+
'slates_remove_timeline_track',
|
|
137
|
+
'slates_update_timeline_settings',
|
|
138
|
+
'slates_export_video',
|
|
139
|
+
'slates_export_timeline_xml',
|
|
140
|
+
'slates_trim_video',
|
|
141
|
+
],
|
|
142
|
+
// Rename, delete, reorder. The 63-op CRUD tail the review measured at 39.5 KB.
|
|
143
|
+
admin: [
|
|
144
|
+
'slates_update_project',
|
|
145
|
+
'slates_delete_project',
|
|
146
|
+
'slates_delete_asset',
|
|
147
|
+
'slates_update_character',
|
|
148
|
+
'slates_delete_character',
|
|
149
|
+
'slates_update_environment',
|
|
150
|
+
'slates_delete_environment',
|
|
151
|
+
'slates_update_storyboard',
|
|
152
|
+
'slates_delete_storyboard',
|
|
153
|
+
'slates_update_scene',
|
|
154
|
+
'slates_delete_scene',
|
|
155
|
+
'slates_reorder_scenes',
|
|
156
|
+
'slates_delete_frame',
|
|
157
|
+
'slates_batch_update_frames',
|
|
158
|
+
'slates_duplicate_shot',
|
|
159
|
+
'slates_split_shot',
|
|
160
|
+
'slates_merge_shots',
|
|
161
|
+
],
|
|
162
|
+
// A third transport nobody without Blender installed can reach.
|
|
163
|
+
blender: [
|
|
164
|
+
'slates_blender_status',
|
|
165
|
+
'slates_blender_execute',
|
|
166
|
+
'slates_blender_scene',
|
|
167
|
+
'slates_blender_docs',
|
|
168
|
+
'slates_blender_search_docs',
|
|
169
|
+
'slates_blender_render_blocking',
|
|
170
|
+
],
|
|
171
|
+
};
|
|
172
|
+
const GROUP_BY_OP = new Map();
|
|
173
|
+
for (const [group, ids] of Object.entries(OPERATION_GROUPS)) {
|
|
174
|
+
for (const id of ids)
|
|
175
|
+
GROUP_BY_OP.set(id, group);
|
|
176
|
+
}
|
|
177
|
+
export function groupFor(id) {
|
|
178
|
+
return GROUP_BY_OP.get(id);
|
|
179
|
+
}
|
|
180
|
+
/** `extended` iff the op names a group. Absent from every group ⇒ `core`, so a
|
|
181
|
+
* new op is visible until someone deliberately defers it. */
|
|
182
|
+
export function tierFor(id) {
|
|
183
|
+
return GROUP_BY_OP.has(id) ? 'extended' : 'core';
|
|
184
|
+
}
|
|
185
|
+
/** One-line summary of each group, for `slates_load_tools`' own description. */
|
|
186
|
+
export const GROUP_SUMMARY = {
|
|
187
|
+
library: 'folders, styles, moving and copying assets between projects, revealing files on disk',
|
|
188
|
+
timeline: 'the timeline (tracks, clips, settings), video export and NLE XML export, clip trimming',
|
|
189
|
+
admin: 'rename / delete / reorder for projects, characters, environments, storyboards, scenes and frames; shot duplicate, split and merge',
|
|
190
|
+
blender: 'the Blender previs bridge — scene inspection, bpy execution, API docs, grey-box render',
|
|
191
|
+
};
|
|
192
|
+
/**
|
|
193
|
+
* Render one op's tool definition.
|
|
194
|
+
*
|
|
195
|
+
* `$refStrategy: 'none'` is the shape both surfaces now emit: inlined, so no
|
|
196
|
+
* client has to resolve a `$ref`, and byte-identical between them — which is
|
|
197
|
+
* what makes "the two surfaces expose the same tools" checkable rather than
|
|
198
|
+
* merely asserted about the id set.
|
|
199
|
+
*/
|
|
200
|
+
export function toolDefinition(op) {
|
|
201
|
+
const schema = zodToJsonSchema(op.input, { $refStrategy: 'none' });
|
|
202
|
+
delete schema.$schema;
|
|
203
|
+
return {
|
|
204
|
+
name: op.id,
|
|
205
|
+
description: op.description,
|
|
206
|
+
inputSchema: schema,
|
|
207
|
+
annotations: op.annotations ?? annotate(op.id, op.billable),
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Render a tool surface.
|
|
212
|
+
*
|
|
213
|
+
* `desktop` sends `core` plus whatever groups have been loaded this run; `mcp`
|
|
214
|
+
* sends everything, because a stdio server has no run to append to.
|
|
215
|
+
*/
|
|
216
|
+
export function toolDefinitions(ops, opts) {
|
|
217
|
+
const loaded = new Set(opts.groups ?? []);
|
|
218
|
+
return ops
|
|
219
|
+
.filter((op) => {
|
|
220
|
+
if (opts.surface === 'mcp')
|
|
221
|
+
return true;
|
|
222
|
+
const group = groupFor(op.id);
|
|
223
|
+
return group === undefined || loaded.has(group);
|
|
224
|
+
})
|
|
225
|
+
.map(toolDefinition);
|
|
226
|
+
}
|
|
227
|
+
//# sourceMappingURL=surface.js.map
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which agent surface is being briefed.
|
|
3
|
+
*
|
|
4
|
+
* `desktop` — the in-app Studio Agent. Its loop enforces plan approval in
|
|
5
|
+
* code, auto-polls generation status, and displays orchestration cost itself.
|
|
6
|
+
* `mcp` — Claude Code / Claude Desktop / Cursor / Codex over stdio. No
|
|
7
|
+
* `present_plan` tool, no auto-poll, no app chrome; the consent gate is the
|
|
8
|
+
* op-level `requires_confirm` threshold plus the host client's own per-call
|
|
9
|
+
* tool approval. The asymmetry is DELIBERATE — see slates-mcp/CLAUDE.md.
|
|
10
|
+
*/
|
|
11
|
+
export type AgentSurface = 'desktop' | 'mcp';
|
|
12
|
+
interface SkillIndexEntry {
|
|
13
|
+
name: string;
|
|
14
|
+
description: string;
|
|
15
|
+
}
|
|
16
|
+
/** Parse `name:`/`description:` out of each embedded skill's frontmatter. */
|
|
17
|
+
export declare function buildSkillIndex(): SkillIndexEntry[];
|
|
18
|
+
export declare const WORKING_METHOD: ReadonlyArray<Record<AgentSurface, string>>;
|
|
19
|
+
export declare const HARD_RULES: ReadonlyArray<Record<AgentSurface, string>>;
|
|
20
|
+
/**
|
|
21
|
+
* THE doctrine string for a surface. The desktop's whole system prompt and the
|
|
22
|
+
* MCP server's `instructions` are both exactly this — neither consumer adds
|
|
23
|
+
* doctrine prose of its own.
|
|
24
|
+
*
|
|
25
|
+
* SENTINEL: the literal below must appear in this workspace in exactly ONE
|
|
26
|
+
* file — this one. scripts/agent-surface-lockstep-check.mjs assembles the same
|
|
27
|
+
* string from its parts rather than writing it out, precisely so that grepping
|
|
28
|
+
* for it stays a meaningful question. It is how "no consumer grew a private
|
|
29
|
+
* copy of the doctrine" is proved rather than assumed.
|
|
30
|
+
* SLATES-AGENT-DOCTRINE-SSOT
|
|
31
|
+
*/
|
|
32
|
+
export declare function buildAgentDoctrine({ surface }: {
|
|
33
|
+
surface: AgentSurface;
|
|
34
|
+
}): string;
|
|
35
|
+
export {};
|
|
36
|
+
//# sourceMappingURL=agent-doctrine.d.ts.map
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// AGENT GUIDANCE SSOT — one doctrine, both surfaces.
|
|
3
|
+
//
|
|
4
|
+
// `ALL_OPERATIONS` has been the SSOT for what the agent CAN DO since the ops
|
|
5
|
+
// registry was built, and it held. The guidance layer — the working method,
|
|
6
|
+
// the hard rules, the guide index — never was, and it drifted the moment a
|
|
7
|
+
// second surface existed: a 37K-character system prompt reached ONLY the
|
|
8
|
+
// desktop Studio Agent, while the MCP server shipped no `instructions` at all.
|
|
9
|
+
// A Claude Code user got tool descriptions and nothing else: no working
|
|
10
|
+
// method, no REAL NUMBERS rule, no guide index.
|
|
11
|
+
//
|
|
12
|
+
// This module is that missing SSOT. Both surfaces compose from it:
|
|
13
|
+
// slate/src/main/studio-agent/context.ts buildSystemPrompt()
|
|
14
|
+
// slates-mcp/packages/mcp/src/server.ts `instructions` on the Server
|
|
15
|
+
//
|
|
16
|
+
// 🚨 RULES
|
|
17
|
+
//
|
|
18
|
+
// 1. ONE DOCTRINE STRING, ONE FILE. Any working-method or hard-rule prose
|
|
19
|
+
// living in a consumer after this is a bug. `context.ts` composes; it does
|
|
20
|
+
// not author.
|
|
21
|
+
// 2. SURFACE-AWARE, NOT SURFACE-FORKED. A step or rule forks ONLY where the
|
|
22
|
+
// mechanism genuinely differs (the desktop's `present_plan` gate does not
|
|
23
|
+
// exist on MCP; the desktop's loop auto-polls generation status and MCP's
|
|
24
|
+
// does not). Everything else is one string used by both. Two hand-maintained
|
|
25
|
+
// copies is the drift this file exists to kill — so `both()` is the default
|
|
26
|
+
// and `fork()` needs a reason you could defend in review.
|
|
27
|
+
// 3. PROSE IS EXPLANATION, NOT ENFORCEMENT. The two rules the agent actually
|
|
28
|
+
// breaks — "load the guide" and "quality-check with vision" — are enforced
|
|
29
|
+
// STRUCTURALLY in the op layer (generated banned-token lists inlined into
|
|
30
|
+
// the generate ops' descriptions, non-blocking warnings on the submitted
|
|
31
|
+
// prompt, and a review pointer on every generation result). A rule with no
|
|
32
|
+
// check is a suggestion, and an LLM is the least reliable enforcer you
|
|
33
|
+
// could pick. Do not "fix" a skipped rule by adding a sentence here.
|
|
34
|
+
// 4. NEVER BLOCK. PRODUCT_PHILOSOPHY.md → the Sandbox Doctrine: make state
|
|
35
|
+
// visible, never block. Enforcement means the guidance is already present
|
|
36
|
+
// and violations are reported back — never a refused call or a wizard step.
|
|
37
|
+
// 5. CACHE DISCIPLINE. This output is the desktop's cached prompt prefix and
|
|
38
|
+
// its byte-stability IS the cache mechanism (measured 97.4% steady-state
|
|
39
|
+
// cache hit). No timestamps, no balances, no project names, no per-session
|
|
40
|
+
// anything. Dynamic state reaches the brain through ops, never through here.
|
|
41
|
+
//
|
|
42
|
+
// SSOT DISCIPLINE: this module no longer renders model routing AT ALL. Routing
|
|
43
|
+
// rides `describeRouting()` on the four ops that choose a model, because that is
|
|
44
|
+
// where the choice is made; all the doctrine says is that the three KINDS are
|
|
45
|
+
// disjoint, which is the one part no single op can say. The guide index is
|
|
46
|
+
// DERIVED from SKILLS. Never hand-type either.
|
|
47
|
+
//
|
|
48
|
+
// Not exported from ./prompts on purpose: that subpath is bundled by the
|
|
49
|
+
// desktop RENDERER and must stay small and Node-free, and this module pulls in
|
|
50
|
+
// the whole embedded SKILLS record. Root barrel only.
|
|
51
|
+
// ============================================================
|
|
52
|
+
import { SKILLS } from '../skills/content.js';
|
|
53
|
+
import { VIDEO_MODELS, AUDIO_MODELS } from '../operations/index.js';
|
|
54
|
+
/** A line that is identical on both surfaces. The default. */
|
|
55
|
+
function both(text) {
|
|
56
|
+
return { desktop: text, mcp: text };
|
|
57
|
+
}
|
|
58
|
+
/** A line whose MECHANISM differs between surfaces. Needs a reason in-comment. */
|
|
59
|
+
function fork(desktop, mcp) {
|
|
60
|
+
return { desktop, mcp };
|
|
61
|
+
}
|
|
62
|
+
/** Parse `name:`/`description:` out of each embedded skill's frontmatter. */
|
|
63
|
+
export function buildSkillIndex() {
|
|
64
|
+
const entries = [];
|
|
65
|
+
for (const key of Object.keys(SKILLS).sort()) {
|
|
66
|
+
const content = SKILLS[key];
|
|
67
|
+
const fm = /^---\n([\s\S]*?)\n---/.exec(content);
|
|
68
|
+
let description = '';
|
|
69
|
+
if (fm) {
|
|
70
|
+
const m = /^description:\s*(.+)$/m.exec(fm[1]);
|
|
71
|
+
if (m)
|
|
72
|
+
description = m[1].trim().replace(/^['"]|['"]$/g, '');
|
|
73
|
+
}
|
|
74
|
+
entries.push({ name: key, description });
|
|
75
|
+
}
|
|
76
|
+
return entries;
|
|
77
|
+
}
|
|
78
|
+
// ── Preamble ───────────────────────────────────────────────────────
|
|
79
|
+
const PREAMBLE = fork(`You are the Slates Studio Agent — a production assistant living inside Slates, the AI video creation studio. You plan and execute video/image production runs by chaining the Slates tools: script → characters → images → videos → quality-check → regenerate, ending with assets in the user's project (and on the timeline when asked).`, `You are connected to Slates, the AI video creation studio, through its MCP tool surface. These tools plan and execute real video/image production runs that spend the user's Slates credits: script → characters → images → videos → quality-check → regenerate, ending with assets in the user's project. Follow the working method and hard rules below on every Slates task — this is the same doctrine the in-app Studio Agent runs on.`);
|
|
80
|
+
// ── The working method ─────────────────────────────────────────────
|
|
81
|
+
export const WORKING_METHOD = [
|
|
82
|
+
both(`1. UNDERSTAND the outcome the user wants. If intent is clear, act with sane defaults — don't interrogate. If genuinely ambiguous, batch every question into ONE message.`),
|
|
83
|
+
both(`2. ORIENT: call slates_get_workspace_state once at the start of a workflow. Work in the user's CURRENT project — this chat lives inside it. NEVER create a new project unless explicitly asked; if there's no current project, ask which to use.`),
|
|
84
|
+
both(`3. LOAD KNOWLEDGE ON DEMAND: before prompting any model or running a multi-step workflow, load the matching guide with slates_get_prompting_guide (index below). Only the guides the task needs, when it needs them.`),
|
|
85
|
+
// FORKED: `present_plan` is a loop-level DESKTOP tool, deliberately not in
|
|
86
|
+
// ALL_OPERATIONS, so MCP never sees it and has no plan gate at all. Its
|
|
87
|
+
// substitute is the per-op `requires_confirm` threshold plus the host
|
|
88
|
+
// client's own per-call approval UI. Describing the desktop gate to an MCP
|
|
89
|
+
// client would name a tool that does not exist.
|
|
90
|
+
fork(`4. PLAN + GET APPROVAL: before ANY generation, call present_plan with itemized credit costs (slates_estimate_generation_cost per step). One approval covers the plan's listed steps ONLY. Generation tools are rejected without an approved plan — and any user revision, question, or new instruction after an approval means you MUST re-present the plan BEFORE the next generation call (calling a generation op first just gets BLOCKED and wastes a turn).`, `4. PLAN + GET APPROVAL: before ANY generation, price every step with slates_estimate_generation_cost and put the itemized total in front of the user in ONE message, then wait for their answer. There is no present_plan tool on this surface — consent is per call: a generation over the confirm threshold returns requires_confirm, and confirm: true is only ever a relay of an explicit user OK for that exact spend. Never pass confirm: true to clear a gate the user has not seen.`),
|
|
91
|
+
fork(`5. EXECUTE: after approval, pass confirm: true (the approval IS the consent — never re-ask per step). Use background: true + status polling for video.`, `5. EXECUTE: run the approved steps, passing confirm: true only for the spend the user actually OK'd. Use background: true + status polling for video.`),
|
|
92
|
+
both(`6. QUALITY-CHECK: you have vision. After key generations, fetch the result (slates_get_asset_image / slates_get_asset_video_frames) and review it against the brief (slates-vision-feedback-loop). Fix real problems; don't churn credits polishing what works. Change ONE variable per regeneration.`),
|
|
93
|
+
both(`7. REPORT: when done, summarize what was made and where it landed. Concise and concrete.`),
|
|
94
|
+
];
|
|
95
|
+
// ── Hard rules ─────────────────────────────────────────────────────
|
|
96
|
+
//
|
|
97
|
+
// Each entry is a COMPLETE line including its leading "- ". MODEL ROUTING is
|
|
98
|
+
// the exception: it is multi-line and generated, and supplies its own bullet.
|
|
99
|
+
export const HARD_RULES = [
|
|
100
|
+
// FORKED: "outside an approved plan" names the desktop's code-level gate.
|
|
101
|
+
fork(`- COST DISCIPLINE (slates-cost-discipline): never fire a billable generation outside an approved plan. Estimate before you promise. Batch related generations into one plan.`, `- COST DISCIPLINE (slates-cost-discipline): never fire a billable generation the user has not agreed to. Estimate before you promise. Batch related generations into one quote so the user approves a total, not a drip.`),
|
|
102
|
+
both(`- CONTENT POLICY: before writing prompts involving real people/celebrities, minors, brands/logos, weapons, or gore, load slates-content-policy and build the scene safe from the first word. If a provider rejects (e.g. real-face detection), explain in plain language — refunds for provider rejections are automatic.`),
|
|
103
|
+
both(`- PROMPT IS LAW (reference doctrine): references are cited inline by name and image number ("Marcus (images 1 and 2)"); the prompt text leads. Never write role-essays about what each reference is "for".`),
|
|
104
|
+
both(`- NAMES AND DESCRIPTIONS ARE THE USER'S UI, NOT YOUR NOTEPAD. A project, storyboard or shot name is rendered at the top of the user's screen at all times. Name it after the piece ("Kaiju selfie"), never after your process, and NEVER append your own status or housekeeping — no "shot list", no "v2", no "Written from IMG-A172 and IMG-A182", no "Delete freely", no "scratch". Leave the description empty unless the user gave you one worth keeping: a note-to-self at the top of the screen reads as part of the product, and the user has to look at it every day. Say that kind of thing in your reply to them instead.`),
|
|
105
|
+
both(`- RESOLUTION DEFAULT IS UNIFORM: 1080p on the best available video model. Do not crank resolution the user didn't ask for.`),
|
|
106
|
+
// ⛔ THE MODEL ROUTING BLOCK IS GONE FROM HERE, DELIBERATELY (2026-08-30).
|
|
107
|
+
//
|
|
108
|
+
// It was 17,700 characters — 47% of the whole doctrine — and every lane of it
|
|
109
|
+
// now rides the op that actually makes the decision: describeRouting('image')
|
|
110
|
+
// in slates_generate_image, ('video','generate') in slates_generate_video's
|
|
111
|
+
// model param, ('video','edit') in slates_edit_video, ('audio') in
|
|
112
|
+
// slates_generate_audio. An op description is always in context on both
|
|
113
|
+
// surfaces, so nothing was lost in reach — it moved next to the choice.
|
|
114
|
+
//
|
|
115
|
+
// The measured argument for doing this: the never-use token list went from 0%
|
|
116
|
+
// to 94% compliance when it moved from prose into an op description, while
|
|
117
|
+
// the same guidance in prose moved nothing. Placement beats presence.
|
|
118
|
+
// The old `buildModelRouting()` renderer was DELETED rather than left
|
|
119
|
+
// exported "in case": an export nothing calls is an export nothing keeps
|
|
120
|
+
// honest. `describeRouting()` in model-facts.ts is the renderer now.
|
|
121
|
+
both(`- MODEL KINDS: image, video and audio models are disjoint — no image model makes a video, no video model makes a standalone image, no image or video model makes audio. Which SEAT to pick inside a kind is on each generate op's own \`model\` description, and the full table is the slates-model-selection skill.`),
|
|
122
|
+
both(`- ASSET CODES + STALENESS: every asset carries a badge code (IMG-A12 / VID-V3 / AUD-S1, top-left of its gallery card); speak about assets by code + label. Asset lists go STALE — the user creates assets in the Slates UI mid-conversation. When the user names a code you have NOT seen in a tool result this session, resolve it with slates_list_assets (use the search filter) BEFORE using it. NEVER guess an asset id or reuse a nearby UUID — pass the exact id a tool returned for that exact code. A wrong start frame burns real credits.`),
|
|
123
|
+
both(`- CREDITS ONLY: every generation you drive bills Slates credits (your tool calls enforce this). Never suggest BYOK keys for agent work.`),
|
|
124
|
+
both(`- Do not invent tools, asset ids, or credit prices. If a tool errors, read the error and fix that exact issue; don't repeat the same call unchanged and never switch models to route around a parameter mistake.`),
|
|
125
|
+
// FORKED: only the desktop loop auto-polls generation status
|
|
126
|
+
// (loop.ts → autoPollUntilTerminal). Telling an MCP client "the app keeps
|
|
127
|
+
// polling for you" would strand a job nobody is watching.
|
|
128
|
+
fork(`- TOKEN DISCIPLINE: every tool result you read costs the user money. generate_* results already return the new asset ids/codes — NEVER call slates_list_assets to find something you just created. When you do list, pass search/type/limit filters. Poll generation status ONCE with waitSeconds: 45 — the app keeps auto-polling for you and returns the terminal status; do not narrate between polls or call status in a loop. Use slates_estimate_generation_cost for known models instead of dumping the registry; if you need the registry, pass a filter.`, `- TOKEN DISCIPLINE: every tool result you read costs the user money. generate_* results already return the new asset ids/codes — NEVER call slates_list_assets to find something you just created. When you do list, pass search/type/limit filters. Poll generation status with waitSeconds: 45 — one long poll per check, no tight loops and no narration between polls. Use slates_estimate_generation_cost for known models instead of dumping the registry; if you need the registry, pass a filter.`),
|
|
129
|
+
// FORKED: the desktop app renders orchestration cost itself; there is no such
|
|
130
|
+
// display on MCP, so that clause would point at nothing. The MCP variant
|
|
131
|
+
// spends the saved words on the claim an MCP client is most likely to
|
|
132
|
+
// fabricate: what the generation LOOKS like.
|
|
133
|
+
fork(`- REAL NUMBERS ONLY — NEVER approximate, estimate, or recall any figure. Every cost, balance, count, or duration you state must be copied verbatim from a tool result IN THIS SESSION: generation costs come from the cost_credits field in generate/status results (whole credits); the balance comes from a fresh slates_get_credit_balance call at summary time (never arithmetic you did yourself); orchestration cost is displayed by the app automatically — NEVER state or estimate it. No "approx", no "~", no rounded guesses. A number you cannot point to in a tool result is a number you do not say.`, `- REAL NUMBERS ONLY — NEVER approximate, estimate, or recall any figure. Every cost, balance, count, or duration you state must be copied verbatim from a tool result IN THIS SESSION: generation costs come from the cost_credits field in generate/status results (whole credits); the balance comes from a fresh slates_get_credit_balance call at summary time (never arithmetic you did yourself). No "approx", no "~", no rounded guesses. A number you cannot point to in a tool result is a number you do not say. The same rule covers what you SAW: never describe how a generation looks unless you fetched it this session with slates_get_asset_image / slates_get_asset_video_frames.`),
|
|
134
|
+
both(`- MODEL IDS ARE FIXED: slates_generate_video takes exactly ${VIDEO_MODELS.join(' | ')}, with duration + videoResolution as separate params. slates_generate_audio takes exactly ${AUDIO_MODELS.join(' | ')}. Registry entries like "kling-v3-standard-8s" or "seed-audio-15s" are billing keys, not model ids.`),
|
|
135
|
+
// ONE LINE, NOT A SECTION. The counts themselves ride the op RESULT, where
|
|
136
|
+
// the agent is already reading — measured: a rule inlined into an op moved
|
|
137
|
+
// compliance 0/8 → 30/32, while the same guidance behind a guide fetch sat at
|
|
138
|
+
// 13% before and after. This sentence exists only to say the numbers are
|
|
139
|
+
// there and must be looked at; the craft is the skill.
|
|
140
|
+
both(`- READ THE VARIETY COUNTS BEFORE FIRING A SET: slates_list_shots and slates_get_storyboard_with_frames return the distribution (shot sizes, camera moves, runs of three or more) with the rows. If one bucket is the plurality, fix the board before you spend — slates-shot-variety is the craft.`),
|
|
141
|
+
both(`- AUDIO LENGTH IS PROMPT-DRIVEN ON SEED AUDIO: it has no duration parameter, so the durationSeconds you pass is written into the prompt AND is what the user is charged, whatever comes back. Choose it deliberately and load slates-prompting-seed-audio before the first call.`),
|
|
142
|
+
];
|
|
143
|
+
// ── Surface-only footers ───────────────────────────────────────────
|
|
144
|
+
//
|
|
145
|
+
// MCP clients have no Slates chrome and may have no skill FILES installed
|
|
146
|
+
// (`slates install-skills` covers Claude Code; Claude Desktop, Cursor and
|
|
147
|
+
// Codex have no equivalent mechanism). The desktop always has the embedded
|
|
148
|
+
// record, so this paragraph would be dead weight in its cached prefix.
|
|
149
|
+
const MCP_FOOTER = `## Working without skill files
|
|
150
|
+
|
|
151
|
+
If slates-* skill files are not installed in this client, every one of them is still reachable as data: call slates_get_prompting_guide with the skill name (or a model id — it resolves aliases). The guide index above is the complete list. "No skill installed" is never a reason to prompt a model blind.
|
|
152
|
+
`;
|
|
153
|
+
/**
|
|
154
|
+
* THE doctrine string for a surface. The desktop's whole system prompt and the
|
|
155
|
+
* MCP server's `instructions` are both exactly this — neither consumer adds
|
|
156
|
+
* doctrine prose of its own.
|
|
157
|
+
*
|
|
158
|
+
* SENTINEL: the literal below must appear in this workspace in exactly ONE
|
|
159
|
+
* file — this one. scripts/agent-surface-lockstep-check.mjs assembles the same
|
|
160
|
+
* string from its parts rather than writing it out, precisely so that grepping
|
|
161
|
+
* for it stays a meaningful question. It is how "no consumer grew a private
|
|
162
|
+
* copy of the doctrine" is proved rather than assumed.
|
|
163
|
+
* SLATES-AGENT-DOCTRINE-SSOT
|
|
164
|
+
*/
|
|
165
|
+
export function buildAgentDoctrine({ surface }) {
|
|
166
|
+
// COMPRESSED on purpose (2026-08-30). The full frontmatter descriptions were
|
|
167
|
+
// 14,556 characters — 38% of the doctrine — and they are DISCOVERY blurbs,
|
|
168
|
+
// written in Claude Code's "Use when X, or when Y" style for a fuzzy matcher
|
|
169
|
+
// choosing among skills. Here the mapping is close to deterministic:
|
|
170
|
+
// `resolveGuideTopic()` turns a model id into the right guide in code.
|
|
171
|
+
//
|
|
172
|
+
// So the per-model guides are listed as bare names (the name IS the
|
|
173
|
+
// description) and everything else keeps its FIRST SENTENCE, which is the
|
|
174
|
+
// part that says when to reach for it. The complete list with full
|
|
175
|
+
// descriptions is one `slates_get_prompting_guide` call away.
|
|
176
|
+
const entries = buildSkillIndex();
|
|
177
|
+
const firstSentence = (d) => {
|
|
178
|
+
const m = /^(.*?[.!?])(\s|$)/.exec(d.trim());
|
|
179
|
+
return (m ? m[1] : d.trim()).slice(0, 180);
|
|
180
|
+
};
|
|
181
|
+
const perModel = entries.filter((s) => s.name.startsWith('slates-prompting-'));
|
|
182
|
+
const rest = entries.filter((s) => !s.name.startsWith('slates-prompting-'));
|
|
183
|
+
const skillIndex = rest.map((s) => `- ${s.name}: ${firstSentence(s.description)}`).join('\n') +
|
|
184
|
+
`\n- PER-MODEL PROMPTING GUIDES — pass the model id, or the name: ` +
|
|
185
|
+
perModel.map((s) => s.name).join(', ');
|
|
186
|
+
return `${PREAMBLE[surface]}
|
|
187
|
+
|
|
188
|
+
## How you work
|
|
189
|
+
|
|
190
|
+
${WORKING_METHOD.map((s) => s[surface]).join('\n')}
|
|
191
|
+
|
|
192
|
+
## Hard rules
|
|
193
|
+
|
|
194
|
+
${HARD_RULES.map((r) => r[surface]).join('\n')}
|
|
195
|
+
|
|
196
|
+
## Guide index (load via slates_get_prompting_guide)
|
|
197
|
+
|
|
198
|
+
${skillIndex}
|
|
199
|
+
${surface === 'mcp' ? `\n${MCP_FOOTER}` : ''}`;
|
|
200
|
+
}
|
|
201
|
+
//# sourceMappingURL=agent-doctrine.js.map
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A readable label for a generated asset, from its prompt.
|
|
3
|
+
*
|
|
4
|
+
* Strips a reference preamble, then takes the first CLAUSE rather than a fixed
|
|
5
|
+
* character slice — so the label ends on a word instead of mid-syllable.
|
|
6
|
+
* Returns null for a prompt with nothing usable in it; the caller decides what
|
|
7
|
+
* to show instead (a Shot name, "Untitled").
|
|
8
|
+
*/
|
|
9
|
+
export declare function labelFromPrompt(prompt: string | null | undefined): string | null;
|
|
10
|
+
/**
|
|
11
|
+
* The caption for one asset row, in priority order: the label it was given, the
|
|
12
|
+
* Shot it came from, then the prompt reduced to a clause.
|
|
13
|
+
*
|
|
14
|
+
* The Shot name sits second because an author who named a Shot chose that word
|
|
15
|
+
* on purpose, and it is the word on the row in the storyboard — so the gallery
|
|
16
|
+
* and the document call the same thing the same thing.
|
|
17
|
+
*/
|
|
18
|
+
export declare function assetCaption(a: {
|
|
19
|
+
label?: string | null;
|
|
20
|
+
shotName?: string | null;
|
|
21
|
+
prompt?: string | null;
|
|
22
|
+
}): string;
|
|
23
|
+
//# sourceMappingURL=asset-label.d.ts.map
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// ASSET LABELS — what a generated thing is CALLED in a list.
|
|
3
|
+
//
|
|
4
|
+
// 🚨 THE FIRST 40 CHARACTERS OF A PROMPT IS NOT A LABEL, and on
|
|
5
|
+
// reference-driven work it is actively misleading: every one of those prompts
|
|
6
|
+
// opens with the same composed preamble, so a whole gallery reads
|
|
7
|
+
// "Reference image 1 is a character ide" — the same string on every row, which
|
|
8
|
+
// is worse than no label because it looks like information.
|
|
9
|
+
//
|
|
10
|
+
// One rule, two consumers: the op surface's `compactAsset` (what an agent reads
|
|
11
|
+
// back from `slates_list_assets`) and the desktop gallery caption. A second
|
|
12
|
+
// copy of this would drift the way every other hand-mirrored rule in this
|
|
13
|
+
// workspace has.
|
|
14
|
+
//
|
|
15
|
+
// Dependency-free LEAF, same as `shot-grammar`: the desktop RENDERER imports it
|
|
16
|
+
// directly, so it may never reach for `node:` anything.
|
|
17
|
+
// ============================================================
|
|
18
|
+
/**
|
|
19
|
+
* A composed prompt's reference preamble.
|
|
20
|
+
*
|
|
21
|
+
* The composer writes "Reference image 1 is a character identity sheet." — so
|
|
22
|
+
* the optional leading "Reference" has to be matched SEPARATELY from the noun,
|
|
23
|
+
* or the pattern only catches the bare "Image 1 is …" form and every real
|
|
24
|
+
* composed prompt walks straight past it. (It did, until the labels were
|
|
25
|
+
* exercised on actual prompts.)
|
|
26
|
+
*/
|
|
27
|
+
const REFERENCE_PREAMBLE_RE = /^\s*(?:reference\s*)?(?:image|video|audio|clip)?\s*\d*\s*is\b[^.;]*[.;]\s*/i;
|
|
28
|
+
/** A sentence boundary always ends the label. */
|
|
29
|
+
const SENTENCE_BOUNDARY_RE = /(?<=[.;:])\s/;
|
|
30
|
+
/**
|
|
31
|
+
* An em-dash ends it too — but only once there is enough label to be worth
|
|
32
|
+
* keeping. Composed prompts join a subject to its treatment with one, and the
|
|
33
|
+
* subject alone is the label; a hand-written "wide shot — the barn at dawn"
|
|
34
|
+
* would otherwise be cut down to "wide shot", which names nothing.
|
|
35
|
+
*/
|
|
36
|
+
const DASH_BOUNDARY_RE = /\s—\s/;
|
|
37
|
+
const MIN_BEFORE_DASH = 25;
|
|
38
|
+
const MAX = 60;
|
|
39
|
+
/**
|
|
40
|
+
* A readable label for a generated asset, from its prompt.
|
|
41
|
+
*
|
|
42
|
+
* Strips a reference preamble, then takes the first CLAUSE rather than a fixed
|
|
43
|
+
* character slice — so the label ends on a word instead of mid-syllable.
|
|
44
|
+
* Returns null for a prompt with nothing usable in it; the caller decides what
|
|
45
|
+
* to show instead (a Shot name, "Untitled").
|
|
46
|
+
*/
|
|
47
|
+
export function labelFromPrompt(prompt) {
|
|
48
|
+
if (!prompt)
|
|
49
|
+
return null;
|
|
50
|
+
const body = (prompt.replace(REFERENCE_PREAMBLE_RE, '').trim() || prompt).trim();
|
|
51
|
+
let clause = body.split(SENTENCE_BOUNDARY_RE)[0].trim();
|
|
52
|
+
const dash = clause.split(DASH_BOUNDARY_RE)[0].trim();
|
|
53
|
+
if (dash.length >= MIN_BEFORE_DASH)
|
|
54
|
+
clause = dash;
|
|
55
|
+
if (!clause)
|
|
56
|
+
return null;
|
|
57
|
+
return clause.length > MAX ? `${clause.slice(0, MAX - 1).trimEnd()}…` : clause;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* The caption for one asset row, in priority order: the label it was given, the
|
|
61
|
+
* Shot it came from, then the prompt reduced to a clause.
|
|
62
|
+
*
|
|
63
|
+
* The Shot name sits second because an author who named a Shot chose that word
|
|
64
|
+
* on purpose, and it is the word on the row in the storyboard — so the gallery
|
|
65
|
+
* and the document call the same thing the same thing.
|
|
66
|
+
*/
|
|
67
|
+
export function assetCaption(a) {
|
|
68
|
+
return a.label?.trim() || a.shotName?.trim() || labelFromPrompt(a.prompt) || 'Untitled';
|
|
69
|
+
}
|
|
70
|
+
//# sourceMappingURL=asset-label.js.map
|