editorbrain 0.28.2 → 0.29.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,246 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ // packages/cli/src/sfx.ts
4
+ import { readdir as readdir2, lstat as lstat3 } from "node:fs/promises";
5
+ import { join as join2, extname as extname2, basename } from "node:path";
6
+
7
+ // packages/core/src/onboarding-guide.ts
8
+ var onboardingStudioSteps = [
9
+ ["Empecemos", "Un editor que entiende tu mundo.", "Tu marca, tu gusto y tu forma de trabajar. Sin empezar de cero en cada video."],
10
+ ["Empecemos", "Tus ideas. Tu agente. Tu criterio.", "EditorBrain aporta contexto y referencias. Claude o Codex adapta, edita y revisa."],
11
+ ["Empecemos", "Trae un pedacito de tu mundo.", "Puedes usar un clip para probar c\xF3mo se ven los textos. Tu web y tus redes se guardan en el siguiente paso."],
12
+ ["Empecemos", "Que se vea. Que se sienta tuyo.", "Define tu identidad, tus colores y tus tipograf\xEDas. O recupera una marca que ya tengas."],
13
+ ["Empecemos", "\xBFQu\xE9 quieres contar?", "A c\xE1mara, sin aparecer, con im\xE1genes o con animaciones: tu contenido determina la edici\xF3n."],
14
+ ["Tu gusto", "Elige lo que te hace mirar.", "Hasta tres direcciones. Nos interesa c\xF3mo cuentan la historia, no copiar el tema ni la paleta."],
15
+ ["Tu gusto", "Movimiento con intenci\xF3n.", "Explora c\xF3mo explicar una idea. Tu agente adaptar\xE1 las animaciones a cada escena."],
16
+ ["Tu gusto", "Tu universo visual.", "Fotograf\xEDa, ilustraci\xF3n, collage\u2026 Combina lo que te guste o deja que tu agente decida."],
17
+ ["Tu gusto", "Encuentra tu ritmo.", "Una misma idea puede respirar o ir a toda velocidad. Prueba la diferencia."],
18
+ ["Tus detalles", "Los primeros segundos importan.", "Elige c\xF3mo te gusta abrir. El guion y el material mandan en cada video."],
19
+ ["Tus detalles", "Palabras que se ven.", "Prueba subt\xEDtulos, \xE9nfasis y posiciones antes de elegir."],
20
+ ["Tus detalles", "Tu voz. Tu cierre.", "Define el sonido y qu\xE9 quieres que haga quien te ve al terminar."],
21
+ ["Tus detalles", "Tus l\xEDmites tambi\xE9n cuentan.", "Lo que nunca deber\xEDa aparecer, y lo que prefieres decidir despu\xE9s."],
22
+ ["Listo para editar", "Antes del primer video.", "Tres cosas claras para empezar con las expectativas correctas."],
23
+ ["Listo para editar", "Tu caja de herramientas.", "Declara los servicios que tienes. Tu agente comprobar\xE1 el acceso antes de usarlos."],
24
+ ["Listo para editar", "As\xED empieza tu cerebro.", "Tus elecciones ya son documentos. Puedes revisar una propuesta de IA antes de incorporarla."],
25
+ ["Listo para editar", "P\xEDdelo como lo imaginas.", "Describe tu objetivo y entrega tu material. Elige cu\xE1ndo quieres revisar."],
26
+ ["Listo para editar", "Ll\xE9valo a tu agente.", "Instala y autoriza EditorBrain con una indicaci\xF3n. Una conexi\xF3n real requiere tu consentimiento."],
27
+ ["Listo para editar", "Tu siguiente idea empieza aqu\xED.", "Tu marca est\xE1 preparada. Crea un proyecto y entrega el material a tu agente."]
28
+ ];
29
+ var localOnboardingSteps = [
30
+ ["Tu contenido", ...onboardingStudioSteps[4].slice(1)],
31
+ ["Tu marca", ...onboardingStudioSteps[3].slice(1)],
32
+ ["Tus gustos", "Elige lo que te hace mirar.", "Explora im\xE1genes y movimiento. Tu agente adapta estas referencias a cada escena."],
33
+ ["Tu edici\xF3n", "Encuentra tu ritmo.", "Decide cu\xE1nto apoyo visual, texto y subt\xEDtulos quieres. Puedes dejarlo a criterio de tu agente."],
34
+ ["Tu sonido", "Tu voz. Tu cierre.", "Voz, m\xFAsica y efectos se eligen por separado. La cantidad de efectos no es su volumen."],
35
+ ["Tu cerebro", "As\xED empieza tu cerebro.", "Revisa tus decisiones antes de guardarlas. Puedes modificarlas despu\xE9s."]
36
+ ];
37
+ function getOnboardingGuide() {
38
+ return {
39
+ schemaVersion: 1,
40
+ web: { url: "https://editorbrain.com/app/onboarding", description: "Recorrido visual completo: preferencias, integraciones personales y conexi\xF3n con tu agente. Requiere tu sesi\xF3n web y elegir el espacio correcto." },
41
+ local: {
42
+ description: "Edici\xF3n local de preferencias en seis grupos, con el dise\xF1o del estudio. Funciona sin cuenta; no simula conexiones, integraciones ni an\xE1lisis de IA.",
43
+ steps: localOnboardingSteps.map(([label]) => label),
44
+ commands: ["editorbrain onboarding create --brand <brand-id> --project <new-project-id> --dir <folder>", "editorbrain onboarding serve --session <returned-session> --dir <folder>"],
45
+ completion: "Lee las respuestas; valida y aplica con onboarding apply. Sincroniza expl\xEDcitamente con onboarding sync despu\xE9s de autenticarte. Conserva versiones y resuelve conflictos; nunca sobrescribas una marca existente para completar el onboarding."
46
+ },
47
+ agent: [
48
+ "Ofrece la web o el artefacto local seg\xFAn el entorno del usuario; no le pidas rellenar ambos.",
49
+ "Descubre los comandos disponibles con editorbrain help antes de ejecutarlos. Lee references/onboarding.md solo al configurar o modificar preferencias.",
50
+ "Si la marca ya existe, recupera sus archivos actuales antes de crear el artefacto. Prefiere actualizaciones parciales y conserva lo que no cambi\xF3.",
51
+ "MCP no puede abrir un servidor en la computadora del usuario. Sin terminal local, enlaza el recorrido web; no inventes una URL localhost.",
52
+ "Al terminar, consulta check_workflow con el proyecto seleccionado y carga sus restricciones actuales. No supongas que terminar una p\xE1gina conect\xF3 las herramientas.",
53
+ "El analista de marca es opcional: requiere petici\xF3n expl\xEDcita, documentos sincronizados y revisi\xF3n de su propuesta. No se ejecuta al abrir esta gu\xEDa."
54
+ ]
55
+ };
56
+ }
57
+
58
+ // packages/core/src/audio-production.ts
59
+ function record(value, name, keys2) {
60
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`${name} must be an object.`);
61
+ const result2 = value;
62
+ if (Object.keys(result2).some((key) => !keys2.includes(key))) throw new Error(`${name} contains an unsupported field.`);
63
+ return result2;
64
+ }
65
+ function text(value, name, max = 200) {
66
+ if (typeof value !== "string" || !value.trim() || value.length > max || /[\x00-\x1f\x7f]/.test(value)) throw new Error(`${name} must be nonempty text of at most ${max} characters.`);
67
+ return value.trim();
68
+ }
69
+ function validateAudioLoudnessTarget(value) {
70
+ const input = record(value, "loudness", ["targetIntegratedLufs", "toleranceLu", "maxTruePeakDbtp"]);
71
+ for (const field of ["targetIntegratedLufs", "toleranceLu", "maxTruePeakDbtp"]) {
72
+ const number2 = input[field];
73
+ if (typeof number2 !== "number" || !Number.isFinite(number2) || (field === "toleranceLu" ? number2 < 0 || number2 > 150 : number2 < -150 || number2 > 24)) throw new Error(`loudness.${field} must be a finite measurement target within the supported range.`);
74
+ }
75
+ return { targetIntegratedLufs: input.targetIntegratedLufs, toleranceLu: input.toleranceLu, maxTruePeakDbtp: input.maxTruePeakDbtp };
76
+ }
77
+ function validateAudioProductionPreferences(value) {
78
+ const input = record(value, "audioProduction", ["schemaVersion", "voice", "music", "sfx", "loudness"]);
79
+ if (input.schemaVersion !== 1) throw new Error("audioProduction.schemaVersion must be 1.");
80
+ const result2 = { schemaVersion: 1 };
81
+ if (input.voice === null) result2.voice = null;
82
+ else if (input.voice !== void 0) {
83
+ const voice = record(input.voice, "voice", ["mode", "provider", "voiceId"]);
84
+ if (voice.mode === "original" || voice.mode === "none") {
85
+ if (voice.provider !== void 0 || voice.voiceId !== void 0) throw new Error("Original/no narration does not select a generated voice.");
86
+ result2.voice = { mode: voice.mode };
87
+ } else if (voice.mode === "catalog" || voice.mode === "cloned") {
88
+ result2.voice = { mode: voice.mode, provider: text(voice.provider, "voice.provider"), voiceId: text(voice.voiceId, "voice.voiceId") };
89
+ } else throw new Error("voice.mode must explicitly select original, none, catalog or cloned.");
90
+ }
91
+ if (input.music === null) result2.music = null;
92
+ else if (input.music !== void 0) {
93
+ const music = record(input.music, "music", ["intensity"]);
94
+ result2.music = {};
95
+ if (music.intensity !== void 0) {
96
+ if (music.intensity !== null && (typeof music.intensity !== "string" || !["none", "subtle", "present"].includes(music.intensity))) throw new Error("music.intensity must be none, subtle, present or null.");
97
+ result2.music.intensity = music.intensity;
98
+ }
99
+ }
100
+ if (input.sfx === null) result2.sfx = null;
101
+ else if (input.sfx !== void 0) {
102
+ const sfx = record(input.sfx, "sfx", ["library", "generation", "density", "hookWithoutEffects", "character"]);
103
+ result2.sfx = {};
104
+ if (sfx.library === null) result2.sfx.library = null;
105
+ else if (sfx.library !== void 0) {
106
+ const library = record(sfx.library, "sfx.library", ["bankId"]);
107
+ const bankId = text(library.bankId, "sfx.library.bankId");
108
+ if (!/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/.test(bankId)) throw new Error("sfx.library.bankId must be a portable identifier, not a path.");
109
+ result2.sfx.library = { bankId };
110
+ }
111
+ if (sfx.generation !== void 0) {
112
+ if (sfx.generation !== null && (typeof sfx.generation !== "string" || !["allowed", "forbidden"].includes(sfx.generation))) throw new Error("sfx.generation must be allowed, forbidden or null.");
113
+ result2.sfx.generation = sfx.generation;
114
+ }
115
+ if (sfx.density !== void 0) {
116
+ if (sfx.density !== null && (typeof sfx.density !== "string" || !["none", "sparse", "moderate", "dense"].includes(sfx.density))) throw new Error("sfx.density must be none, sparse, moderate, dense or null.");
117
+ result2.sfx.density = sfx.density;
118
+ }
119
+ if (sfx.character !== void 0) {
120
+ if (sfx.character !== null && !["organic", "digital", "cinematic", "mixed"].includes(sfx.character)) throw new Error("Invalid SFX character.");
121
+ result2.sfx.character = sfx.character;
122
+ }
123
+ if (sfx.hookWithoutEffects !== void 0) {
124
+ if (sfx.hookWithoutEffects !== null && typeof sfx.hookWithoutEffects !== "boolean") throw new Error("sfx.hookWithoutEffects must be a boolean or null.");
125
+ result2.sfx.hookWithoutEffects = sfx.hookWithoutEffects;
126
+ }
127
+ }
128
+ if (input.loudness !== void 0) result2.loudness = input.loudness === null ? null : validateAudioLoudnessTarget(input.loudness);
129
+ return result2;
130
+ }
131
+ function resolveAudioProductionPreferences(workspace, project) {
132
+ const base = workspace === void 0 ? { schemaVersion: 1 } : validateAudioProductionPreferences(workspace);
133
+ const override = project === void 0 ? { schemaVersion: 1 } : validateAudioProductionPreferences(project);
134
+ return { ...base, ...override, ...override.music ? { music: { ...base.music, ...override.music } } : {}, ...override.sfx ? { sfx: { ...base.sfx, ...override.sfx } } : {} };
135
+ }
136
+ function applyEditorialAudioConstraints(preferences, profile) {
137
+ let effective = preferences === void 0 ? void 0 : validateAudioProductionPreferences(preferences);
138
+ const warnings = [];
139
+ const withoutMusic = profile.sound === "Sin m\xFAsica ni efectos";
140
+ const withoutSfx = withoutMusic || profile.avoid?.includes("Sin efectos de sonido");
141
+ if (withoutMusic) {
142
+ if (effective?.music?.intensity && effective.music.intensity !== "none") warnings.push("Music is disabled by the saved editorial restriction; the original audio preferences remain unchanged.");
143
+ effective = { ...effective, schemaVersion: 1, music: { ...effective?.music, intensity: "none" } };
144
+ }
145
+ if (withoutSfx) {
146
+ if (effective?.sfx?.density && effective.sfx.density !== "none") warnings.push("SFX are disabled by the saved editorial restriction; the original audio preferences remain unchanged.");
147
+ effective = { ...effective, schemaVersion: 1, sfx: { ...effective?.sfx, density: "none", generation: "forbidden" } };
148
+ }
149
+ const narration = profile.editorialDelivery?.narration;
150
+ const voice = effective?.voice;
151
+ if (narration === "original" || narration === "none") {
152
+ if (voice && voice.mode !== narration) warnings.push(`Narration preference ${narration} conflicts with audio voice ${voice.mode}. Do not generate narration; confirm any deliberate project exception with the current request.`);
153
+ effective = { ...effective, schemaVersion: 1, voice: { mode: narration } };
154
+ } else if (narration === "voiceover" && voice && (voice.mode === "original" || voice.mode === "none")) {
155
+ warnings.push(`Voiceover preference conflicts with audio voice ${voice.mode}. Resolve narration with the current request; do not infer a paid provider or voice.`);
156
+ }
157
+ return { ...effective ? { preferences: effective } : {}, warnings };
158
+ }
159
+
160
+ // packages/core/src/sfx-library.ts
161
+ var sfxCharacters = ["organic", "digital", "cinematic", "mixed"];
162
+ var hash = /^[a-f0-9]{64}$/;
163
+ function sfxBankPath(bankId) {
164
+ if (!/^[a-z0-9][a-z0-9_-]{0,79}$/.test(bankId)) throw new Error("SFX bank ID must use lowercase letters, numbers, _ or - (max 80).");
165
+ return `.editorbrain/sfx/${bankId}/catalog.json`;
166
+ }
167
+ function object(value, keys2) {
168
+ if (!value || typeof value !== "object" || Array.isArray(value) || Object.keys(value).some((k) => !keys2.includes(k))) throw new Error("Unsupported SFX catalog fields. Local paths stay in .editorbrain-local/.");
169
+ return value;
170
+ }
171
+ function text2(value, max) {
172
+ if (typeof value !== "string" || !value.trim() || value.length > max || /[\x00-\x1f\x7f]/.test(value)) throw new Error("Invalid SFX text.");
173
+ return value.trim();
174
+ }
175
+ function validateSfxBank(value) {
176
+ const v = object(value, ["schemaVersion", "bankId", "entries"]);
177
+ if (v.schemaVersion !== 1 || typeof v.bankId !== "string") throw new Error("Invalid SFX bank.");
178
+ sfxBankPath(v.bankId);
179
+ if (!Array.isArray(v.entries) || v.entries.length > 100) throw new Error("A SFX bank supports at most 100 entries. Split larger collections by purpose.");
180
+ const ids = /* @__PURE__ */ new Set();
181
+ const entries = v.entries.map((raw) => {
182
+ const e = object(raw, ["id", "name", "tags", "character", "durationSeconds", "sha256", "assetId", "license", "audition"]);
183
+ if (typeof e.id !== "string" || !/^[a-z0-9][a-z0-9_-]{0,79}$/.test(e.id) || ids.has(e.id)) throw new Error("Invalid or duplicate SFX ID.");
184
+ ids.add(e.id);
185
+ if (typeof e.sha256 !== "string" || !hash.test(e.sha256) || e.assetId !== void 0 && (typeof e.assetId !== "string" || !hash.test(e.assetId))) throw new Error("SFX must pin a SHA-256 and optional owned asset ID.");
186
+ if (!Number.isFinite(e.durationSeconds) || e.durationSeconds <= 0 || e.durationSeconds > 600) throw new Error("SFX duration must be measured, between 0 and 600 seconds.");
187
+ if (!Array.isArray(e.tags) || e.tags.length > 24) throw new Error("Use up to 24 SFX tags.");
188
+ if (e.character !== void 0 && !sfxCharacters.includes(e.character)) throw new Error("Unknown SFX character.");
189
+ const license = object(e.license, ["kind", "note"]);
190
+ if (!["owned", "cc0", "licensed", "unverified"].includes(license.kind) || !["unreviewed", "reviewed"].includes(e.audition)) throw new Error("Declare rights and listening status separately.");
191
+ return { id: e.id, name: text2(e.name, 180), tags: e.tags.map((t) => text2(t, 60)), ...e.character ? { character: e.character } : {}, durationSeconds: e.durationSeconds, sha256: e.sha256, ...e.assetId ? { assetId: e.assetId } : {}, license: { kind: license.kind, note: text2(license.note, 500) }, audition: e.audition };
192
+ });
193
+ return { schemaVersion: 1, bankId: v.bankId, entries };
194
+ }
195
+ function sfxPolicy(audio, sceneRole = "body") {
196
+ if (!["hook", "body", "cta"].includes(sceneRole)) throw new Error("Choose sceneRole hook, body or cta.");
197
+ const sfx = validateAudioProductionPreferences(audio ?? { schemaVersion: 1 }).sfx;
198
+ const blocked = sfx?.density === "none" || sceneRole === "hook" && sfx?.hookWithoutEffects === true;
199
+ return {
200
+ enabled: !blocked,
201
+ density: sfx?.density ?? "adaptive",
202
+ character: sfx?.character ?? "adaptive",
203
+ hookWithoutEffects: sfx?.hookWithoutEffects ?? false,
204
+ bankId: sfx?.library?.bankId ?? null,
205
+ reason: blocked ? sfx?.density === "none" ? "SFX disabled by effective preferences." : "Keep this hook free of added SFX." : null,
206
+ guidance: "Density is frequency, not volume or a quota. Choose by scene purpose, audition first, synchronize the accent, protect speech, vary repeated sounds and inspect the final mix. No automatic generation or spending."
207
+ };
208
+ }
209
+ var groups = [["click", "clic", "boton", "button", "interfaz", "interface"], ["whoosh", "swish", "transicion", "transition", "barrido"], ["paper", "papel", "pagina", "page"], ["tick", "reloj", "clock", "tictac"], ["impact", "impacto", "golpe", "hit"], ["pop", "popping", "burbuja"], ["bell", "campana", "notificacion", "notification"]];
210
+ var normalize = (v) => v.normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase();
211
+ function words(v) {
212
+ return normalize(v).match(/[a-z0-9]+/g) ?? [];
213
+ }
214
+ function searchSfx(bank, options) {
215
+ bank = validateSfxBank(bank);
216
+ if (typeof options.query !== "string" || options.query.length > 200 || /[\x00-\x1f\x7f]/.test(options.query) || !Number.isInteger(options.limit ?? 6) || (options.limit ?? 6) < 1 || (options.limit ?? 6) > 20 || options.sceneRole !== void 0 && !["hook", "body", "cta"].includes(options.sceneRole)) throw new Error("Use query up to 200 characters, limit 1\u201320 and sceneRole hook/body/cta.");
217
+ const policy = sfxPolicy(options.audio, options.sceneRole);
218
+ if (policy.bankId && policy.bankId !== bank.bankId) throw new Error("The requested bank differs from the effective project preference. Record a deliberate project exception first.");
219
+ const terms = words(options.query).filter((w) => !["un", "una", "de", "para", "con", "el", "la", "a", "the", "for", "and", "sound", "sonido", "efecto"].includes(w));
220
+ const expanded = new Set(terms.flatMap((t) => groups.find((g) => g.includes(t)) ?? [t]));
221
+ const candidates = policy.enabled ? bank.entries.filter((e) => options.includeUnverified || e.license.kind !== "unverified").map((e) => {
222
+ const haystack = new Set(words([e.name, ...e.tags].join(" ")));
223
+ const matches = [...expanded].filter((t) => haystack.has(t));
224
+ return { entry: e, matches, score: matches.length * 4 + (policy.character !== "adaptive" && e.character === policy.character ? 2 : 0) };
225
+ }).filter((e) => !terms.length || e.matches.length > 0).sort((a, b) => b.score - a.score || a.entry.id.localeCompare(b.entry.id)) : [];
226
+ return { bankId: bank.bankId, policy, total: candidates.length, items: candidates.slice(0, options.limit ?? 6).map(({ entry, matches }) => ({ ...entry, matchedTerms: matches })), notice: "Lexical candidates with bilingual tags, not audio understanding. Rights are declared, not independently verified. Audition before use; metadata or a preview is not proof of an audible final mix." };
227
+ }
228
+ function checkSfxCues(audio, input) {
229
+ const p = object(input, ["durationSeconds", "hookEndSeconds", "cues"]);
230
+ if (!Number.isFinite(p.durationSeconds) || p.durationSeconds <= 0 || !Number.isFinite(p.hookEndSeconds) || p.hookEndSeconds < 0 || p.hookEndSeconds > p.durationSeconds || !Array.isArray(p.cues) || p.cues.length > 1e3) throw new Error("Declare video duration, hookEndSeconds and up to 1000 cues.");
231
+ const issues = [], warnings = [];
232
+ const cues = p.cues.map((raw) => {
233
+ const c = object(raw, ["at", "durationSeconds", "soundId", "purpose"]);
234
+ if (!Number.isFinite(c.at) || c.at < 0 || !Number.isFinite(c.durationSeconds) || c.durationSeconds <= 0 || c.at + c.durationSeconds > p.durationSeconds + 1e-3) throw new Error("SFX cue lies outside the video.");
235
+ return { at: c.at, durationSeconds: c.durationSeconds, soundId: text2(c.soundId, 80), purpose: text2(c.purpose, 200) };
236
+ }).sort((a, b) => a.at - b.at);
237
+ if (!sfxPolicy(audio).enabled && cues.length) issues.push("SFX are disabled, but the plan contains added effects.");
238
+ if (audio?.sfx?.hookWithoutEffects && cues.some((c) => c.at < p.hookEndSeconds)) issues.push("Added SFX overlap the protected hook.");
239
+ if (cues.some((c, i) => i > 0 && c.at - cues[i - 1].at < 0.3)) warnings.push("Closely spaced accents: listen for saturation.");
240
+ if (cues.some((c, i) => i > 1 && c.soundId === cues[i - 1].soundId && c.soundId === cues[i - 2].soundId)) warnings.push("Same sound repeated three times: check whether repetition is intentional.");
241
+ return { passed: issues.length === 0, issues, warnings, policy: sfxPolicy(audio), cueCount: cues.length, scope: "Declared cue plan only; does not inspect or certify audio. Run production audio-check and listen to the final render." };
242
+ }
243
+
3
244
  // packages/core/src/memory.ts
4
245
  function memoryPath(scope, id2) {
5
246
  if (!["brand", "style", "project"].includes(scope) || !/^[a-z0-9][a-z0-9_-]{0,79}$/.test(id2)) throw new Error("Invalid memory scope");
@@ -32,7 +273,7 @@ function resolveMemory(files, p) {
32
273
 
33
274
  // packages/core/src/style-authoring.ts
34
275
  var own = (value, key) => Object.prototype.hasOwnProperty.call(value, key);
35
- var object = (value) => !!value && typeof value === "object" && !Array.isArray(value);
276
+ var object2 = (value) => !!value && typeof value === "object" && !Array.isArray(value);
36
277
  var nonempty = (value) => typeof value === "string" && !!value.trim();
37
278
  var identifier = /^[a-z0-9][a-z0-9_-]{0,79}$/;
38
279
  var digest = /^[a-f0-9]{64}$/;
@@ -52,11 +293,11 @@ function validateStyleResources(value, expectedId) {
52
293
  const fields = (entry, allowed, label) => {
53
294
  if (Object.keys(entry).some((key) => !allowed.includes(key))) add(`${label} has unsupported fields; do not include URLs, credentials or hidden resource sources`);
54
295
  };
55
- const text7 = (value2, label) => {
296
+ const text9 = (value2, label) => {
56
297
  if (!nonempty(value2)) add(`${label} must be nonempty text`);
57
298
  else if (forbiddenText.test(value2) || /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/.test(value2)) add(`${label} cannot contain URLs, credentials or control characters`);
58
299
  };
59
- if (!object(value)) return { valid: false, issues: [{ path, message: "Style resources must be a JSON object" }] };
300
+ if (!object2(value)) return { valid: false, issues: [{ path, message: "Style resources must be a JSON object" }] };
60
301
  fields(value, ["schemaVersion", "id", "resources"], "Style resources");
61
302
  if (value.schemaVersion !== 1) add("schemaVersion must be 1");
62
303
  if (!identifier.test(expectedId) || value.id !== expectedId) add(`Style resource id must match path: ${expectedId}`);
@@ -65,7 +306,7 @@ function validateStyleResources(value, expectedId) {
65
306
  const ids = /* @__PURE__ */ new Set();
66
307
  for (const [index, resource] of value.resources.entries()) {
67
308
  const label = `Resource ${index + 1}`;
68
- if (!object(resource)) {
309
+ if (!object2(resource)) {
69
310
  add(`${label} must be an object`);
70
311
  continue;
71
312
  }
@@ -74,10 +315,10 @@ function validateStyleResources(value, expectedId) {
74
315
  else if (ids.has(resource.id)) add(`Duplicate resource id: ${resource.id}`);
75
316
  else ids.add(resource.id);
76
317
  if (!kinds.includes(resource.kind)) add(`${label} has an invalid kind`);
77
- text7(resource.purpose, `${label} purpose`);
318
+ text9(resource.purpose, `${label} purpose`);
78
319
  if (typeof resource.required !== "boolean") add(`${label} required must be boolean`);
79
320
  const source = resource.source;
80
- if (!object(source)) {
321
+ if (!object2(source)) {
81
322
  add(`${label} source must be local, asset or requirement`);
82
323
  continue;
83
324
  }
@@ -94,7 +335,7 @@ function validateStyleResources(value, expectedId) {
94
335
  if (typeof source.assetId !== "string" || !digest.test(source.assetId)) add(`${label} assetId must be a 64-character lowercase hexadecimal digest`);
95
336
  } else if (source.type === "requirement") {
96
337
  fields(source, ["type", "description"], `${label} requirement source`);
97
- text7(source.description, `${label} requirement description`);
338
+ text9(source.description, `${label} requirement description`);
98
339
  } else add(`${label} source must be local, asset or requirement`);
99
340
  }
100
341
  }
@@ -743,10 +984,10 @@ var MEDIA_STYLE_CATALOG = {
743
984
  function getMediaStyle(id2, version) {
744
985
  return MEDIA_STYLE_CATALOG.styles.find((style) => style.id === id2 && (version === void 0 || style.version === version));
745
986
  }
746
- var record = (value) => !!value && typeof value === "object" && !Array.isArray(value);
987
+ var record2 = (value) => !!value && typeof value === "object" && !Array.isArray(value);
747
988
  var only = (value, fields) => Object.keys(value).every((key) => fields.includes(key));
748
989
  function visualTasteIssues(value) {
749
- if (!record(value) || !only(value, ["density", "energy", "contrast"])) return ["visualTaste supports density, energy and contrast only."];
990
+ if (!record2(value) || !only(value, ["density", "energy", "contrast"])) return ["visualTaste supports density, energy and contrast only."];
750
991
  const options = { density: ["minimal", "layered"], energy: ["calm", "lively"], contrast: ["soft", "strong"] };
751
992
  return Object.entries(options).flatMap(([key, allowed]) => value[key] === void 0 || typeof value[key] === "string" && allowed.includes(value[key]) ? [] : [`Invalid visualTaste ${key}. Omit undecided choices.`]);
752
993
  }
@@ -763,7 +1004,7 @@ function mediaStyleSelectionIssues(value, category) {
763
1004
  if (!Array.isArray(value) || value.length > 24) return ["mediaStyles must be an array of at most 24 selections."];
764
1005
  const issues = [], ids = /* @__PURE__ */ new Set(), defaults = /* @__PURE__ */ new Set();
765
1006
  for (const selection of value) {
766
- if (!record(selection) || !only(selection, ["styleId", "version", "state"]) || typeof selection.styleId !== "string" || typeof selection.version !== "string" || !["favorite", "default", "avoid"].includes(String(selection.state))) {
1007
+ if (!record2(selection) || !only(selection, ["styleId", "version", "state"]) || typeof selection.styleId !== "string" || typeof selection.version !== "string" || !["favorite", "default", "avoid"].includes(String(selection.state))) {
767
1008
  issues.push("Media style selections require styleId, version and favorite/default/avoid state.");
768
1009
  continue;
769
1010
  }
@@ -783,12 +1024,12 @@ function mediaStyleSelectionIssues(value, category) {
783
1024
  return issues;
784
1025
  }
785
1026
  function mediaStyleOverrideIssues(value) {
786
- if (!record(value) || !only(value, ["image", "motion"])) return ["mediaStyleOverrides supports image and motion categories only."];
1027
+ if (!record2(value) || !only(value, ["image", "motion"])) return ["mediaStyleOverrides supports image and motion categories only."];
787
1028
  const issues = [];
788
1029
  for (const category of ["image", "motion"]) {
789
1030
  const override = value[category];
790
1031
  if (override === void 0) continue;
791
- if (!record(override) || !["inherit", "replace", "disabled"].includes(String(override.mode))) {
1032
+ if (!record2(override) || !["inherit", "replace", "disabled"].includes(String(override.mode))) {
792
1033
  issues.push(`Invalid ${category} media style override.`);
793
1034
  continue;
794
1035
  }
@@ -867,11 +1108,11 @@ var profileOptions = {
867
1108
  aspects: ["Tipograf\xEDa", "Composici\xF3n", "Colores", "Im\xE1genes", "Ritmo", "Animaci\xF3n", "Sonido"]
868
1109
  };
869
1110
  var emptyProfile = () => ({ schemaVersion: 1, experience: "", role: "", niche: "", platforms: [], formats: [], goal: "", pace: "", captions: "", sound: "", freedom: "", assets: "", avoid: [], notes: "", references: [] });
870
- var record2 = (value) => !!value && typeof value === "object" && !Array.isArray(value);
1111
+ var record3 = (value) => !!value && typeof value === "object" && !Array.isArray(value);
871
1112
  var string = (value, min, max) => typeof value === "string" && value.length >= min && value.length <= max;
872
1113
  var choices = (value, options) => Array.isArray(value) && value.length <= options.length && value.every((item) => typeof item === "string" && options.includes(item));
873
1114
  function editorialProfileIssues(value) {
874
- if (!record2(value)) return ["El perfil editorial debe ser un objeto."];
1115
+ if (!record3(value)) return ["El perfil editorial debe ser un objeto."];
875
1116
  const issues = [];
876
1117
  if (value.schemaVersion !== 1) issues.push("schemaVersion del perfil editorial debe ser 1.");
877
1118
  const fields = [...Object.keys(emptyProfile()), "mediaStyles", "visualTaste", "motionSelection", "motionProduction", "editorialDelivery", "mediaSources"];
@@ -886,7 +1127,7 @@ function editorialProfileIssues(value) {
886
1127
  if (!string(value.notes, 0, 1500)) issues.push("notes debe ser texto de hasta 1500 caracteres.");
887
1128
  if (!Array.isArray(value.references) || value.references.length > 12) issues.push("references admite hasta 12 referencias.");
888
1129
  else for (const ref of value.references) {
889
- if (!record2(ref) || !string(ref.videoId, 1, 150) || !string(ref.exampleId, 1, 150) || typeof ref.timestampMs !== "number" || !Number.isFinite(ref.timestampMs) || ref.timestampMs < 0 || !["like", "avoid"].includes(String(ref.sentiment)) || !choices(ref.aspects, profileOptions.aspects)) issues.push("Referencia editorial inv\xE1lida.");
1130
+ if (!record3(ref) || !string(ref.videoId, 1, 150) || !string(ref.exampleId, 1, 150) || typeof ref.timestampMs !== "number" || !Number.isFinite(ref.timestampMs) || ref.timestampMs < 0 || !["like", "avoid"].includes(String(ref.sentiment)) || !choices(ref.aspects, profileOptions.aspects)) issues.push("Referencia editorial inv\xE1lida.");
890
1131
  }
891
1132
  if (value.mediaStyles !== void 0) issues.push(...mediaStyleSelectionIssues(value.mediaStyles));
892
1133
  if (value.visualTaste !== void 0) issues.push(...visualTasteIssues(value.visualTaste));
@@ -941,115 +1182,17 @@ ${block}
941
1182
  `;
942
1183
  }
943
1184
 
944
- // packages/core/src/audio-production.ts
945
- function record3(value, name, keys2) {
946
- if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`${name} must be an object.`);
947
- const result2 = value;
948
- if (Object.keys(result2).some((key) => !keys2.includes(key))) throw new Error(`${name} contains an unsupported field.`);
949
- return result2;
950
- }
951
- function text(value, name, max = 200) {
952
- if (typeof value !== "string" || !value.trim() || value.length > max || /[\x00-\x1f\x7f]/.test(value)) throw new Error(`${name} must be nonempty text of at most ${max} characters.`);
953
- return value.trim();
954
- }
955
- function validateAudioLoudnessTarget(value) {
956
- const input = record3(value, "loudness", ["targetIntegratedLufs", "toleranceLu", "maxTruePeakDbtp"]);
957
- for (const field of ["targetIntegratedLufs", "toleranceLu", "maxTruePeakDbtp"]) {
958
- const number2 = input[field];
959
- if (typeof number2 !== "number" || !Number.isFinite(number2) || (field === "toleranceLu" ? number2 < 0 || number2 > 150 : number2 < -150 || number2 > 24)) throw new Error(`loudness.${field} must be a finite measurement target within the supported range.`);
960
- }
961
- return { targetIntegratedLufs: input.targetIntegratedLufs, toleranceLu: input.toleranceLu, maxTruePeakDbtp: input.maxTruePeakDbtp };
962
- }
963
- function validateAudioProductionPreferences(value) {
964
- const input = record3(value, "audioProduction", ["schemaVersion", "voice", "music", "sfx", "loudness"]);
965
- if (input.schemaVersion !== 1) throw new Error("audioProduction.schemaVersion must be 1.");
966
- const result2 = { schemaVersion: 1 };
967
- if (input.voice === null) result2.voice = null;
968
- else if (input.voice !== void 0) {
969
- const voice = record3(input.voice, "voice", ["mode", "provider", "voiceId"]);
970
- if (voice.mode === "original" || voice.mode === "none") {
971
- if (voice.provider !== void 0 || voice.voiceId !== void 0) throw new Error("Original/no narration does not select a generated voice.");
972
- result2.voice = { mode: voice.mode };
973
- } else if (voice.mode === "catalog" || voice.mode === "cloned") {
974
- result2.voice = { mode: voice.mode, provider: text(voice.provider, "voice.provider"), voiceId: text(voice.voiceId, "voice.voiceId") };
975
- } else throw new Error("voice.mode must explicitly select original, none, catalog or cloned.");
976
- }
977
- if (input.music === null) result2.music = null;
978
- else if (input.music !== void 0) {
979
- const music = record3(input.music, "music", ["intensity"]);
980
- result2.music = {};
981
- if (music.intensity !== void 0) {
982
- if (music.intensity !== null && (typeof music.intensity !== "string" || !["none", "subtle", "present"].includes(music.intensity))) throw new Error("music.intensity must be none, subtle, present or null.");
983
- result2.music.intensity = music.intensity;
984
- }
985
- }
986
- if (input.sfx === null) result2.sfx = null;
987
- else if (input.sfx !== void 0) {
988
- const sfx = record3(input.sfx, "sfx", ["library", "generation", "density", "hookWithoutEffects"]);
989
- result2.sfx = {};
990
- if (sfx.library === null) result2.sfx.library = null;
991
- else if (sfx.library !== void 0) {
992
- const library = record3(sfx.library, "sfx.library", ["bankId"]);
993
- const bankId = text(library.bankId, "sfx.library.bankId");
994
- if (!/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/.test(bankId)) throw new Error("sfx.library.bankId must be a portable identifier, not a path.");
995
- result2.sfx.library = { bankId };
996
- }
997
- if (sfx.generation !== void 0) {
998
- if (sfx.generation !== null && (typeof sfx.generation !== "string" || !["allowed", "forbidden"].includes(sfx.generation))) throw new Error("sfx.generation must be allowed, forbidden or null.");
999
- result2.sfx.generation = sfx.generation;
1000
- }
1001
- if (sfx.density !== void 0) {
1002
- if (sfx.density !== null && (typeof sfx.density !== "string" || !["none", "sparse", "moderate", "dense"].includes(sfx.density))) throw new Error("sfx.density must be none, sparse, moderate, dense or null.");
1003
- result2.sfx.density = sfx.density;
1004
- }
1005
- if (sfx.hookWithoutEffects !== void 0) {
1006
- if (sfx.hookWithoutEffects !== null && typeof sfx.hookWithoutEffects !== "boolean") throw new Error("sfx.hookWithoutEffects must be a boolean or null.");
1007
- result2.sfx.hookWithoutEffects = sfx.hookWithoutEffects;
1008
- }
1009
- }
1010
- if (input.loudness !== void 0) result2.loudness = input.loudness === null ? null : validateAudioLoudnessTarget(input.loudness);
1011
- return result2;
1012
- }
1013
- function resolveAudioProductionPreferences(workspace, project) {
1014
- const base = workspace === void 0 ? { schemaVersion: 1 } : validateAudioProductionPreferences(workspace);
1015
- const override = project === void 0 ? { schemaVersion: 1 } : validateAudioProductionPreferences(project);
1016
- return { ...base, ...override, ...override.music ? { music: { ...base.music, ...override.music } } : {}, ...override.sfx ? { sfx: { ...base.sfx, ...override.sfx } } : {} };
1017
- }
1018
- function applyEditorialAudioConstraints(preferences, profile) {
1019
- let effective = preferences === void 0 ? void 0 : validateAudioProductionPreferences(preferences);
1020
- const warnings = [];
1021
- const withoutMusic = profile.sound === "Sin m\xFAsica ni efectos";
1022
- const withoutSfx = withoutMusic || profile.avoid?.includes("Sin efectos de sonido");
1023
- if (withoutMusic) {
1024
- if (effective?.music?.intensity && effective.music.intensity !== "none") warnings.push("Music is disabled by the saved editorial restriction; the original audio preferences remain unchanged.");
1025
- effective = { ...effective, schemaVersion: 1, music: { ...effective?.music, intensity: "none" } };
1026
- }
1027
- if (withoutSfx) {
1028
- if (effective?.sfx?.density && effective.sfx.density !== "none") warnings.push("SFX are disabled by the saved editorial restriction; the original audio preferences remain unchanged.");
1029
- effective = { ...effective, schemaVersion: 1, sfx: { ...effective?.sfx, density: "none", generation: "forbidden" } };
1030
- }
1031
- const narration = profile.editorialDelivery?.narration;
1032
- const voice = effective?.voice;
1033
- if (narration === "original" || narration === "none") {
1034
- if (voice && voice.mode !== narration) warnings.push(`Narration preference ${narration} conflicts with audio voice ${voice.mode}. Do not generate narration; confirm any deliberate project exception with the current request.`);
1035
- effective = { ...effective, schemaVersion: 1, voice: { mode: narration } };
1036
- } else if (narration === "voiceover" && voice && (voice.mode === "original" || voice.mode === "none")) {
1037
- warnings.push(`Voiceover preference conflicts with audio voice ${voice.mode}. Resolve narration with the current request; do not infer a paid provider or voice.`);
1038
- }
1039
- return { ...effective ? { preferences: effective } : {}, warnings };
1040
- }
1041
-
1042
1185
  // packages/core/src/onboarding.ts
1043
1186
  var pretty = (value) => JSON.stringify(value, null, 2) + "\n";
1044
1187
  var designText = "# Dise\xF1o\n\nAdapta composici\xF3n, ritmo y jerarqu\xEDa al contenido. Consulta tokens.json para los colores de la marca. Las tipograf\xEDas y recursos pendientes se deciden con el usuario antes de editar.\n";
1045
1188
  var onboardingDocumentNames = ["brand.md", "design.md", "tokens.json", "language.json", "assets.json", "audio.json"];
1046
- var object2 = (value) => !!value && typeof value === "object" && !Array.isArray(value);
1189
+ var object3 = (value) => !!value && typeof value === "object" && !Array.isArray(value);
1047
1190
  var own2 = (value, key) => Object.prototype.hasOwnProperty.call(value, key);
1048
1191
  function record4(value, label, allowed) {
1049
- if (!object2(value) || Object.keys(value).some((key) => !allowed.includes(key))) throw new Error(`${label} contiene campos desconocidos o no es un objeto.`);
1192
+ if (!object3(value) || Object.keys(value).some((key) => !allowed.includes(key))) throw new Error(`${label} contiene campos desconocidos o no es un objeto.`);
1050
1193
  return value;
1051
1194
  }
1052
- function text2(value, label, max, nonempty3 = false) {
1195
+ function text3(value, label, max, nonempty3 = false) {
1053
1196
  if (typeof value !== "string" || value.length > max || /[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/.test(value) || nonempty3 && !value.trim()) throw new Error(`${label} debe ser texto${nonempty3 ? " no vac\xEDo" : ""} de hasta ${max} caracteres.`);
1054
1197
  }
1055
1198
  function validDocument(path, content) {
@@ -1069,7 +1212,7 @@ function mergeProfile(base, patch) {
1069
1212
  }
1070
1213
  if (key === "editorialDelivery" || key === "visualTaste") {
1071
1214
  const fields = record4(value, key, key === "editorialDelivery" ? Object.keys(editorialDeliveryOptions) : ["density", "energy", "contrast"]);
1072
- const previous = object2(result2[key]) ? result2[key] : {};
1215
+ const previous = object3(result2[key]) ? result2[key] : {};
1073
1216
  const next = { ...previous };
1074
1217
  for (const [field, selection] of Object.entries(fields)) {
1075
1218
  if (selection === null) delete next[field];
@@ -1082,7 +1225,7 @@ function mergeProfile(base, patch) {
1082
1225
  }
1083
1226
  function validateIdentity(value) {
1084
1227
  const input = record4(value, "identity", ["name", "description", "audience", "tone", "links"]);
1085
- for (const [key, max] of [["name", 100], ["description", 1500], ["audience", 500], ["tone", 500], ["links", 1500]]) if (input[key] !== void 0) text2(input[key], `identity.${key}`, max, key === "name");
1228
+ for (const [key, max] of [["name", 100], ["description", 1500], ["audience", 500], ["tone", 500], ["links", 1500]]) if (input[key] !== void 0) text3(input[key], `identity.${key}`, max, key === "name");
1086
1229
  return clone(input);
1087
1230
  }
1088
1231
  function validateOnboardingIntake(value) {
@@ -1092,17 +1235,17 @@ function validateOnboardingIntake(value) {
1092
1235
  if (new TextEncoder().encode(JSON.stringify(input)).length > 96 * 1024) throw new Error("El formulario de onboarding supera 96 KiB.");
1093
1236
  if (input.identity !== void 0) validateIdentity(input.identity);
1094
1237
  if (input.tokens !== void 0) {
1095
- if (!object2(input.tokens) || Object.keys(input.tokens).length > 100) throw new Error("tokens debe contener hasta 100 reglas.");
1238
+ if (!object3(input.tokens) || Object.keys(input.tokens).length > 100) throw new Error("tokens debe contener hasta 100 reglas.");
1096
1239
  for (const [key, rule] of Object.entries(input.tokens)) {
1097
1240
  const check = rule === null ? { value: "", strength: "preferred" } : rule;
1098
1241
  record4(check, `tokens.${key}`, ["value", "strength", "rationale"]);
1099
- if (object2(check) && check.rationale !== void 0) text2(check.rationale, "rationale", 1500);
1242
+ if (object3(check) && check.rationale !== void 0) text3(check.rationale, "rationale", 1500);
1100
1243
  validDocument(`.editorbrain/brands/${input.brandId}/tokens.json`, pretty({ schemaVersion: 1, id: input.brandId, tokens: { [key]: check } }));
1101
1244
  }
1102
1245
  }
1103
1246
  if (input.language !== void 0) {
1104
1247
  const language = record4(input.language, "language", ["defaultLanguage", "glossary", "pronunciations", "doNotTranslate"]);
1105
- if (language.defaultLanguage !== void 0 && language.defaultLanguage !== null) text2(language.defaultLanguage, "defaultLanguage", 120, true);
1248
+ if (language.defaultLanguage !== void 0 && language.defaultLanguage !== null) text3(language.defaultLanguage, "defaultLanguage", 120, true);
1106
1249
  for (const key of ["glossary", "pronunciations", "doNotTranslate"]) if (language[key] !== void 0 && (!Array.isArray(language[key]) || language[key].length > 1e3)) throw new Error(`${key} admite hasta 1000 entradas.`);
1107
1250
  const next = { ...language };
1108
1251
  if (next.defaultLanguage === null) delete next.defaultLanguage;
@@ -1397,14 +1540,14 @@ function record5(value, name, keys2) {
1397
1540
  if (!value || typeof value !== "object" || Array.isArray(value) || Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null) throw new Error(`${name} must be an object.`);
1398
1541
  if (Object.keys(value).some((key) => !keys2.includes(key))) throw new Error(`${name} has unsupported fields.`);
1399
1542
  }
1400
- function text3(value, name, max) {
1543
+ function text4(value, name, max) {
1401
1544
  if (typeof value !== "string" || !value.trim() || value.length > max * 2 || Array.from(value).length > max || /[\x00-\x1f\x7f-\x9f\u202a-\u202e\u2066-\u2069]/.test(value) || /[\ud800-\udfff]/u.test(value)) throw new Error(`${name} requires 1\u2013${max} Unicode code points without control characters.`);
1402
1545
  }
1403
1546
  function numeric(value, name, min, max, integer2 = false) {
1404
1547
  if (typeof value !== "number" || !Number.isFinite(value) || value < min || value > max || integer2 && !Number.isInteger(value)) throw new Error(`${name} must be ${integer2 ? "an integer" : "a number"} from ${min} to ${max}.`);
1405
1548
  }
1406
1549
  function rasterDimensions(binary, mime, name) {
1407
- const invalid4 = () => new Error(`${name} has malformed or missing raster dimensions.`);
1550
+ const invalid5 = () => new Error(`${name} has malformed or missing raster dimensions.`);
1408
1551
  const byte = (offset2) => binary.charCodeAt(offset2);
1409
1552
  const u16 = (offset2) => byte(offset2) * 256 + byte(offset2 + 1);
1410
1553
  const u32 = (offset2) => u16(offset2) * 65536 + u16(offset2 + 2);
@@ -1412,50 +1555,50 @@ function rasterDimensions(binary, mime, name) {
1412
1555
  const le24 = (offset2) => le16(offset2) + byte(offset2 + 2) * 65536;
1413
1556
  const le32 = (offset2) => le24(offset2) + byte(offset2 + 3) * 16777216;
1414
1557
  const check = (width, height) => {
1415
- if (!Number.isInteger(width) || !Number.isInteger(height) || width <= 0 || height <= 0) throw invalid4();
1558
+ if (!Number.isInteger(width) || !Number.isInteger(height) || width <= 0 || height <= 0) throw invalid5();
1416
1559
  if (width > MOTION_INPUT_LIMITS.maxImageDimension || height > MOTION_INPUT_LIMITS.maxImageDimension || width * height > MOTION_INPUT_LIMITS.maxImagePixels) throw new Error(`${name} raster dimensions exceed ${MOTION_INPUT_LIMITS.maxImageDimension} pixels per side or ${MOTION_INPUT_LIMITS.maxImagePixels} total pixels.`);
1417
1560
  };
1418
1561
  if (mime === "image/png") {
1419
- if (u32(8) !== 13) throw invalid4();
1562
+ if (u32(8) !== 13) throw invalid5();
1420
1563
  check(u32(16), u32(20));
1421
1564
  return;
1422
1565
  }
1423
1566
  if (mime === "image/jpeg") {
1424
1567
  let offset2 = 2, dimensions;
1425
1568
  while (offset2 < binary.length) {
1426
- if (byte(offset2++) !== 255) throw invalid4();
1569
+ if (byte(offset2++) !== 255) throw invalid5();
1427
1570
  while (offset2 < binary.length && byte(offset2) === 255) offset2++;
1428
- if (offset2 >= binary.length) throw invalid4();
1571
+ if (offset2 >= binary.length) throw invalid5();
1429
1572
  const marker = byte(offset2++);
1430
1573
  if (marker === 217 || marker === 218) break;
1431
- if (marker === 216 || marker === 0) throw invalid4();
1574
+ if (marker === 216 || marker === 0) throw invalid5();
1432
1575
  if (marker === 1 || marker >= 208 && marker <= 215) continue;
1433
- if (offset2 + 2 > binary.length) throw invalid4();
1576
+ if (offset2 + 2 > binary.length) throw invalid5();
1434
1577
  const size = u16(offset2);
1435
- if (size < 2 || offset2 + size > binary.length) throw invalid4();
1578
+ if (size < 2 || offset2 + size > binary.length) throw invalid5();
1436
1579
  if ([192, 193, 194, 195, 197, 198, 199, 201, 202, 203, 205, 206, 207].includes(marker)) {
1437
- if (size < 11 || size !== 8 + byte(offset2 + 7) * 3) throw invalid4();
1580
+ if (size < 11 || size !== 8 + byte(offset2 + 7) * 3) throw invalid5();
1438
1581
  const width = u16(offset2 + 5), height = u16(offset2 + 3);
1439
1582
  check(width, height);
1440
- if (dimensions && (dimensions[0] !== width || dimensions[1] !== height)) throw invalid4();
1583
+ if (dimensions && (dimensions[0] !== width || dimensions[1] !== height)) throw invalid5();
1441
1584
  dimensions = [width, height];
1442
1585
  }
1443
1586
  offset2 += size;
1444
1587
  }
1445
- if (!dimensions) throw invalid4();
1588
+ if (!dimensions) throw invalid5();
1446
1589
  return;
1447
1590
  }
1448
- if (le32(4) !== binary.length - 8) throw invalid4();
1591
+ if (le32(4) !== binary.length - 8) throw invalid5();
1449
1592
  const frameDimensions = (kind, size, data) => {
1450
1593
  if (kind === "VP8L") {
1451
- if (size < 5 || byte(data) !== 47 || byte(data + 4) >> 5 !== 0) throw invalid4();
1594
+ if (size < 5 || byte(data) !== 47 || byte(data + 4) >> 5 !== 0) throw invalid5();
1452
1595
  const width = 1 + (byte(data + 1) | (byte(data + 2) & 63) << 8);
1453
1596
  const height = 1 + (byte(data + 2) >> 6 | byte(data + 3) << 2 | (byte(data + 4) & 15) << 10);
1454
1597
  check(width, height);
1455
1598
  return true;
1456
1599
  }
1457
1600
  if (kind === "VP8 ") {
1458
- if (size < 10 || byte(data) & 1 || binary.slice(data + 3, data + 6) !== "\x9D*") throw invalid4();
1601
+ if (size < 10 || byte(data) & 1 || binary.slice(data + 3, data + 6) !== "\x9D*") throw invalid5();
1459
1602
  check(le16(data + 6) & 16383, le16(data + 8) & 16383);
1460
1603
  return true;
1461
1604
  }
@@ -1464,26 +1607,26 @@ function rasterDimensions(binary, mime, name) {
1464
1607
  let offset = 12, found = false;
1465
1608
  while (offset + 8 <= binary.length) {
1466
1609
  const kind = binary.slice(offset, offset + 4), size = le32(offset + 4), data = offset + 8;
1467
- if (size > binary.length - data) throw invalid4();
1610
+ if (size > binary.length - data) throw invalid5();
1468
1611
  if (kind === "VP8X") {
1469
- if (size !== 10) throw invalid4();
1612
+ if (size !== 10) throw invalid5();
1470
1613
  check(le24(data + 4) + 1, le24(data + 7) + 1);
1471
1614
  found = true;
1472
1615
  } else if (kind === "ANMF") {
1473
- if (size < 16) throw invalid4();
1616
+ if (size < 16) throw invalid5();
1474
1617
  check(le24(data + 6) + 1, le24(data + 9) + 1);
1475
1618
  let nested = data + 16, frame = false;
1476
1619
  while (nested + 8 <= data + size) {
1477
1620
  const frameKind = binary.slice(nested, nested + 4), frameSize = le32(nested + 4), frameData = nested + 8;
1478
- if (frameSize > data + size - frameData) throw invalid4();
1621
+ if (frameSize > data + size - frameData) throw invalid5();
1479
1622
  frame = frameDimensions(frameKind, frameSize, frameData) || frame;
1480
1623
  nested = frameData + frameSize + frameSize % 2;
1481
1624
  }
1482
- if (!frame || nested !== data + size) throw invalid4();
1625
+ if (!frame || nested !== data + size) throw invalid5();
1483
1626
  } else found = frameDimensions(kind, size, data) || found;
1484
1627
  offset = data + size + size % 2;
1485
1628
  }
1486
- if (!found || offset !== binary.length) throw invalid4();
1629
+ if (!found || offset !== binary.length) throw invalid5();
1487
1630
  }
1488
1631
  function dataUrl(value, name, kind) {
1489
1632
  const max = kind === "font" ? MOTION_INPUT_LIMITS.fontBytes : MOTION_INPUT_LIMITS.imageBytes;
@@ -1519,16 +1662,16 @@ function validateMotionSceneConfig(value) {
1519
1662
  const bg = luminance(value.palette.background), fg = luminance(value.palette.foreground);
1520
1663
  if ((Math.max(bg, fg) + 0.05) / (Math.min(bg, fg) + 0.05) < 4.5) throw new Error("Foreground/background contrast must be at least 4.5:1; brand colors were not changed.");
1521
1664
  record5(value.font, "font", ["family", "dataUrl"]);
1522
- text3(value.font.family, "font.family", 100);
1665
+ text4(value.font.family, "font.family", 100);
1523
1666
  if (!/^[\p{L}\p{N} _-]+$/u.test(value.font.family)) throw new Error("font.family must be one font family using letters, numbers, spaces, underscores or hyphens.");
1524
1667
  dataUrl(value.font.dataUrl, "font.dataUrl", "font");
1525
- text3(value.title, "title", MOTION_INPUT_LIMITS.titleCodePoints);
1526
- if (value.subtitle !== void 0) text3(value.subtitle, "subtitle", MOTION_INPUT_LIMITS.subtitleCodePoints);
1668
+ text4(value.title, "title", MOTION_INPUT_LIMITS.titleCodePoints);
1669
+ if (value.subtitle !== void 0) text4(value.subtitle, "subtitle", MOTION_INPUT_LIMITS.subtitleCodePoints);
1527
1670
  if (!Array.isArray(value.items) || value.items.length < 2 || value.items.length > 4) throw new Error("Motion scene requires 2\u20134 items.");
1528
1671
  value.items.forEach((item, index) => {
1529
1672
  record5(item, `items[${index}]`, ["label", "detail", "image", "shape"]);
1530
- text3(item.label, `items[${index}].label`, MOTION_INPUT_LIMITS.labelCodePoints);
1531
- if (item.detail !== void 0) text3(item.detail, `items[${index}].detail`, MOTION_INPUT_LIMITS.detailCodePoints);
1673
+ text4(item.label, `items[${index}].label`, MOTION_INPUT_LIMITS.labelCodePoints);
1674
+ if (item.detail !== void 0) text4(item.detail, `items[${index}].detail`, MOTION_INPUT_LIMITS.detailCodePoints);
1532
1675
  if (item.image !== void 0 && codeNative(String(value.componentId))) throw new Error(`items[${index}].image is unsupported by this code-native component.`);
1533
1676
  if (item.image !== void 0) dataUrl(item.image, `items[${index}].image`, "image");
1534
1677
  if (item.shape !== void 0 && !["circle", "capsule", "diamond", "heart", "shield", "chat", "star"].includes(String(item.shape))) throw new Error(`items[${index}].shape is unsupported.`);
@@ -1541,8 +1684,8 @@ function validateMotionSceneConfig(value) {
1541
1684
  var record6 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
1542
1685
  var positive = (v) => typeof v === "number" && Number.isFinite(v) && v > 0;
1543
1686
  var nonnegative = (v) => v === 0 || positive(v);
1544
- var hash = (v) => typeof v === "string" && /^[a-f0-9]{64}$/.test(v);
1545
- var text4 = (v) => typeof v === "string" && v.trim().length > 0 && v.length <= 2e3;
1687
+ var hash2 = (v) => typeof v === "string" && /^[a-f0-9]{64}$/.test(v);
1688
+ var text5 = (v) => typeof v === "string" && v.trim().length > 0 && v.length <= 2e3;
1546
1689
  var range = (v) => record6(v) && nonnegative(v.startSeconds) && positive(v.endSeconds) && v.endSeconds > v.startSeconds;
1547
1690
  var MAX_ENTRIES = 2e3;
1548
1691
  function validateProductionPlan(value) {
@@ -1552,12 +1695,12 @@ function validateProductionPlan(value) {
1552
1695
  if (!Array.isArray(value.inspections) || value.inspections.length > MAX_ENTRIES) issues.push("inspections must be an array of at most 2000 observations.");
1553
1696
  else value.inspections.forEach((item, index) => {
1554
1697
  const label = `inspections[${index}]`;
1555
- if (!record6(item) || !hash(item.sourceSha256) || !["visual", "motion", "audio"].includes(String(item.modality)) || !["sampled_frames", "playback", "listening"].includes(String(item.method))) {
1698
+ if (!record6(item) || !hash2(item.sourceSha256) || !["visual", "motion", "audio"].includes(String(item.modality)) || !["sampled_frames", "playback", "listening"].includes(String(item.method))) {
1556
1699
  issues.push(`${label}: invalid source hash, modality or method.`);
1557
1700
  return;
1558
1701
  }
1559
1702
  if (Object.keys(item).some((key) => !["sourceSha256", "modality", "method", "intervals", "sampledSeconds", "note"].includes(key))) issues.push(`${label}: unsupported field.`);
1560
- if (item.note !== void 0 && !text4(item.note)) issues.push(`${label}: note must be nonempty text of at most 2000 characters.`);
1703
+ if (item.note !== void 0 && !text5(item.note)) issues.push(`${label}: note must be nonempty text of at most 2000 characters.`);
1561
1704
  if (item.method === "sampled_frames") {
1562
1705
  if (item.modality !== "visual" || item.intervals !== void 0 || !Array.isArray(item.sampledSeconds) || !item.sampledSeconds.length || item.sampledSeconds.length > MAX_ENTRIES || !item.sampledSeconds.every(nonnegative)) issues.push(`${label}: sampled_frames requires visual modality and sampledSeconds; snapshots never establish continuous interval coverage.`);
1563
1706
  } else {
@@ -1570,14 +1713,14 @@ function validateProductionPlan(value) {
1570
1713
  const ids = /* @__PURE__ */ new Set();
1571
1714
  value.uses.forEach((item, index) => {
1572
1715
  const label = `uses[${index}]`;
1573
- if (!record6(item) || typeof item.id !== "string" || !/^[a-zA-Z0-9_-]{1,80}$/.test(item.id) || !hash(item.sourceSha256) || !range(item.source) || !range(item.timeline)) {
1716
+ if (!record6(item) || typeof item.id !== "string" || !/^[a-zA-Z0-9_-]{1,80}$/.test(item.id) || !hash2(item.sourceSha256) || !range(item.source) || !range(item.timeline)) {
1574
1717
  issues.push(`${label}: requires ID, source hash, source and timeline intervals.`);
1575
1718
  return;
1576
1719
  }
1577
1720
  if (ids.has(item.id)) issues.push(`${label}: duplicate use ID ${item.id}.`);
1578
1721
  ids.add(item.id);
1579
1722
  if (Object.keys(item).some((key) => !["id", "sourceSha256", "source", "timeline", "purpose", "reuseReason"].includes(key))) issues.push(`${label}: unsupported field.`);
1580
- for (const key of ["purpose", "reuseReason"]) if (item[key] !== void 0 && !text4(item[key])) issues.push(`${label}: ${key} must be nonempty text of at most 2000 characters.`);
1723
+ for (const key of ["purpose", "reuseReason"]) if (item[key] !== void 0 && !text5(item[key])) issues.push(`${label}: ${key} must be nonempty text of at most 2000 characters.`);
1581
1724
  });
1582
1725
  }
1583
1726
  return { valid: !issues.length, issues };
@@ -1605,7 +1748,7 @@ function checkProductionPlan(inventory, value) {
1605
1748
  findings.push({ code: "INVENTORY_INCOMPLETE", severity: "warning", message: "Inventory traversal or probing was incomplete. This plan check covers only recorded assets." });
1606
1749
  }
1607
1750
  for (const asset of inventory.assets) {
1608
- if (!record6(asset) || !hash(asset.sha256) || !(asset.durationSeconds === null || nonnegative(asset.durationSeconds)) || asset.streams !== void 0 && (!Array.isArray(asset.streams) || asset.streams.some((s) => !record6(s) || typeof s.type !== "string"))) {
1751
+ if (!record6(asset) || !hash2(asset.sha256) || !(asset.durationSeconds === null || nonnegative(asset.durationSeconds)) || asset.streams !== void 0 && (!Array.isArray(asset.streams) || asset.streams.some((s) => !record6(s) || typeof s.type !== "string"))) {
1609
1752
  findings.push({ code: "INVALID_INVENTORY", severity: "error", message: "Inventory assets need a source hash, nonnegative duration or null, and valid optional streams." });
1610
1753
  continue;
1611
1754
  }
@@ -1670,11 +1813,11 @@ function checkProductionPlan(inventory, value) {
1670
1813
 
1671
1814
  // packages/core/src/production-evidence.ts
1672
1815
  var record7 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
1673
- var text5 = (v) => typeof v === "string" && v.trim().length > 0 && v.length <= 2e3;
1674
- var hash2 = (v) => typeof v === "string" && /^[a-f0-9]{64}$/.test(v);
1816
+ var text6 = (v) => typeof v === "string" && v.trim().length > 0 && v.length <= 2e3;
1817
+ var hash3 = (v) => typeof v === "string" && /^[a-f0-9]{64}$/.test(v);
1675
1818
  var id = (v) => typeof v === "string" && /^[a-zA-Z0-9_-]{1,80}$/.test(v);
1676
1819
  var url = (v) => {
1677
- if (!text5(v)) return false;
1820
+ if (!text6(v)) return false;
1678
1821
  try {
1679
1822
  const u = new URL(v);
1680
1823
  return u.protocol === "https:" && !u.username && !u.password;
@@ -1695,7 +1838,7 @@ function validateProductionEvidence(value) {
1695
1838
  const seen = { assets: /* @__PURE__ */ new Set(), uses: /* @__PURE__ */ new Set(), claims: /* @__PURE__ */ new Set() };
1696
1839
  for (const [i, item] of value.assets.entries()) {
1697
1840
  const label = `assets[${i}]`;
1698
- if (!record7(item) || !hash2(item.sha256) || !text5(item.description) || !oneOf(item.origin, ["owned", "licensed", "generated"]) || !keys(item, ["sha256", "origin", "description", "source", "review", "anchors"])) {
1841
+ if (!record7(item) || !hash3(item.sha256) || !text6(item.description) || !oneOf(item.origin, ["owned", "licensed", "generated"]) || !keys(item, ["sha256", "origin", "description", "source", "review", "anchors"])) {
1699
1842
  issues.push(`${label}: invalid asset.`);
1700
1843
  continue;
1701
1844
  }
@@ -1704,13 +1847,13 @@ function validateProductionEvidence(value) {
1704
1847
  if (item.origin === "licensed" && !item.source) issues.push(`${label}: licensed material requires source metadata.`);
1705
1848
  if (item.source !== void 0) {
1706
1849
  const s = item.source;
1707
- if (!record7(s) || !keys(s, ["url", "author", "license", "retrievedAt"]) || !url(s.url) || !text5(s.author) || !text5(s.license) || typeof s.retrievedAt !== "string" || !/^\d{4}-\d{2}-\d{2}T/.test(s.retrievedAt) || !Number.isFinite(Date.parse(s.retrievedAt))) issues.push(`${label}: source needs HTTPS page URL, author, license and ISO retrieval time.`);
1850
+ if (!record7(s) || !keys(s, ["url", "author", "license", "retrievedAt"]) || !url(s.url) || !text6(s.author) || !text6(s.license) || typeof s.retrievedAt !== "string" || !/^\d{4}-\d{2}-\d{2}T/.test(s.retrievedAt) || !Number.isFinite(Date.parse(s.retrievedAt))) issues.push(`${label}: source needs HTTPS page URL, author, license and ISO retrieval time.`);
1708
1851
  }
1709
- if (item.review !== void 0 && (!record7(item.review) || !keys(item.review, ["method", "reviewedSha256"]) || !oneOf(item.review.method, ["view_image", "playback", "listening"]) || !hash2(item.review.reviewedSha256))) issues.push(`${label}: invalid review declaration.`);
1710
- if (item.anchors !== void 0 && (!Array.isArray(item.anchors) || item.anchors.length > 50 || item.anchors.some((a) => !record7(a) || !keys(a, ["label", "x", "y", "evidenceUrl"]) || !text5(a.label) || !fraction(a.x) || !fraction(a.y) || a.evidenceUrl !== void 0 && !url(a.evidenceUrl)))) issues.push(`${label}: anchors need label and normalized source-image x/y; optional evidenceUrl must be HTTPS.`);
1852
+ if (item.review !== void 0 && (!record7(item.review) || !keys(item.review, ["method", "reviewedSha256"]) || !oneOf(item.review.method, ["view_image", "playback", "listening"]) || !hash3(item.review.reviewedSha256))) issues.push(`${label}: invalid review declaration.`);
1853
+ if (item.anchors !== void 0 && (!Array.isArray(item.anchors) || item.anchors.length > 50 || item.anchors.some((a) => !record7(a) || !keys(a, ["label", "x", "y", "evidenceUrl"]) || !text6(a.label) || !fraction(a.x) || !fraction(a.y) || a.evidenceUrl !== void 0 && !url(a.evidenceUrl)))) issues.push(`${label}: anchors need label and normalized source-image x/y; optional evidenceUrl must be HTTPS.`);
1711
1854
  }
1712
1855
  for (const [i, item] of value.uses.entries()) {
1713
- if (!record7(item) || !keys(item, ["id", "sha256", "representation", "claimIds", "disclosure"]) || !id(item.id) || !hash2(item.sha256) || !oneOf(item.representation, ["documentary", "illustrative", "original"]) || item.disclosure !== void 0 && !text5(item.disclosure) || item.claimIds !== void 0 && (!Array.isArray(item.claimIds) || item.claimIds.length > 100 || !item.claimIds.every(id))) {
1856
+ if (!record7(item) || !keys(item, ["id", "sha256", "representation", "claimIds", "disclosure"]) || !id(item.id) || !hash3(item.sha256) || !oneOf(item.representation, ["documentary", "illustrative", "original"]) || item.disclosure !== void 0 && !text6(item.disclosure) || item.claimIds !== void 0 && (!Array.isArray(item.claimIds) || item.claimIds.length > 100 || !item.claimIds.every(id))) {
1714
1857
  issues.push(`uses[${i}]: invalid use.`);
1715
1858
  continue;
1716
1859
  }
@@ -1718,7 +1861,7 @@ function validateProductionEvidence(value) {
1718
1861
  seen.uses.add(item.id);
1719
1862
  }
1720
1863
  for (const [i, item] of value.claims.entries()) {
1721
- if (!record7(item) || !keys(item, ["id", "text", "status", "sources"]) || !id(item.id) || !text5(item.text) || !oneOf(item.status, ["supported", "disputed", "unverified"]) || !Array.isArray(item.sources) || item.sources.length > 20 || item.sources.some((s) => !record7(s) || !keys(s, ["url", "kind", "evidence"]) || !url(s.url) || !oneOf(s.kind, ["primary", "secondary"]) || !text5(s.evidence))) {
1864
+ if (!record7(item) || !keys(item, ["id", "text", "status", "sources"]) || !id(item.id) || !text6(item.text) || !oneOf(item.status, ["supported", "disputed", "unverified"]) || !Array.isArray(item.sources) || item.sources.length > 20 || item.sources.some((s) => !record7(s) || !keys(s, ["url", "kind", "evidence"]) || !url(s.url) || !oneOf(s.kind, ["primary", "secondary"]) || !text6(s.evidence))) {
1722
1865
  issues.push(`claims[${i}]: invalid claim or source.`);
1723
1866
  continue;
1724
1867
  }
@@ -1738,7 +1881,7 @@ function checkProductionEvidence(inventory, value) {
1738
1881
  else {
1739
1882
  inventoryComplete = inventory.complete && !(Array.isArray(inventory.failures) && inventory.failures.length);
1740
1883
  for (const asset of inventory.assets) {
1741
- if (!record7(asset) || !hash2(asset.sha256) || !Array.isArray(asset.streams) || asset.streams.some((s) => !record7(s) || typeof s.type !== "string")) {
1884
+ if (!record7(asset) || !hash3(asset.sha256) || !Array.isArray(asset.streams) || asset.streams.some((s) => !record7(s) || typeof s.type !== "string")) {
1742
1885
  add("INVALID_INVENTORY", "error", "Inventory assets require hashes and stream types.");
1743
1886
  continue;
1744
1887
  }
@@ -1963,7 +2106,7 @@ var integrationCatalog = [
1963
2106
  { id: "pexels", name: "Pexels", description: "Fotos y videos de stock; revisa la licencia y el contexto de uso.", capabilities: ["stock-image", "stock-video"], accessMethods: ["agent", "api", "browser", "files"], docsUrl: "https://www.pexels.com/api/documentation/" },
1964
2107
  { id: "assemblyai", name: "AssemblyAI", description: "Transcripci\xF3n con marcas de tiempo para planificar cortes y subt\xEDtulos.", capabilities: ["transcription"], accessMethods: ["agent", "api", "files"], docsUrl: "https://www.assemblyai.com/docs" }
1965
2108
  ];
1966
- function object3(value) {
2109
+ function object4(value) {
1967
2110
  return value !== null && typeof value === "object" && !Array.isArray(value);
1968
2111
  }
1969
2112
  function knownFields(value, allowed) {
@@ -1975,12 +2118,12 @@ function containsCredential(value) {
1975
2118
  function validateIntegrationUpdate(providerId, input) {
1976
2119
  const provider = integrationCatalog.find((entry) => entry.id === providerId);
1977
2120
  if (!provider) throw new Error("Unknown integration provider.");
1978
- if (!object3(input)) throw new Error("Integration settings must be an object.");
2121
+ if (!object4(input)) throw new Error("Integration settings must be an object.");
1979
2122
  knownFields(input, ["enabled", "accessMethod", "preferences", "expectedRevision"]);
1980
2123
  if (typeof input.enabled !== "boolean") throw new Error("enabled must be a boolean.");
1981
2124
  if (!provider.accessMethods.includes(input.accessMethod)) throw new Error("Unsupported access method for this provider.");
1982
2125
  if (!Number.isSafeInteger(input.expectedRevision) || input.expectedRevision < 0) throw new Error("The current expectedRevision is required.");
1983
- if (!object3(input.preferences)) throw new Error("preferences must be an object.");
2126
+ if (!object4(input.preferences)) throw new Error("preferences must be an object.");
1984
2127
  knownFields(input.preferences, ["instructions", "model", "voiceId", "language", "musicMode", "spending"]);
1985
2128
  const preferences = {};
1986
2129
  for (const [field, max] of [["instructions", 1e3], ["model", 120], ["voiceId", 120], ["language", 60]]) {
@@ -16036,8 +16179,8 @@ var aliases = {
16036
16179
  "spotlight-roll": "testimonio archivo persona foto foco montaje"
16037
16180
  };
16038
16181
  var stopwords = new Set("a al algo como con cual de del donde el ella en entre es esa ese esta este hay la las lo los mas mi muy no o para pero por que se si sin sobre su sus te tu un una uno unos unas y ya mostrar explicar video escena animacion animar".split(" "));
16039
- function stems(text7) {
16040
- return new Set((normalized(text7).match(/[a-z0-9]+/g) ?? []).filter((word) => word.length > 2 && !stopwords.has(word)).map((word) => word.slice(0, 5)));
16182
+ function stems(text9) {
16183
+ return new Set((normalized(text9).match(/[a-z0-9]+/g) ?? []).filter((word) => word.length > 2 && !stopwords.has(word)).map((word) => word.slice(0, 5)));
16041
16184
  }
16042
16185
  var searchIndex = scenes.map((scene) => ({ scene, fields: [
16043
16186
  [scene.title, 3],
@@ -16047,7 +16190,7 @@ var searchIndex = scenes.map((scene) => ({ scene, fields: [
16047
16190
  [(scene.crossNicheUses ?? []).join(" "), 2.5],
16048
16191
  [(scene.presets ?? []).map((preset) => `${preset.name.replace(/-/g, " ")} ${preset.summary}`).join(" "), 2],
16049
16192
  [scene.useFor ?? "", 1.5]
16050
- ].map(([text7, weight]) => ({ terms: stems(text7), weight })) }));
16193
+ ].map(([text9, weight]) => ({ terms: stems(text9), weight })) }));
16051
16194
  function rankedCandidates(search) {
16052
16195
  if (!search) return scenes;
16053
16196
  const exact = scenes.find((scene) => scene.id === search);
@@ -16062,8 +16205,8 @@ function rankedCandidates(search) {
16062
16205
  function legacyCandidates(search) {
16063
16206
  const terms = search.split(" ").filter(Boolean);
16064
16207
  return legacyScenes.filter((scene) => {
16065
- const text7 = normalized([scene.id, scene.title, scene.mechanism, scene.intent, scene.niche, scene.style, aliases[scene.id] || ""].join(" "));
16066
- return terms.every((term) => text7.includes(term));
16208
+ const text9 = normalized([scene.id, scene.title, scene.mechanism, scene.intent, scene.niche, scene.style, aliases[scene.id] || ""].join(" "));
16209
+ return terms.every((term) => text9.includes(term));
16067
16210
  });
16068
16211
  }
16069
16212
  function summary(scene) {
@@ -16133,7 +16276,7 @@ var identifier3 = /^[a-z0-9][a-z0-9_-]{0,79}$/;
16133
16276
  var tokenName = /^[a-z][a-z0-9_.-]{0,79}$/;
16134
16277
  var tokenNameHelp = "Use 1\u201380 characters: start with a lowercase letter, then lowercase letters, digits, _, . or - (for example font_family). __proto__, constructor and prototype are reserved. Keys are flat semantic names, not CSS property names.";
16135
16278
  var own3 = (value, key) => Object.prototype.hasOwnProperty.call(value, key);
16136
- var object4 = (value) => !!value && typeof value === "object" && !Array.isArray(value);
16279
+ var object5 = (value) => !!value && typeof value === "object" && !Array.isArray(value);
16137
16280
  var nonempty2 = (value) => typeof value === "string" && value.trim().length > 0;
16138
16281
  var finite = (value) => typeof value === "number" && Number.isFinite(value);
16139
16282
  var tokenValue = (value) => typeof value === "string" || typeof value === "boolean" || finite(value) || Array.isArray(value) && value.every((v) => typeof v === "string");
@@ -16150,13 +16293,13 @@ function safeDocumentPath(path) {
16150
16293
  return path;
16151
16294
  }
16152
16295
  function checkRules(value, add) {
16153
- if (!object4(value)) {
16296
+ if (!object5(value)) {
16154
16297
  add("tokens must be an object");
16155
16298
  return;
16156
16299
  }
16157
16300
  for (const [key, rule] of Object.entries(value)) {
16158
16301
  if (!tokenName.test(key) || ["__proto__", "constructor", "prototype"].includes(key)) add(`Invalid semantic token ${key}. ${tokenNameHelp}`);
16159
- if (!object4(rule) || !tokenValue(rule.value) || !["required", "preferred", "example"].includes(String(rule.strength))) add(`Token ${key} requires value and strength required/preferred/example`);
16302
+ if (!object5(rule) || !tokenValue(rule.value) || !["required", "preferred", "example"].includes(String(rule.strength))) add(`Token ${key} requires value and strength required/preferred/example`);
16160
16303
  }
16161
16304
  }
16162
16305
  function checkEvidence(value, add) {
@@ -16165,7 +16308,7 @@ function checkEvidence(value, add) {
16165
16308
  return;
16166
16309
  }
16167
16310
  for (const ref of value) {
16168
- if (!object4(ref) || !nonempty2(ref.videoId) || !["reference_only", "licensed_asset", "owned_asset"].includes(String(ref.relationship))) add("Evidence must include videoId and explicit relationship");
16311
+ if (!object5(ref) || !nonempty2(ref.videoId) || !["reference_only", "licensed_asset", "owned_asset"].includes(String(ref.relationship))) add("Evidence must include videoId and explicit relationship");
16169
16312
  else {
16170
16313
  if (ref.timestampSeconds !== void 0 && (!finite(ref.timestampSeconds) || ref.timestampSeconds < 0)) add("Evidence timestamp must be nonnegative seconds");
16171
16314
  if (ref.reviewStatus !== void 0 && !["unreviewed", "agent_reviewed", "human_verified"].includes(String(ref.reviewStatus))) add("Invalid evidence reviewStatus");
@@ -16210,7 +16353,7 @@ function validateDocument(path, content) {
16210
16353
  add("Invalid JSON");
16211
16354
  return { valid: false, issues };
16212
16355
  }
16213
- if (!object4(doc)) {
16356
+ if (!object5(doc)) {
16214
16357
  add("JSON document must be an object");
16215
16358
  return { valid: false, issues };
16216
16359
  }
@@ -16227,6 +16370,14 @@ function validateDocument(path, content) {
16227
16370
  if (!identifier3.test(expected)) add("Invalid resource identifier in path");
16228
16371
  if (doc.id !== void 0 && doc.id !== expected) add(`Document id must match path: ${expected}`);
16229
16372
  }
16373
+ if (parts[0] === ".editorbrain" && parts[1] === "sfx") {
16374
+ try {
16375
+ const bank = validateSfxBank(doc);
16376
+ if (parts.length !== 4 || parts[3] !== "catalog.json" || bank.bankId !== parts[2]) add("SFX bank must match .editorbrain/sfx/<bankId>/catalog.json");
16377
+ } catch (error) {
16378
+ add(error.message);
16379
+ }
16380
+ }
16230
16381
  if (isBrand && parts[3] === "audio.json" || parts[0] === "videos" && parts.length === 3 && parts[2] === "audio.json") {
16231
16382
  try {
16232
16383
  validateAudioProductionPreferences(doc);
@@ -16246,20 +16397,20 @@ function validateDocument(path, content) {
16246
16397
  if (isStyle) {
16247
16398
  if (!nonempty2(doc.name)) add("Style name is required");
16248
16399
  if (doc.references !== void 0) checkEvidence(doc.references, add);
16249
- if (doc.components !== void 0 && (!Array.isArray(doc.components) || doc.components.some((c) => !object4(c) || !nonempty2(c.id) || !["concept", "implemented", "transfer_tested"].includes(String(c.status))))) add("Components need id and explicit implementation status");
16400
+ if (doc.components !== void 0 && (!Array.isArray(doc.components) || doc.components.some((c) => !object5(c) || !nonempty2(c.id) || !["concept", "implemented", "transfer_tested"].includes(String(c.status))))) add("Components need id and explicit implementation status");
16250
16401
  }
16251
16402
  }
16252
16403
  if (isProject) {
16253
16404
  for (const key of ["id", "brand", "style", "outputProfile"]) if (typeof doc[key] !== "string" || !identifier3.test(doc[key])) add(`${key} must be a valid identifier`);
16254
- if (!object4(doc.intent) || !nonempty2(doc.intent.topic) || !nonempty2(doc.intent.goal)) add("intent requires topic and goal");
16405
+ if (!object5(doc.intent) || !nonempty2(doc.intent.topic) || !nonempty2(doc.intent.goal)) add("intent requires topic and goal");
16255
16406
  if (doc.reviewMode !== void 0 && !["direct", "directions", "sample"].includes(doc.reviewMode)) add("reviewMode must be direct, directions or sample");
16256
16407
  if (doc.mediaStyleOverrides !== void 0) for (const issue of mediaStyleOverrideIssues(doc.mediaStyleOverrides)) add(issue);
16257
16408
  if (doc.script !== void 0 && typeof doc.script !== "string") add("script must be text");
16258
16409
  if (doc.overrides !== void 0) {
16259
- if (!object4(doc.overrides)) add("overrides must be an object");
16410
+ if (!object5(doc.overrides)) add("overrides must be an object");
16260
16411
  else for (const [key, rule] of Object.entries(doc.overrides)) {
16261
16412
  if (!tokenName.test(key) || ["__proto__", "constructor", "prototype"].includes(key)) add(`Invalid override token ${key}. ${tokenNameHelp}`);
16262
- if (!object4(rule) || !tokenValue(rule.value) || !nonempty2(rule.reason) || rule.overrideRequired !== void 0 && typeof rule.overrideRequired !== "boolean") add(`Override ${key} requires value and a reason; overrideRequired must be boolean`);
16413
+ if (!object5(rule) || !tokenValue(rule.value) || !nonempty2(rule.reason) || rule.overrideRequired !== void 0 && typeof rule.overrideRequired !== "boolean") add(`Override ${key} requires value and a reason; overrideRequired must be boolean`);
16263
16414
  }
16264
16415
  }
16265
16416
  }
@@ -16269,7 +16420,7 @@ function validateDocument(path, content) {
16269
16420
  else {
16270
16421
  const ids = /* @__PURE__ */ new Set();
16271
16422
  for (const asset of doc.assets) {
16272
- if (!object4(asset) || typeof asset.id !== "string" || !/^[a-f0-9]{64}$/.test(asset.id) || !nonempty2(asset.name) || typeof asset.mimeType !== "string" || !/^(video|image|audio)\/[a-zA-Z0-9][a-zA-Z0-9.+-]*$/.test(asset.mimeType) || !Number.isSafeInteger(asset.bytes) || Number(asset.bytes) < 1) {
16423
+ if (!object5(asset) || typeof asset.id !== "string" || !/^[a-f0-9]{64}$/.test(asset.id) || !nonempty2(asset.name) || typeof asset.mimeType !== "string" || !/^(video|image|audio)\/[a-zA-Z0-9][a-zA-Z0-9.+-]*$/.test(asset.mimeType) || !Number.isSafeInteger(asset.bytes) || Number(asset.bytes) < 1) {
16273
16424
  add("Production assets require a SHA-256 id, name, media mimeType and positive integer bytes");
16274
16425
  continue;
16275
16426
  }
@@ -16283,10 +16434,10 @@ function validateDocument(path, content) {
16283
16434
  for (const key of ["width", "height"]) if (!Number.isInteger(doc[key]) || Number(doc[key]) < 1 || Number(doc[key]) > 16384) add(`${key} must be an integer between 1 and 16384`);
16284
16435
  if (!finite(doc.fps) || doc.fps <= 0 || doc.fps > 240) add("fps must be between 0 and 240");
16285
16436
  if (doc.safeAreas !== void 0) {
16286
- if (!object4(doc.safeAreas) || ["top", "right", "bottom", "left"].some((k) => !finite(doc.safeAreas[k]) || Number(doc.safeAreas[k]) < 0 || Number(doc.safeAreas[k]) >= 0.5)) add("safeAreas must have normalized fractions from 0 up to (not including) 0.5 for all four sides");
16437
+ if (!object5(doc.safeAreas) || ["top", "right", "bottom", "left"].some((k) => !finite(doc.safeAreas[k]) || Number(doc.safeAreas[k]) < 0 || Number(doc.safeAreas[k]) >= 0.5)) add("safeAreas must have normalized fractions from 0 up to (not including) 0.5 for all four sides");
16287
16438
  }
16288
16439
  if (doc.captions !== void 0) {
16289
- if (!object4(doc.captions)) add("captions must be an object");
16440
+ if (!object5(doc.captions)) add("captions must be an object");
16290
16441
  else {
16291
16442
  if (typeof doc.captions.enabled !== "boolean") add("captions.enabled must be a boolean");
16292
16443
  if (doc.captions.language !== void 0 && !nonempty2(doc.captions.language)) add("captions.language must be nonempty text");
@@ -16325,13 +16476,13 @@ function validateWorkspace(files) {
16325
16476
  return { valid: issues.length === 0, issues };
16326
16477
  }
16327
16478
  function stableJson(value) {
16328
- const sort = (item) => Array.isArray(item) ? item.map(sort) : object4(item) ? Object.fromEntries(Object.keys(item).sort().map((key) => [key, sort(item[key])])) : item;
16479
+ const sort = (item) => Array.isArray(item) ? item.map(sort) : object5(item) ? Object.fromEntries(Object.keys(item).sort().map((key) => [key, sort(item[key])])) : item;
16329
16480
  return JSON.stringify(sort(value), null, 2) + "\n";
16330
16481
  }
16331
16482
  async function sha256(content) {
16332
16483
  const bytes = typeof content === "string" ? new TextEncoder().encode(content) : content;
16333
- const digest2 = await crypto.subtle.digest("SHA-256", new Uint8Array(bytes));
16334
- return Array.from(new Uint8Array(digest2), (b) => b.toString(16).padStart(2, "0")).join("");
16484
+ const digest3 = await crypto.subtle.digest("SHA-256", new Uint8Array(bytes));
16485
+ return Array.from(new Uint8Array(digest3), (b) => b.toString(16).padStart(2, "0")).join("");
16335
16486
  }
16336
16487
  async function resolveProject(files, projectId) {
16337
16488
  files = canonicalFiles(files);
@@ -16528,42 +16679,263 @@ function canonicalFiles(files) {
16528
16679
  return result2;
16529
16680
  }
16530
16681
 
16531
- // packages/cli/src/io.ts
16682
+ // packages/cli/src/media-runtime.ts
16683
+ import { spawn } from "node:child_process";
16684
+ import { createHash } from "node:crypto";
16532
16685
  import { constants } from "node:fs";
16533
- import { lstat, mkdir, open, readdir, realpath, rename, unlink, link } from "node:fs/promises";
16534
- import { dirname, join, relative, resolve, sep } from "node:path";
16535
- import { randomUUID } from "node:crypto";
16536
- async function ensureRoot(directory, create = false) {
16537
- const root = resolve(directory);
16538
- if (create) await mkdir(root, { recursive: true });
16539
- const info = await lstat(root);
16540
- if (info.isSymbolicLink() || !info.isDirectory()) throw new Error("Workspace root must be a real directory, not a symlink");
16541
- return realpath(root);
16542
- }
16543
- async function checkedPath(root, path, document2 = true) {
16544
- if (document2) safeDocumentPath(path);
16545
- const target = resolve(root, path);
16546
- const rel = relative(root, target);
16547
- if (!rel || rel.startsWith(`..${sep}`) || rel === ".." || resolve(path) === path) throw new Error("Path escapes workspace");
16548
- let cursor = root;
16549
- for (const segment of rel.split(sep)) {
16550
- cursor = join(cursor, segment);
16551
- try {
16552
- const info = await lstat(cursor);
16553
- if (info.isSymbolicLink()) throw new Error(`Symlinks are not allowed: ${path}`);
16554
- } catch (error) {
16555
- if (error.code !== "ENOENT") throw error;
16556
- }
16686
+ import { lstat, open, realpath } from "node:fs/promises";
16687
+ import { dirname, extname, parse, resolve } from "node:path";
16688
+
16689
+ // packages/cli/src/errors.ts
16690
+ var CliError = class extends Error {
16691
+ constructor(code, message, hint, retryable = false, details = {}) {
16692
+ super(message);
16693
+ this.code = code;
16694
+ this.hint = hint;
16695
+ this.retryable = retryable;
16696
+ this.details = details;
16557
16697
  }
16558
- return target;
16559
- }
16560
- async function readText(root, path, document2 = true) {
16561
- const target = await checkedPath(root, path, document2);
16562
- const handle = await open(target, constants.O_RDONLY | constants.O_NOFOLLOW);
16563
- try {
16564
- const info = await handle.stat();
16565
- if (!info.isFile()) throw new Error(`Not a regular file: ${path}`);
16566
- if (document2 && info.size > MAX_DOCUMENT_BYTES) throw new Error(`Document exceeds 128 KiB: ${path}`);
16698
+ code;
16699
+ hint;
16700
+ retryable;
16701
+ details;
16702
+ };
16703
+ function errorOutput(error) {
16704
+ if (error instanceof CliError) return { error: error.message, code: error.code, hint: error.hint, retryable: error.retryable, ...error.details };
16705
+ if (error instanceof ValidationError) return { error: "Workspace validation failed.", code: "VALIDATION_FAILED", retryable: false, hint: "Correct the reported documents before retrying.", issues: error.issues };
16706
+ const message = error instanceof Error ? error.message : "Unknown error";
16707
+ const safe = message.replace(/\b(?:eh_[a-f0-9]{64}|npm_[A-Za-z0-9]+)\b/g, "[REDACTED]").replace(/Bearer\s+\S+/gi, "Bearer [REDACTED]").replace(/(https?:\/\/[^\s?]+)\?[^\s]*/g, "$1?[REDACTED]");
16708
+ return { error: safe, code: "COMMAND_FAILED", retryable: false, hint: "Read the command help and run editorbrain doctor. Do not retry writes blindly." };
16709
+ }
16710
+
16711
+ // packages/cli/src/media-runtime.ts
16712
+ var MediaToolError = class extends CliError {
16713
+ constructor(code, message) {
16714
+ super(code, message, code === "MEDIA_TOOL_NOT_FOUND" ? "Install FFmpeg/FFprobe separately and make them available on PATH; no account or model is needed." : "Review the local input and reported limit. An incomplete check does not validate the media.");
16715
+ this.name = "MediaToolError";
16716
+ }
16717
+ };
16718
+ var SUPPORTED_MEDIA_EXTENSIONS = /* @__PURE__ */ new Set([
16719
+ ".mp4",
16720
+ ".mov",
16721
+ ".m4v",
16722
+ ".mkv",
16723
+ ".webm",
16724
+ ".avi",
16725
+ ".mpg",
16726
+ ".mpeg",
16727
+ ".mts",
16728
+ ".m2ts",
16729
+ ".ts",
16730
+ ".ogv",
16731
+ ".wmv",
16732
+ ".wav",
16733
+ ".mp3",
16734
+ ".m4a",
16735
+ ".aac",
16736
+ ".flac",
16737
+ ".ogg",
16738
+ ".opus",
16739
+ ".aif",
16740
+ ".aiff",
16741
+ ".ac3",
16742
+ ".eac3",
16743
+ ".jpg",
16744
+ ".jpeg",
16745
+ ".png",
16746
+ ".webp",
16747
+ ".bmp",
16748
+ ".tif",
16749
+ ".tiff",
16750
+ ".gif",
16751
+ ".avif",
16752
+ ".heic",
16753
+ ".heif"
16754
+ ]);
16755
+ var PROTOCOLS = "file,pipe";
16756
+ var FORMATS = "mov,matroska,webm,avi,mpeg,mpegts,ogg,wav,flac,mp3,aac,aiff,ac3,eac3,asf,jpeg_pipe,png_pipe,webp_pipe,bmp_pipe,tiff_pipe,gif,heif,avif";
16757
+ var DEFAULT_TIMEOUT = 3e4;
16758
+ var DEFAULT_OUTPUT_LIMIT = 2 * 1024 * 1024;
16759
+ function resolveLocalMediaPath(value) {
16760
+ if (!value || value.includes("\0") || /[\r\n]/.test(value) || /^[a-z][a-z\d+.-]*:/i.test(value) && !(process.platform === "win32" && /^[a-z]:[\\/]/i.test(value)) || value.startsWith("\\\\")) {
16761
+ throw new MediaToolError("MEDIA_LOCAL_PATH_REQUIRED", "Provide a local filesystem path, not a URL, protocol or network share.");
16762
+ }
16763
+ return resolve(value);
16764
+ }
16765
+ async function assertNoMediaSymlinks(value) {
16766
+ const path = resolveLocalMediaPath(value);
16767
+ let cursor = path;
16768
+ while (cursor !== parse(cursor).root) {
16769
+ const info = await lstat(cursor);
16770
+ if (info.isSymbolicLink()) {
16771
+ const systemAlias = process.platform === "darwin" && (cursor === "/tmp" || cursor === "/var") && await realpath(cursor) === `/private${cursor}`;
16772
+ if (!systemAlias) throw new MediaToolError("MEDIA_SYMLINK", "Symlink media paths are not followed. Use the original local path.");
16773
+ }
16774
+ cursor = dirname(cursor);
16775
+ }
16776
+ return path;
16777
+ }
16778
+ async function assertLocalMediaFile(value) {
16779
+ const path = await assertNoMediaSymlinks(value);
16780
+ const info = await lstat(path);
16781
+ if (!info.isFile()) throw new MediaToolError("MEDIA_NOT_REGULAR", "The media input must be a regular local file.");
16782
+ if (!SUPPORTED_MEDIA_EXTENSIONS.has(extname(path).toLowerCase())) throw new MediaToolError("MEDIA_FORMAT_UNSUPPORTED", "This file extension is not supported for local media inspection.");
16783
+ return path;
16784
+ }
16785
+ function localInputArgs(path) {
16786
+ return ["-protocol_whitelist", PROTOCOLS, "-format_whitelist", FORMATS, "-i", resolveLocalMediaPath(path)];
16787
+ }
16788
+ async function runMediaTool(tool, args, options = {}) {
16789
+ if (tool !== "ffmpeg" && tool !== "ffprobe") throw new MediaToolError("MEDIA_TOOL_INVALID", "Unsupported media executable.");
16790
+ const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT;
16791
+ const maxOutputBytes = options.maxOutputBytes ?? DEFAULT_OUTPUT_LIMIT;
16792
+ if (!Number.isInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 10 * 6e4 || !Number.isInteger(maxOutputBytes) || maxOutputBytes < 1 || maxOutputBytes > 16 * 1024 * 1024) {
16793
+ throw new MediaToolError("MEDIA_TOOL_LIMITS_INVALID", "Media runtime limits are outside the supported range.");
16794
+ }
16795
+ if (!Array.isArray(args) || args.length > 200 || args.some((arg) => typeof arg !== "string" || arg.length > 8192 || arg.includes("\0"))) {
16796
+ throw new MediaToolError("MEDIA_TOOL_ARGUMENTS_INVALID", "Invalid media tool arguments.");
16797
+ }
16798
+ for (let i = 0; i < args.length; i++) {
16799
+ const arg = args[i];
16800
+ if (/(?:^|[=,;\s])(?:https?|ftp|sftp|tcp|udp|rtsp|rtmp|smb|data|concat|subfile|crypto):/i.test(arg) || arg === "-safe" && args[i + 1] === "0" || arg === "-enable_drefs" && args[i + 1] !== "0") {
16801
+ throw new MediaToolError("MEDIA_REMOTE_INPUT_FORBIDDEN", "Only self-contained local media inputs are allowed.");
16802
+ }
16803
+ if (arg === "-protocol_whitelist" && args[i + 1] !== PROTOCOLS || arg === "-format_whitelist" && args[i + 1] !== FORMATS) {
16804
+ throw new MediaToolError("MEDIA_TOOL_ARGUMENTS_INVALID", "The local media allowlists cannot be widened.");
16805
+ }
16806
+ if (arg === "-i") {
16807
+ await assertLocalMediaFile(args[i + 1] ?? "");
16808
+ if (args[i - 4] !== "-protocol_whitelist" || args[i - 3] !== PROTOCOLS || args[i - 2] !== "-format_whitelist" || args[i - 1] !== FORMATS) {
16809
+ throw new MediaToolError("MEDIA_TOOL_ARGUMENTS_INVALID", "Every media input must use localInputArgs.");
16810
+ }
16811
+ }
16812
+ }
16813
+ return new Promise((resolveResult, reject) => {
16814
+ const stdout = [], stderr = [];
16815
+ let size = 0, done = false;
16816
+ const child = spawn(tool, ["-hide_banner", ...tool === "ffmpeg" ? ["-nostdin"] : [], ...args], { shell: false, stdio: ["ignore", "pipe", "pipe"], windowsHide: true });
16817
+ const fail2 = (error) => {
16818
+ if (done) return;
16819
+ done = true;
16820
+ clearTimeout(timer);
16821
+ child.kill("SIGKILL");
16822
+ reject(error);
16823
+ };
16824
+ const timer = setTimeout(() => fail2(new MediaToolError("MEDIA_TOOL_TIMEOUT", "The local media check exceeded its time limit. No complete result is available.")), timeoutMs);
16825
+ const collect = (target) => (chunk) => {
16826
+ if (done) return;
16827
+ size += chunk.length;
16828
+ if (size > maxOutputBytes) return fail2(new MediaToolError("MEDIA_TOOL_OUTPUT_LIMIT", "The local media check exceeded its output limit. No complete result is available."));
16829
+ target.push(chunk);
16830
+ };
16831
+ child.stdout.on("data", collect(stdout));
16832
+ child.stderr.on("data", collect(stderr));
16833
+ child.on("error", (error) => fail2(new MediaToolError(error.code === "ENOENT" ? "MEDIA_TOOL_NOT_FOUND" : "MEDIA_TOOL_FAILED", error.code === "ENOENT" ? `${tool} is not installed or available on PATH. Install it separately to use local media checks.` : "The local media executable could not run.")));
16834
+ child.on("close", (code) => {
16835
+ if (done) return;
16836
+ done = true;
16837
+ clearTimeout(timer);
16838
+ if (code !== 0) return reject(new MediaToolError("MEDIA_TOOL_FAILED", "The local media tool could not inspect or decode this input. No complete result is available."));
16839
+ resolveResult({ stdout: Buffer.concat(stdout).toString("utf8"), stderr: Buffer.concat(stderr).toString("utf8") });
16840
+ });
16841
+ });
16842
+ }
16843
+ var finite2 = (value) => {
16844
+ if (typeof value !== "number" && typeof value !== "string" || value === "") return null;
16845
+ const n = Number(value);
16846
+ return Number.isFinite(n) ? n : null;
16847
+ };
16848
+ var nonnegative2 = (value) => {
16849
+ const n = finite2(value);
16850
+ return n !== null && n >= 0 ? n : null;
16851
+ };
16852
+ var positiveInteger = (value) => {
16853
+ const n = finite2(value);
16854
+ return n !== null && Number.isInteger(n) && n > 0 ? n : void 0;
16855
+ };
16856
+ var rate2 = (value) => {
16857
+ if (typeof value !== "string" || !/^\d+(?:\/\d+)?$/.test(value)) return null;
16858
+ const [n, d = "1"] = value.split("/");
16859
+ const result2 = Number(n) / Number(d);
16860
+ return Number.isFinite(result2) && result2 > 0 ? result2 : null;
16861
+ };
16862
+ var sameFile = (a, b) => a.dev === b.dev && a.ino === b.ino && a.size === b.size && a.mtimeMs === b.mtimeMs && a.ctimeMs === b.ctimeMs;
16863
+ async function probeLocalMedia(value) {
16864
+ const path = await assertLocalMediaFile(value);
16865
+ const handle = await open(path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
16866
+ try {
16867
+ const before = await handle.stat();
16868
+ if (!before.isFile()) throw new MediaToolError("MEDIA_NOT_REGULAR", "The media input must be a regular local file.");
16869
+ const entries = "format=duration,start_time:stream=index,codec_type,codec_name,width,height,avg_frame_rate,sample_rate,channels,duration,color_transfer,sample_aspect_ratio,pix_fmt:stream_disposition=attached_pic";
16870
+ const { stdout } = await runMediaTool("ffprobe", ["-v", "error", "-show_entries", entries, "-of", "json", ...localInputArgs(path)]);
16871
+ const hash6 = createHash("sha256");
16872
+ for await (const chunk of handle.createReadStream({ autoClose: false })) hash6.update(chunk);
16873
+ const after = await handle.stat();
16874
+ await assertNoMediaSymlinks(path);
16875
+ const current2 = await lstat(path);
16876
+ if (!sameFile(before, after) || !sameFile(after, current2)) throw new MediaToolError("MEDIA_FILE_CHANGED", "The media file changed while it was being inspected. Retry with a stable source.");
16877
+ let raw;
16878
+ try {
16879
+ raw = JSON.parse(stdout);
16880
+ } catch {
16881
+ throw new MediaToolError("MEDIA_PROBE_INVALID", "The media probe returned invalid metadata.");
16882
+ }
16883
+ if (!raw || !Array.isArray(raw.streams) || raw.streams.length > 256 || raw.streams.some((stream) => !stream || typeof stream !== "object" || Array.isArray(stream))) throw new MediaToolError("MEDIA_PROBE_INVALID", "The media probe returned invalid or excessive stream metadata.");
16884
+ const streams = raw.streams.map((stream, offset) => {
16885
+ const type = typeof stream.codec_type === "string" && /^(video|audio|subtitle|data|attachment|unknown)$/.test(stream.codec_type) ? stream.codec_type : "unknown";
16886
+ const codec = typeof stream.codec_name === "string" && /^[a-zA-Z0-9_]{1,64}$/.test(stream.codec_name) ? stream.codec_name : null;
16887
+ const index = finite2(stream.index);
16888
+ return {
16889
+ index: index !== null && Number.isInteger(index) && index >= 0 ? index : offset,
16890
+ type,
16891
+ codec,
16892
+ durationSeconds: nonnegative2(stream.duration),
16893
+ ...type === "video" ? { width: positiveInteger(stream.width), height: positiveInteger(stream.height), fps: rate2(stream.avg_frame_rate), ...typeof stream.sample_aspect_ratio === "string" ? { sampleAspectRatio: stream.sample_aspect_ratio } : {}, ...typeof stream.pix_fmt === "string" ? { pixelFormat: stream.pix_fmt } : {}, ...typeof stream.color_transfer === "string" ? { colorTransfer: stream.color_transfer } : {}, attachedPicture: stream.disposition?.attached_pic === 1 } : {},
16894
+ ...type === "audio" ? { sampleRate: nonnegative2(stream.sample_rate), channels: positiveInteger(stream.channels) ?? null } : {}
16895
+ };
16896
+ });
16897
+ return { path, sha256: hash6.digest("hex"), bytes: after.size, durationSeconds: nonnegative2(raw.format?.duration), startTimeSeconds: finite2(raw.format?.start_time), streams };
16898
+ } finally {
16899
+ await handle.close();
16900
+ }
16901
+ }
16902
+
16903
+ // packages/cli/src/io.ts
16904
+ import { constants as constants2 } from "node:fs";
16905
+ import { lstat as lstat2, mkdir, open as open2, readdir, realpath as realpath2, rename, unlink, link } from "node:fs/promises";
16906
+ import { dirname as dirname2, join, relative, resolve as resolve2, sep } from "node:path";
16907
+ import { randomUUID } from "node:crypto";
16908
+ async function ensureRoot(directory, create = false) {
16909
+ const root = resolve2(directory);
16910
+ if (create) await mkdir(root, { recursive: true });
16911
+ const info = await lstat2(root);
16912
+ if (info.isSymbolicLink() || !info.isDirectory()) throw new Error("Workspace root must be a real directory, not a symlink");
16913
+ return realpath2(root);
16914
+ }
16915
+ async function checkedPath(root, path, document2 = true) {
16916
+ if (document2) safeDocumentPath(path);
16917
+ const target = resolve2(root, path);
16918
+ const rel = relative(root, target);
16919
+ if (!rel || rel.startsWith(`..${sep}`) || rel === ".." || resolve2(path) === path) throw new Error("Path escapes workspace");
16920
+ let cursor = root;
16921
+ for (const segment of rel.split(sep)) {
16922
+ cursor = join(cursor, segment);
16923
+ try {
16924
+ const info = await lstat2(cursor);
16925
+ if (info.isSymbolicLink()) throw new Error(`Symlinks are not allowed: ${path}`);
16926
+ } catch (error) {
16927
+ if (error.code !== "ENOENT") throw error;
16928
+ }
16929
+ }
16930
+ return target;
16931
+ }
16932
+ async function readText(root, path, document2 = true) {
16933
+ const target = await checkedPath(root, path, document2);
16934
+ const handle = await open2(target, constants2.O_RDONLY | constants2.O_NOFOLLOW);
16935
+ try {
16936
+ const info = await handle.stat();
16937
+ if (!info.isFile()) throw new Error(`Not a regular file: ${path}`);
16938
+ if (document2 && info.size > MAX_DOCUMENT_BYTES) throw new Error(`Document exceeds 128 KiB: ${path}`);
16567
16939
  return new TextDecoder("utf-8", { fatal: true }).decode(await handle.readFile());
16568
16940
  } finally {
16569
16941
  await handle.close();
@@ -16571,10 +16943,10 @@ async function readText(root, path, document2 = true) {
16571
16943
  }
16572
16944
  async function atomicWrite(root, path, content, options = {}) {
16573
16945
  const target = await checkedPath(root, path, options.document ?? true);
16574
- await mkdir(dirname(target), { recursive: true });
16946
+ await mkdir(dirname2(target), { recursive: true });
16575
16947
  await checkedPath(root, path, options.document ?? true);
16576
- const temporary = join(dirname(target), `.eh-${randomUUID()}.tmp`);
16577
- const handle = await open(temporary, "wx", options.mode ?? 420);
16948
+ const temporary = join(dirname2(target), `.eh-${randomUUID()}.tmp`);
16949
+ const handle = await open2(temporary, "wx", options.mode ?? 420);
16578
16950
  try {
16579
16951
  await handle.writeFile(content, "utf8");
16580
16952
  await handle.close();
@@ -16643,32 +17015,280 @@ async function readProjectWorkspace(root, projectId) {
16643
17015
  return canonicalFiles(files);
16644
17016
  }
16645
17017
 
16646
- // packages/cli/src/client.ts
16647
- import { setTimeout as delay } from "node:timers/promises";
17018
+ // packages/cli/src/production-context.ts
17019
+ async function prepareProductionContext(root, projectId, client) {
17020
+ const resolved = await resolveProject(await readProjectWorkspace(root, projectId), projectId);
17021
+ const sourceKey = stableJson(resolved.lock.sourceHashes);
17022
+ let contextVersion;
17023
+ const remote = async () => {
17024
+ const result2 = await client.request("/v1/workflow/check", { method: "POST", body: { projectId } });
17025
+ const snapshot = result2.editorialSnapshot;
17026
+ if (!snapshot || snapshot.projectId !== projectId || typeof snapshot.contextVersion !== "string" || !/^[a-f0-9]{64}$/.test(snapshot.contextVersion) || !snapshot.sourceHashes || typeof snapshot.sourceHashes !== "object" || Array.isArray(snapshot.sourceHashes) || Object.values(snapshot.sourceHashes).some((hash6) => typeof hash6 !== "string" || !/^[a-f0-9]{64}$/.test(hash6))) throw new CliError("EDITORIAL_CONTEXT_UNAVAILABLE", "The service did not return a versioned editorial context.", "Update the service before connected verification, or explicitly use --offline to check local documents only.");
17027
+ if (stableJson(snapshot.sourceHashes) !== sourceKey) throw new CliError("PRODUCTION_CONTEXT_CONFLICT", "Local and web project preferences or material differ.", "Use status and pull --project to inspect/reconcile changes. Do not overwrite either side automatically.");
17028
+ return snapshot.contextVersion;
17029
+ };
17030
+ if (client) contextVersion = await remote();
17031
+ else contextVersion = await sha256(sourceKey);
17032
+ const validateContext = async () => {
17033
+ const localUnchanged = async () => stableJson((await resolveProject(await readProjectWorkspace(root, projectId), projectId)).lock.sourceHashes) === sourceKey;
17034
+ if (!await localUnchanged()) return false;
17035
+ if (!client) return true;
17036
+ try {
17037
+ if (await remote() !== contextVersion) return false;
17038
+ } catch (error) {
17039
+ if (error instanceof CliError && error.code === "PRODUCTION_CONTEXT_CONFLICT") return false;
17040
+ throw error;
17041
+ }
17042
+ return localUnchanged();
17043
+ };
17044
+ return { resolved, contextVersion, validateContext, contextScope: client ? "local_and_remote" : "local_only" };
17045
+ }
17046
+
17047
+ // packages/cli/src/sfx.ts
17048
+ var localPath = (id2) => {
17049
+ sfxBankPath(id2);
17050
+ return `.editorbrain-local/sfx/${id2}.json`;
17051
+ };
17052
+ async function indexSfx(directory, input, bankId) {
17053
+ const root = await ensureRoot(directory), target = localPath(bankId), folder = await assertNoMediaSymlinks(input);
17054
+ if (!(await lstat3(folder)).isDirectory()) throw new Error("SFX input must be a directory.");
17055
+ const entries = [], paths2 = {}, failures = [];
17056
+ const files = await readdir2(folder, { withFileTypes: true });
17057
+ const selected = files.filter((e) => e.isFile() && [".wav", ".mp3", ".m4a", ".ogg", ".flac", ".aif", ".aiff"].includes(extname2(e.name).toLowerCase()));
17058
+ if (selected.length > 100) throw new Error("Choose a folder with up to 100 audio files; split larger banks by purpose. No files were changed.");
17059
+ for (const file of selected.sort((a, b) => a.name.localeCompare(b.name))) {
17060
+ try {
17061
+ const probe = await probeLocalMedia(join2(folder, file.name));
17062
+ if (!probe.durationSeconds || probe.durationSeconds > 600 || !probe.streams.some((s) => s.type === "audio") || probe.streams.some((s) => s.type === "video" && !s.attachedPicture)) throw new Error("Expected a measured audio effect up to 600 seconds.");
17063
+ const id2 = "sfx-" + probe.sha256.slice(0, 24);
17064
+ if (paths2[id2]) continue;
17065
+ entries.push({ id: id2, name: basename(file.name, extname2(file.name)).slice(0, 180), tags: ["filename-indexed"], durationSeconds: probe.durationSeconds, sha256: probe.sha256, license: { kind: "unverified", note: "Local user file. Verify this file\u2019s license for the intended video; not approved for redistribution." }, audition: "unreviewed" });
17066
+ paths2[id2] = probe.path;
17067
+ } catch (e) {
17068
+ failures.push({ name: file.name, error: e.message });
17069
+ }
17070
+ }
17071
+ const bank = validateSfxBank({ schemaVersion: 1, bankId, entries });
17072
+ await atomicWrite(root, target, stableJson({ schemaVersion: 1, bank, paths: paths2 }), { document: false, overwrite: false, mode: 384 });
17073
+ return { bankId, entries: entries.length, failures, catalogPath: target, mediaUploaded: false, contentReviewed: false, notice: "Local metadata index only; filenames are hints, not auditory analysis. No originals copied or modified. Existing banks are never overwritten. Use --include-unverified only for discovery; verify rights before use." };
17074
+ }
17075
+ async function localSfx(options) {
17076
+ const root = await ensureRoot(options.directory), context = await prepareProductionContext(root, options.projectId);
17077
+ const audio = context.resolved.audioProduction, policy = sfxPolicy(audio, options.sceneRole);
17078
+ if (!policy.enabled) {
17079
+ if (options.soundId) throw new Error(policy.reason);
17080
+ return { items: [], total: 0, policy, contextHash: context.contextVersion, scope: "local_only" };
17081
+ }
17082
+ const id2 = options.bankId ?? policy.bankId;
17083
+ if (!id2) throw new Error("Choose --bank or configure audio.json sfx.library.bankId. Local mode never queries the cloud.");
17084
+ if (policy.bankId && policy.bankId !== id2) throw new Error("Bank differs from project preferences. Save a project exception first.");
17085
+ const serialized = await readText(root, localPath(id2), false);
17086
+ if (Buffer.byteLength(serialized) > 256 * 1024) throw new Error("Local SFX index exceeds 256 KiB.");
17087
+ const saved = JSON.parse(serialized), bank = validateSfxBank(saved.bank), catalogSha256 = await sha256(stableJson(bank));
17088
+ if (bank.bankId !== id2) throw new Error("Local SFX bank ID mismatch.");
17089
+ if (options.soundId) {
17090
+ if (options.catalogSha256 !== catalogSha256) throw new Error("Catalog changed or pin missing. Search again and pass --catalog-sha256.");
17091
+ const entry = bank.entries.find((e) => e.id === options.soundId), path = saved.paths?.[options.soundId];
17092
+ if (!entry || typeof path !== "string") throw new Error("Sound not found.");
17093
+ if (entry.license.kind === "unverified" && !options.includeUnverified) throw new Error("Verify rights first. --include-unverified permits local audition only, never publication.");
17094
+ const probe = await probeLocalMedia(path);
17095
+ if (probe.sha256 !== entry.sha256) throw new Error("Original sound changed; index it as a new bank before use.");
17096
+ if (!await context.validateContext()) throw new Error("Audio preferences changed; retry with current context.");
17097
+ return { entry, path, catalogSha256, policy, contextHash: context.contextVersion, scope: "local_only", usage: entry.license.kind === "unverified" ? "audition_only" : "verify_license_for_project" };
17098
+ }
17099
+ const result2 = searchSfx(bank, { query: options.query ?? "", limit: options.limit, audio, sceneRole: options.sceneRole, includeUnverified: options.includeUnverified });
17100
+ if (!await context.validateContext()) throw new Error("Audio preferences changed; retry.");
17101
+ return { ...result2, catalogSha256, contextHash: context.contextVersion, scope: "local_only" };
17102
+ }
17103
+ async function checkLocalSfx(directory, projectId, plan) {
17104
+ const root = await ensureRoot(directory), context = await prepareProductionContext(root, projectId);
17105
+ const content = await readText(root, plan, false);
17106
+ if (Buffer.byteLength(content) > 256 * 1024) throw new Error("SFX plan too large.");
17107
+ const result2 = checkSfxCues(context.resolved.audioProduction, JSON.parse(content));
17108
+ if (!await context.validateContext()) throw new Error("Preferences changed during the check.");
17109
+ return { ...result2, contextHash: context.contextVersion, scope: "local_only" };
17110
+ }
17111
+ async function remoteSfx(client, subcommand, params) {
17112
+ return client.request(`/v1/sfx/${subcommand}?${new URLSearchParams(params)}`);
17113
+ }
17114
+
17115
+ // packages/cli/src/media-preview.ts
17116
+ import { chmod, link as link2, mkdtemp, rm } from "node:fs/promises";
17117
+ import { dirname as dirname4, join as join3 } from "node:path";
17118
+
17119
+ // packages/cli/src/production-output.ts
17120
+ import { constants as constants3 } from "node:fs";
17121
+ import { mkdir as mkdir2, open as open3 } from "node:fs/promises";
17122
+ import { dirname as dirname3 } from "node:path";
17123
+ async function productionOutputPath(root, relativePath) {
17124
+ if (!relativePath.startsWith(".editorbrain-local/") || relativePath.split(/[\\/]/).some((p) => !p || p === ".." || p === ".") || /[\\\x00-\x1f\x7f]/.test(relativePath)) {
17125
+ throw new CliError("LOCAL_OUTPUT_REQUIRED", "Production output must be inside .editorbrain-local/.", "Use --out .editorbrain-local/<project>/inventory.json, or omit --out for JSON stdout. These reports are not synced.");
17126
+ }
17127
+ return checkedPath(root, relativePath, false);
17128
+ }
17129
+ async function prepareSampleDirectory(root, path) {
17130
+ const target = await productionOutputPath(root, path);
17131
+ await mkdir2(dirname3(target), { recursive: true });
17132
+ await productionOutputPath(root, path);
17133
+ return target;
17134
+ }
17135
+ async function readProductionJson(root, path) {
17136
+ const target = await checkedPath(root, path, false);
17137
+ const handle = await open3(target, constants3.O_RDONLY | constants3.O_NOFOLLOW | constants3.O_NONBLOCK);
17138
+ try {
17139
+ const stat = await handle.stat();
17140
+ if (!stat.isFile() || stat.size > 16 * 1024 * 1024) throw new CliError("INVALID_REPORT", "Expected a regular JSON file of at most 16 MiB.", "Select a local inventory, plan or check profile.");
17141
+ return JSON.parse(await handle.readFile("utf8"));
17142
+ } finally {
17143
+ await handle.close();
17144
+ }
17145
+ }
17146
+ async function saveProductionReport(root, path, report) {
17147
+ const target = await productionOutputPath(root, path);
17148
+ if (!path.endsWith(".json")) throw new CliError("INVALID_ARGUMENT", "Report output needs a .json extension.", "Use a new path under .editorbrain-local/.");
17149
+ await atomicWrite(root, path, stableJson(report), { document: false, overwrite: false, mode: 384 });
17150
+ return target;
17151
+ }
17152
+ function summarizeProductionReport(value) {
17153
+ const r = value;
17154
+ const base = { schemaVersion: r.schemaVersion, kind: r.kind, summary: true, mediaUploaded: false, quality: "not_assessed" };
17155
+ if (r.kind === "editorbrain.media-inventory") return {
17156
+ ...base,
17157
+ complete: r.complete,
17158
+ contentReviewed: false,
17159
+ counts: { assets: r.assets.length, failures: r.failures.length, skipped: r.skipped.length },
17160
+ assets: r.assets.slice(0, 8),
17161
+ failures: r.failures.slice(0, 5),
17162
+ omitted: { assets: Math.max(0, r.assets.length - 8), failures: Math.max(0, r.failures.length - 5), skipped: r.skipped.length },
17163
+ next: "Inspect selected source content. Use media sample for specific frames. Read the saved report or rerun with --details for omitted metadata; no content was reviewed automatically."
17164
+ };
17165
+ if (r.kind === "editorbrain.media-samples") return { ...base, sourceSha256: r.source.sha256, frames: r.frames, contentReviewed: false, next: "View these frames to inspect composition. They do not prove motion or sound." };
17166
+ if (r.kind === "editorbrain.production-plan-check") return {
17167
+ ...base,
17168
+ valid: r.valid,
17169
+ inventoryComplete: r.inventoryComplete,
17170
+ observations: r.observations,
17171
+ sourceFilesReverified: false,
17172
+ counts: { sources: r.coverage.length, findings: r.findings.length, repeatedPairs: r.repeatedPairs ?? 0 },
17173
+ findings: [...r.findings].sort((a, b) => ({ error: 0, warning: 1, info: 2 })[a.severity] - { error: 0, warning: 1, info: 2 }[b.severity]).slice(0, 8),
17174
+ omittedFindings: Math.max(0, r.findings.length - 8),
17175
+ next: "Review source reuse and inspection gaps in the saved report. Observations are agent-declared, not independently certified."
17176
+ };
17177
+ if (r.kind === "editorbrain.render-check") return {
17178
+ ...base,
17179
+ complete: r.complete,
17180
+ technicalStatus: r.technicalStatus,
17181
+ file: r.file,
17182
+ checks: r.checks,
17183
+ findings: r.findings.slice(0, 8),
17184
+ findingsSummary: r.findingsSummary,
17185
+ omittedFindings: Math.max(0, r.findings.length - 8),
17186
+ next: "Reconcile technical candidates with the brief; inspect the actual video/audio. Technical checks do not assess narrative, aesthetics or SFX audibility."
17187
+ };
17188
+ if (r.kind === "editorbrain.media-source-search") return {
17189
+ ...base,
17190
+ complete: r.complete,
17191
+ results: r.results.slice(0, 5).map((item) => ({ provider: item.provider, id: item.id.slice(0, 180), title: item.title.slice(0, 180), sourceUrl: item.sourceUrl.length <= 1500 ? item.sourceUrl : null, author: item.author?.slice(0, 120) ?? null, license: { id: item.license.id, reason: item.license.reason, url: item.license.url && item.license.url.length <= 500 ? item.license.url : null }, contentReviewed: false, rightsVerified: false })),
17192
+ metadataAbbreviated: true,
17193
+ failures: r.failures.slice(0, 5),
17194
+ counts: r.counts,
17195
+ omitted: { results: Math.max(0, r.results.length - 5), failures: Math.max(0, r.failures.length - 5) },
17196
+ searchExhaustive: false,
17197
+ rightsVerified: false,
17198
+ next: "Inspect relevant source pages and media. These archives supplement web image/video search; empty results do not prove absence on the internet."
17199
+ };
17200
+ if (r.kind === "editorbrain.evidence-check") return {
17201
+ ...base,
17202
+ valid: r.valid,
17203
+ inventoryComplete: r.inventoryComplete,
17204
+ observations: r.observations,
17205
+ sourceFilesReverified: false,
17206
+ factualTruth: "not_assessed",
17207
+ reuseRights: "not_assessed",
17208
+ findings: r.findings.slice(0, 8),
17209
+ omitted: { findings: Math.max(0, r.findings.length - 8) }
17210
+ };
17211
+ if (r.kind === "editorbrain.audio-check") return {
17212
+ ...base,
17213
+ complete: r.complete,
17214
+ technicalStatus: r.technicalStatus,
17215
+ file: r.file,
17216
+ fileStable: r.fileStable,
17217
+ coverage: r.coverage,
17218
+ measurements: r.measurements.slice(0, 8),
17219
+ errorCode: r.errorCode,
17220
+ creativeQuality: r.creativeQuality,
17221
+ omitted: { measurements: Math.max(0, r.measurements.length - 8) },
17222
+ next: "Measurements describe this exact file. Listen for intelligibility, pronunciation, SFX and sync when the modality is available."
17223
+ };
17224
+ if (r.kind === "editorbrain.motion-review") return {
17225
+ ...base,
17226
+ samples: r.samples ? { frameCount: r.samples.frameCount, fps: r.samples.fps, radiusFrames: r.samples.radiusFrames, samples: r.samples.samples.slice(0, 12), omittedSamples: Math.max(0, r.samples.samples.length - 12) } : null,
17227
+ geometryReviews: r.geometryReviews.slice(0, 3).map((g) => ({ scope: g.scope, status: g.status, candidates: g.candidates.slice(0, 8), omittedCandidates: Math.max(0, g.candidates.length - 8), checks: g.checks })),
17228
+ omittedGeometryReviews: Math.max(0, r.geometryReviews.length - 3),
17229
+ next: "This plans samples and checks declared boxes only. Render/view those frames; read saved details for omitted candidates and intentional overlaps."
17230
+ };
17231
+ throw new CliError("INVALID_REPORT", "Unknown production report kind.", "Use media inventory/sample or production check/plan-check.");
17232
+ }
17233
+
17234
+ // packages/cli/src/media-preview.ts
17235
+ async function prepareMediaPreview(root, file, out, quality = 720, audioStream) {
17236
+ if (![720, 1080].includes(quality) || !out.endsWith(".mp4")) throw new CliError("INVALID_ARGUMENT", "Use --quality 720 or 1080 and a new .mp4 output.", "Previews belong under .editorbrain-local/.");
17237
+ const source = await probeLocalMedia(await assertLocalMediaFile(file));
17238
+ const videos = source.streams.filter((s) => s.type === "video" && !s.attachedPicture), audio = source.streams.filter((s) => s.type === "audio");
17239
+ if (videos.length !== 1 || !source.durationSeconds || source.durationSeconds > 1800) throw new CliError("PREVIEW_SOURCE", "A preview needs one video stream and a known duration up to 30 minutes.", "Prepare long or multi-angle material explicitly; never truncate it silently.");
17240
+ if (["smpte2084", "arib-std-b67"].includes(videos[0].colorTransfer ?? "")) throw new CliError("PREVIEW_HDR", "HDR needs an explicit color-managed preview.", "Keep the original; export an SDR review copy in your editor first.");
17241
+ if (audio.length > 1 && audioStream === void 0) throw new CliError("AUDIO_TRACK_REQUIRED", "Choose the audio track for this preview.", "Use --audio-stream with its stream index.");
17242
+ const sar = videos[0].sampleAspectRatio;
17243
+ if (sar && !["1:1", "0:1", "N/A"].includes(sar)) throw new CliError("PREVIEW_PIXEL_ASPECT", "Non-square pixels need an explicit display-aspect conversion.", "Export a square-pixel review copy in your editor first.");
17244
+ if (/^(yuva|gbrap|rgba|bgra|argb|abgr|ya)/.test(videos[0].pixelFormat ?? "")) throw new CliError("PREVIEW_ALPHA", "Transparent media needs an explicit background for review.", "Keep the alpha master and export a composited review copy first.");
17245
+ const selected = audioStream === void 0 ? audio[0] : audio.find((s) => s.index === audioStream);
17246
+ if (audioStream !== void 0 && !selected) throw new CliError("INVALID_ARGUMENT", "The selected audio stream does not exist.", "Inspect the material before selecting a track.");
17247
+ const target = await prepareSampleDirectory(root, out), temp = await mkdtemp(join3(dirname4(target), ".preview-"));
17248
+ try {
17249
+ const prepared = join3(temp, "preview.mp4"), edge = quality === 720 ? 1280 : 1920;
17250
+ await runMediaTool("ffmpeg", ["-v", "error", "-n", "-copyts", "-start_at_zero", ...localInputArgs(source.path), "-map", `0:${videos[0].index}`, ...selected ? ["-map", `0:${selected.index}`, "-c:a", "aac", "-b:a", "128k"] : ["-an"], "-sn", "-dn", "-map_metadata", "-1", "-vf", `scale=w='min(iw,${edge})':h='min(ih,${edge})':force_original_aspect_ratio=decrease:force_divisible_by=2,setsar=1`, "-c:v", "libx264", "-preset", "fast", "-crf", "23", "-pix_fmt", "yuv420p", "-movflags", "+faststart", prepared], { timeoutMs: 6e5 });
17251
+ const result2 = await probeLocalMedia(prepared);
17252
+ if (result2.durationSeconds === null || Math.abs(result2.durationSeconds - source.durationSeconds) > 0.25 || result2.streams.some((s) => s.type === "audio") !== !!selected) throw new CliError("PREVIEW_VERIFICATION", "Preview duration or audio does not match the source.", "No output was accepted. Inspect source timing in your editor.");
17253
+ if ((await probeLocalMedia(source.path)).sha256 !== source.sha256) throw new CliError("SOURCE_CHANGED", "The source changed during compression.", "Retry with a stable original.");
17254
+ await chmod(prepared, 384);
17255
+ await link2(prepared, target);
17256
+ return { kind: "editorbrain.review-preview", path: target, sha256: result2.sha256, sourceSha256: source.sha256, sourceBytes: source.bytes, bytes: result2.bytes, durationSeconds: result2.durationSeconds, quality, originalPreserved: true, uploaded: false, uploadEligible: result2.bytes <= 90 * 1024 * 1024, savedPercent: Math.round((1 - result2.bytes / source.bytes) * 100), purpose: "preview", note: "Review copy, not an export master. Inspect text, color and audio before sharing. Small sources may become larger; originals remain untouched." };
17257
+ } finally {
17258
+ await rm(temp, { recursive: true, force: true });
17259
+ }
17260
+ }
16648
17261
 
16649
- // packages/cli/src/errors.ts
16650
- var CliError = class extends Error {
16651
- constructor(code, message, hint, retryable = false, details = {}) {
16652
- super(message);
16653
- this.code = code;
16654
- this.hint = hint;
16655
- this.retryable = retryable;
16656
- this.details = details;
17262
+ // packages/cli/src/studio-audio.ts
17263
+ import { link as link3, mkdtemp as mkdtemp2, readFile, rm as rm2 } from "node:fs/promises";
17264
+ import { dirname as dirname5, join as join4 } from "node:path";
17265
+ import { createHash as createHash2 } from "node:crypto";
17266
+ async function prepareStudioAudio(root, file, out, streamIndex) {
17267
+ const source = await probeLocalMedia(await assertLocalMediaFile(file));
17268
+ const audio = source.streams.filter((stream) => stream.type === "audio");
17269
+ if (!audio.length) throw new CliError("AUDIO_REQUIRED", "The source has no audio stream.", "Use visual inspection instead; do not invent a transcript.");
17270
+ if (audio.length > 1 && streamIndex === void 0) throw new CliError("AUDIO_TRACK_REQUIRED", "The file has several audio tracks.", "Inspect them and select the intended stream with --audio-stream.", false, { audioStreams: audio.map((s) => s.index) });
17271
+ const selected = audio.find((s) => s.index === (streamIndex ?? audio[0].index));
17272
+ if (!selected) throw new CliError("INVALID_ARGUMENT", "The selected stream is not an audio track.", "Inspect media inventory before selecting a track.");
17273
+ if (source.durationSeconds === null || source.durationSeconds > 1800) throw new CliError("AUDIO_DURATION_LIMIT", "Scribe preparation requires a known source duration up to 30 minutes.", "Split long recordings explicitly and preserve each segment offset; never silently truncate.");
17274
+ if (!out.endsWith(".wav")) throw new CliError("INVALID_ARGUMENT", "The prepared output must be a .wav file.", "Use --out .editorbrain-local/studio/source.wav.");
17275
+ const target = await prepareSampleDirectory(root, out), temp = await mkdtemp2(join4(dirname5(target), ".scribe-"));
17276
+ try {
17277
+ const prepared = join4(temp, "audio.wav");
17278
+ await runMediaTool("ffmpeg", ["-v", "error", "-n", "-copyts", "-start_at_zero", ...localInputArgs(source.path), "-map", `0:${selected.index}`, "-vn", "-sn", "-dn", "-map_metadata", "-1", "-af", "aresample=16000:async=1:first_pts=0", "-ac", "1", "-ar", "16000", "-c:a", "pcm_s16le", "-t", "1801", prepared], { timeoutMs: 6e5 });
17279
+ const verified = await probeLocalMedia(prepared);
17280
+ if (verified.durationSeconds === null || verified.durationSeconds > 1800 || verified.bytes > 90 * 1024 * 1024) throw new CliError("AUDIO_DURATION_LIMIT", "The decoded audio exceeds the supported duration.", "Inspect source timestamps; no file was uploaded.");
17281
+ if ((await probeLocalMedia(source.path)).sha256 !== source.sha256) throw new CliError("SOURCE_CHANGED", "The source changed during extraction.", "Retry only after the source is stable.");
17282
+ const bytes = await readFile(prepared), hash6 = createHash2("sha256").update(bytes).digest("hex");
17283
+ await link3(prepared, target);
17284
+ return { kind: "editorbrain.scribe-input", path: target, sha256: hash6, bytes: bytes.length, durationMs: Math.round(verified.durationSeconds * 1e3), mimeType: "audio/wav", sourceSha256: source.sha256, sourcePath: source.path, audioStream: selected.index, sourceStartTimeSeconds: source.startTimeSeconds, timestampBasis: "milliseconds_from_container_start", silenceRemoved: false, uploaded: false, next: "Upload this WAV using assets upload, then quote Scribe with its assetId. Keep this source mapping locally; returned word times refer to this prepared audio." };
17285
+ } finally {
17286
+ await rm2(temp, { recursive: true, force: true });
16657
17287
  }
16658
- code;
16659
- hint;
16660
- retryable;
16661
- details;
16662
- };
16663
- function errorOutput(error) {
16664
- if (error instanceof CliError) return { error: error.message, code: error.code, hint: error.hint, retryable: error.retryable, ...error.details };
16665
- if (error instanceof ValidationError) return { error: "Workspace validation failed.", code: "VALIDATION_FAILED", retryable: false, hint: "Correct the reported documents before retrying.", issues: error.issues };
16666
- const message = error instanceof Error ? error.message : "Unknown error";
16667
- const safe = message.replace(/\b(?:eh_[a-f0-9]{64}|npm_[A-Za-z0-9]+)\b/g, "[REDACTED]").replace(/Bearer\s+\S+/gi, "Bearer [REDACTED]").replace(/(https?:\/\/[^\s?]+)\?[^\s]*/g, "$1?[REDACTED]");
16668
- return { error: safe, code: "COMMAND_FAILED", retryable: false, hint: "Read the command help and run editorbrain doctor. Do not retry writes blindly." };
16669
17288
  }
16670
17289
 
16671
17290
  // packages/cli/src/client.ts
17291
+ import { setTimeout as delay } from "node:timers/promises";
16672
17292
  var searchErrors = {
16673
17293
  invalid_search_brief: { status: 400, code: "INVALID_SEARCH_BRIEF", hint: "Check references/search-brief.md and the service contract. Older services may reject eventQueries objects with channels. Do not silently remove constraints or retry a changed query as if filtering succeeded." },
16674
17294
  source_inspection_required: { status: 422, code: "SOURCE_INSPECTION_REQUIRED", hint: "Inspect the supplied source and declare only modalities actually reviewed. A script requires script inspection; video requires frames, motion or audio." },
@@ -16862,11 +17482,11 @@ async function pushWorkspace(client, root, files, options = {}) {
16862
17482
  for (const [path, content] of Object.entries(files)) {
16863
17483
  const validation = validateDocument(path, content);
16864
17484
  if (!validation.valid) throw new Error(validation.issues.map((x) => `${x.path}: ${x.message}`).join("\n"));
16865
- const digest2 = await sha256(content);
17485
+ const digest3 = await sha256(content);
16866
17486
  const existing = remote.get(path);
16867
17487
  const base = state.documents[path];
16868
- if (existing?.sha256 === digest2) {
16869
- state.documents[path] = { revision: existing.revision, sha256: digest2 };
17488
+ if (existing?.sha256 === digest3) {
17489
+ state.documents[path] = { revision: existing.revision, sha256: digest3 };
16870
17490
  unchanged2++;
16871
17491
  continue;
16872
17492
  }
@@ -16887,8 +17507,8 @@ ${conflicts.join("\n")}`, "Run push --dry-run and reconcile the reported paths."
16887
17507
  for (const item of pending) {
16888
17508
  const result2 = await client.put(item.path, item.content, item.expectedRevision);
16889
17509
  checkRemote(result2);
16890
- const digest2 = await sha256(item.content);
16891
- if (result2.path !== item.path || result2.sha256 !== digest2) throw new Error("Server returned inconsistent write metadata");
17510
+ const digest3 = await sha256(item.content);
17511
+ if (result2.path !== item.path || result2.sha256 !== digest3) throw new Error("Server returned inconsistent write metadata");
16892
17512
  state.documents[item.path] = { revision: result2.revision, sha256: result2.sha256 };
16893
17513
  uploaded.push(item.path);
16894
17514
  await saveSyncState(root, state);
@@ -16906,14 +17526,14 @@ async function pullWorkspace(client, root, local, options = {}) {
16906
17526
  for (const summary2 of summaries) {
16907
17527
  checkRemote(summary2);
16908
17528
  const content = local[summary2.path];
16909
- const digest2 = content === void 0 ? void 0 : await sha256(content);
17529
+ const digest3 = content === void 0 ? void 0 : await sha256(content);
16910
17530
  const base = state.documents[summary2.path];
16911
- if (digest2 === summary2.sha256) {
17531
+ if (digest3 === summary2.sha256) {
16912
17532
  state.documents[summary2.path] = { revision: summary2.revision, sha256: summary2.sha256 };
16913
17533
  unchanged2++;
16914
17534
  continue;
16915
17535
  }
16916
- if (content !== void 0 && (!base || digest2 !== base.sha256)) {
17536
+ if (content !== void 0 && (!base || digest3 !== base.sha256)) {
16917
17537
  conflicts.push(summary2.path);
16918
17538
  continue;
16919
17539
  }
@@ -16949,6 +17569,39 @@ ${conflicts.join("\n")}`, "Run pull --dry-run and reconcile the reported paths."
16949
17569
  return { downloaded, unchanged: unchanged2 };
16950
17570
  }
16951
17571
 
17572
+ // packages/cli/src/studio.ts
17573
+ async function studioCommand(client, root, command, argument, flags) {
17574
+ const allowed = /* @__PURE__ */ new Set(["dir", "url", ...command === "quote" ? ["file"] : command === "run" ? ["max-credits", "user-confirmed"] : []]);
17575
+ for (const flag of Object.keys(flags)) if (!allowed.has(flag)) throw new CliError("INVALID_ARGUMENT", `--${flag} is not supported for studio ${command}.`, "Read editorbrain help.");
17576
+ try {
17577
+ if (command === "catalog" || command === "jobs") {
17578
+ if (argument) throw new CliError("INVALID_ARGUMENT", "This command has no positional input.", "Read editorbrain help.");
17579
+ return await client.request(`/v1/studio/${command}`);
17580
+ }
17581
+ if (command === "quote") {
17582
+ if (argument || typeof flags.file !== "string") throw new CliError("INVALID_ARGUMENT", "studio quote requires --file request.json.", "Read references/studio.md.");
17583
+ const body = await readProductionJson(root, flags.file);
17584
+ if (Buffer.byteLength(JSON.stringify(body)) > 16e3) throw new CliError("INVALID_ARGUMENT", "Studio input exceeds 16 KB.", "Use a short scene description or script.");
17585
+ return await client.request("/v1/studio/quotes", { method: "POST", body });
17586
+ }
17587
+ if (!argument || !/^[a-f0-9-]{36}$/i.test(argument)) throw new CliError("INVALID_ARGUMENT", "Supply the quote/job ID returned by EditorBrain.", "Read editorbrain studio jobs before repeating a run.");
17588
+ if (command === "get") return await client.request(`/v1/studio/jobs/${argument}`);
17589
+ if (command === "run") {
17590
+ const maxCredits = Number(flags["max-credits"]);
17591
+ if (flags["user-confirmed"] !== true || !Number.isFinite(maxCredits) || maxCredits <= 0 || maxCredits > 1e4) throw new CliError("SPENDING_APPROVAL_REQUIRED", "Supply --user-confirmed and --max-credits only after the user authorized this quote or budget.", "Preferences and direct editing mode do not authorize spending.");
17592
+ return await client.request("/v1/studio/jobs", { method: "POST", body: { quoteId: argument, maxCredits, userConfirmed: true } });
17593
+ }
17594
+ throw new CliError("INVALID_ARGUMENT", "Use studio catalog, quote, run, get or jobs.", "Read editorbrain help.");
17595
+ } catch (error) {
17596
+ if (error instanceof ApiError) {
17597
+ const code = error.body?.error?.code;
17598
+ const hints = { studio_unavailable: "This EditorBrain tool is not activated. No generation was started.", studio_insufficient_credits: "Check studio catalog for your available creative-credit balance.", studio_quote_expired: "Request a new quote and review its price.", studio_budget_exceeded: "The quote exceeds the authorized budget. Do not increase it yourself.", stale_context: "Read the current project preferences and request a new quote.", studio_preference_conflict: "Reconcile the project language, voice or no-music preference with the user before generating.", studio_audio_format: "Prepare PCM 16-bit WAV (mono/stereo, up to 30 minutes) from the original with local FFmpeg, then upload it." };
17599
+ if (code && hints[code]) throw new CliError(code.toUpperCase(), "EditorBrain Studio could not start this request.", hints[code]);
17600
+ }
17601
+ throw error;
17602
+ }
17603
+ }
17604
+
16952
17605
  // packages/cli/src/project-selection.ts
16953
17606
  async function listProjects(client) {
16954
17607
  const documents = await client.list("videos/");
@@ -16991,13 +17644,13 @@ async function pullProject(client, root, local, options = {}) {
16991
17644
  }
16992
17645
 
16993
17646
  // packages/cli/src/setup.ts
16994
- import { spawn } from "node:child_process";
16995
- import { resolve as resolve3 } from "node:path";
17647
+ import { spawn as spawn2 } from "node:child_process";
17648
+ import { resolve as resolve4 } from "node:path";
16996
17649
 
16997
17650
  // packages/cli/src/agent-guidance.ts
16998
- import { createHash } from "node:crypto";
16999
- import { constants as constants2 } from "node:fs";
17000
- import { lstat as lstat2, open as open2, readdir as readdir2, rename as rename2 } from "node:fs/promises";
17651
+ import { createHash as createHash3 } from "node:crypto";
17652
+ import { constants as constants4 } from "node:fs";
17653
+ import { lstat as lstat4, open as open4, readdir as readdir3, rename as rename2 } from "node:fs/promises";
17001
17654
  import { fileURLToPath } from "node:url";
17002
17655
 
17003
17656
  // packages/cli/src/agent-guidance-history.ts
@@ -17131,6 +17784,33 @@ var VERIFIED_GUIDANCE_RELEASES = [
17131
17784
  "editorbrain/references/style-creator.md": "1b1bc289a5bc308cbbf7f4d699de2933b8934733a2b69b3e98fb1b7420d986f2",
17132
17785
  "editorbrain/references/workspace-files.md": "cd3006a7cc4f04ff5dfeb1bd901a21c53455017aa7d19fcde9a6e26f1fbbd5b6"
17133
17786
  }
17787
+ },
17788
+ {
17789
+ "version": "0.28.2",
17790
+ "archiveSha256": "1d73319f34603a7a1df834f8ed0884c49ec9c2b16d255fea3b30388f4d91f416",
17791
+ "files": {
17792
+ "editorbrain-motion/SKILL.md": "cd0e0c25c1025a775ab547bab16d6efb43a4f388b8f3ed928cdfed3999938ac7",
17793
+ "editorbrain-motion/references/authored-scenes.md": "3d617226b0aafe0970c635e77d285b752913fca33417d7c04e51d6b6c60af973",
17794
+ "editorbrain-motion/references/code-motion.md": "202240b2ae7c062c3cdd4b488f1032f991b4509af78206ad846faa6a792e1d0a",
17795
+ "editorbrain-motion/references/component-library.md": "fea1fb710d23830c3ed14c3e4bed7bc53540ae2e2e662b74fe6eb6307b010d28",
17796
+ "editorbrain-motion/references/generated-motion.md": "a8d214b53c15f742658abd84f03b768fa2821f8cde94f50aab82a3759a30b36b",
17797
+ "editorbrain/AGENTS.md": "cda14533a7232f1324b8137dfb24698ecbbf60e3c32c3b2216495a7ab7a3a777",
17798
+ "editorbrain/SKILL.md": "58c490d4f3acb9e83a958b7d93eb91815387b0eb6480bfa523ca4d91b3442a03",
17799
+ "editorbrain/references/audio-production.md": "9e10ea3583e859677eb241c24ddb3ebc77b5c7077654346201ede688d2fcc149",
17800
+ "editorbrain/references/cli-mcp.md": "aa17a0d5c366e9c9318aaea978694575be4f4ba0051e1c5e0684886b1bcaccec",
17801
+ "editorbrain/references/first-edit.md": "ad5682046717d2ccb10a310c237c03ead8de294f1b907e550bd50b479bee0ed6",
17802
+ "editorbrain/references/integrations.md": "a3500dd79c71ab3ee80a865bb5bf8c94287efb933e07d577738891e276a819a0",
17803
+ "editorbrain/references/local-production.md": "e981c0a02f8e0d1998e39d64de393b264703d24bacc904600f12885ff3576588",
17804
+ "editorbrain/references/media-preferences.md": "c7904e4a6475b5ff935d7b942bd2c715de6fda9bead93e90d3c4ee99532d5f54",
17805
+ "editorbrain/references/memory-and-review.md": "51a274e522270f1fc295d0c0b9a1c23672ce01abea097c2499c9a8db56c0c289",
17806
+ "editorbrain/references/motion-review.md": "0fb09a2d61ecd13114ced5cca5daa4580a922cf7d7b3f8698077ceb0e96da6f0",
17807
+ "editorbrain/references/onboarding.md": "96bd762314a1f3dbbeb5172dba85a7b1f2fbc26627a09f7b48fa2889a9d09ab2",
17808
+ "editorbrain/references/production-and-review.md": "ed662a151dedbe168fcacc59be1d997a012818cd7abd9b9a8e936138e2e0b1f2",
17809
+ "editorbrain/references/real-media.md": "e80ba70caca8a1253fbf13b56ee2a85674ccf9f09e71d8da6343f1aa548dc801",
17810
+ "editorbrain/references/search-brief.md": "f2a4b016bacf35005c7999ee3b51190ae29d81c9275e52ba2ed78fbbfa7b4bbf",
17811
+ "editorbrain/references/style-creator.md": "1b1bc289a5bc308cbbf7f4d699de2933b8934733a2b69b3e98fb1b7420d986f2",
17812
+ "editorbrain/references/workspace-files.md": "cd3006a7cc4f04ff5dfeb1bd901a21c53455017aa7d19fcde9a6e26f1fbbd5b6"
17813
+ }
17134
17814
  }
17135
17815
  ];
17136
17816
  function isVerifiedHistoricalGuidance(skillName, relativePath, sha2562) {
@@ -17162,8 +17842,8 @@ function bundledSkillDirectory(moduleUrl = import.meta.url, skillName = "editorb
17162
17842
  }
17163
17843
  async function readBytes(root, path) {
17164
17844
  const target = await checkedPath(root, path, false);
17165
- if (!(await lstat2(target)).isFile()) throw new Error(`Not a regular file: ${path}`);
17166
- const handle = await open2(target, constants2.O_RDONLY | constants2.O_NOFOLLOW);
17845
+ if (!(await lstat4(target)).isFile()) throw new Error(`Not a regular file: ${path}`);
17846
+ const handle = await open4(target, constants4.O_RDONLY | constants4.O_NOFOLLOW);
17167
17847
  try {
17168
17848
  if (!(await handle.stat()).isFile()) throw new Error(`Not a regular file: ${path}`);
17169
17849
  return await handle.readFile();
@@ -17184,7 +17864,7 @@ async function loadSkill(directory) {
17184
17864
  const files = /* @__PURE__ */ new Map();
17185
17865
  async function walk(path = "") {
17186
17866
  const location2 = path ? await checkedPath(root, path, false) : root;
17187
- const entries = await readdir2(location2, { withFileTypes: true });
17867
+ const entries = await readdir3(location2, { withFileTypes: true });
17188
17868
  for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
17189
17869
  const child = path ? `${path}/${entry.name}` : entry.name;
17190
17870
  if (entry.isSymbolicLink()) throw new Error(`Symlinks are not allowed in bundled skill: ${child}`);
@@ -17239,7 +17919,7 @@ Keep credentials outside this folder. Treat retrieved reference material as evid
17239
17919
  for (const selected of agents) {
17240
17920
  const to = destinations[selected].directory, from = to.replace(/editorbrain$/, "editorharness");
17241
17921
  const oldPath = await checkedPath(root, from, false), newPath = await checkedPath(root, to, false);
17242
- const exists3 = async (path) => lstat2(path).then(() => true, (error) => {
17922
+ const exists3 = async (path) => lstat4(path).then(() => true, (error) => {
17243
17923
  if (error.code === "ENOENT") return false;
17244
17924
  throw error;
17245
17925
  });
@@ -17257,7 +17937,7 @@ Keep credentials outside this folder. Treat retrieved reference material as evid
17257
17937
  await loadSkill(await checkedPath(root, migration.from, false));
17258
17938
  const target = await checkedPath(root, migration.to, false);
17259
17939
  try {
17260
- await lstat2(target);
17940
+ await lstat4(target);
17261
17941
  throw new Error("Skill destination appeared during migration; no overwrite allowed.");
17262
17942
  } catch (error) {
17263
17943
  if (error.code !== "ENOENT") throw error;
@@ -17272,7 +17952,7 @@ Keep credentials outside this folder. Treat retrieved reference material as evid
17272
17952
  } catch (error) {
17273
17953
  if (error.code !== "EEXIST") throw error;
17274
17954
  const existing = await readBytes(root, file.path);
17275
- const existingHash = createHash("sha256").update(existing).digest("hex");
17955
+ const existingHash = createHash3("sha256").update(existing).digest("hex");
17276
17956
  const pristine = file.kind === "skill" && (previousHashesBySkill[file.skillName][file.relativePath]?.includes(existingHash) || isVerifiedHistoricalGuidance(file.skillName, file.relativePath, existingHash));
17277
17957
  const oldPointer = Buffer.from(file.content.toString().replaceAll("/skills/editorbrain/", "/skills/editorharness/"));
17278
17958
  if (!existing.equals(file.content) && (pristine || file.kind === "entryPoint" && existing.equals(oldPointer))) {
@@ -17303,8 +17983,8 @@ async function installAgentGuidance(directory, agent = "both") {
17303
17983
  }
17304
17984
 
17305
17985
  // packages/cli/src/workspace-lock.ts
17306
- import { constants as constants3 } from "node:fs";
17307
- import { lstat as lstat3, open as open3, rename as rename3, unlink as unlink2 } from "node:fs/promises";
17986
+ import { constants as constants5 } from "node:fs";
17987
+ import { lstat as lstat5, open as open5, rename as rename3, unlink as unlink2 } from "node:fs/promises";
17308
17988
  import { hostname } from "node:os";
17309
17989
  import { randomUUID as randomUUID2 } from "node:crypto";
17310
17990
  var LOCK_PATH = ".editorbrain-operation.lock";
@@ -17312,7 +17992,7 @@ async function withWorkspaceLock(root, operation, options = {}) {
17312
17992
  const legacyLock = await checkedPath(root, ".editorharness-operation.lock", false);
17313
17993
  let legacy;
17314
17994
  try {
17315
- legacy = await open3(legacyLock, constants3.O_CREAT | constants3.O_EXCL | constants3.O_WRONLY | constants3.O_NOFOLLOW, 384);
17995
+ legacy = await open5(legacyLock, constants5.O_CREAT | constants5.O_EXCL | constants5.O_WRONLY | constants5.O_NOFOLLOW, 384);
17316
17996
  } catch (error) {
17317
17997
  if (error.code === "EEXIST") throw new CliError("WORKSPACE_BUSY", "Another CLI operation holds this workspace lock.", "Wait for the other operation to finish.");
17318
17998
  throw error;
@@ -17321,7 +18001,7 @@ async function withWorkspaceLock(root, operation, options = {}) {
17321
18001
  const path = await checkedPath(root, LOCK_PATH, false);
17322
18002
  let handle;
17323
18003
  try {
17324
- handle = await open3(path, constants3.O_CREAT | constants3.O_EXCL | constants3.O_WRONLY | constants3.O_NOFOLLOW, 384);
18004
+ handle = await open5(path, constants5.O_CREAT | constants5.O_EXCL | constants5.O_WRONLY | constants5.O_NOFOLLOW, 384);
17325
18005
  } catch (error) {
17326
18006
  if (error.code !== "EEXIST") throw error;
17327
18007
  throw new CliError("WORKSPACE_BUSY", "Another CLI operation holds this workspace lock.", "Wait for the other operation. If it crashed, verify no process is using the folder before removing .editorbrain-operation.lock. Locks are never removed automatically.");
@@ -17332,7 +18012,7 @@ async function withWorkspaceLock(root, operation, options = {}) {
17332
18012
  await handle.close();
17333
18013
  const oldRoot = await checkedPath(root, ".editorharness", false);
17334
18014
  const newRoot = await checkedPath(root, ".editorbrain", false);
17335
- const exists3 = async (path2) => lstat3(path2).then(() => true, (error) => {
18015
+ const exists3 = async (path2) => lstat5(path2).then(() => true, (error) => {
17336
18016
  if (error.code === "ENOENT") return false;
17337
18017
  throw error;
17338
18018
  });
@@ -17353,10 +18033,10 @@ async function withWorkspaceLock(root, operation, options = {}) {
17353
18033
  }
17354
18034
 
17355
18035
  // packages/cli/src/doctor.ts
17356
- import { readFile } from "node:fs/promises";
17357
- import { resolve as resolve2 } from "node:path";
18036
+ import { readFile as readFile2 } from "node:fs/promises";
18037
+ import { resolve as resolve3 } from "node:path";
17358
18038
  async function cliVersion() {
17359
- const metadata = JSON.parse(await readFile(new URL("../package.json", import.meta.url), "utf8"));
18039
+ const metadata = JSON.parse(await readFile2(new URL("../package.json", import.meta.url), "utf8"));
17360
18040
  return metadata.version;
17361
18041
  }
17362
18042
  async function diagnose(options, fetcher = fetch) {
@@ -17390,7 +18070,7 @@ async function diagnose(options, fetcher = fetch) {
17390
18070
  try {
17391
18071
  for (const file of await bundledGuidanceFiles(skillName)) {
17392
18072
  const current2 = await readText(root, `${directory}/${file}`, false);
17393
- const bundled = await readFile(resolve2(bundledSkillDirectory(void 0, skillName), file), "utf8");
18073
+ const bundled = await readFile2(resolve3(bundledSkillDirectory(void 0, skillName), file), "utf8");
17394
18074
  if (current2 !== bundled) different = true;
17395
18075
  }
17396
18076
  checks.push({ name, status: different ? "warning" : "pass", detail: different ? "Installed guidance differs from this CLI release." : "All bundled guidance files match.", hint: different ? "Review customizations or older guidance; skill install preserves differences." : void 0 });
@@ -17421,12 +18101,12 @@ async function diagnose(options, fetcher = fetch) {
17421
18101
  failure3("remote", error);
17422
18102
  }
17423
18103
  }
17424
- return { schemaVersion: 1, cliVersion: await cliVersion(), ok: !checks.some((check) => check.status === "fail"), directory: root ?? resolve2(options.directory), workspaceId, checks, mcpClient: "not_verified", renderVerified: false };
18104
+ return { schemaVersion: 1, cliVersion: await cliVersion(), ok: !checks.some((check) => check.status === "fail"), directory: root ?? resolve3(options.directory), workspaceId, checks, mcpClient: "not_verified", renderVerified: false };
17425
18105
  }
17426
18106
 
17427
18107
  // packages/cli/src/setup.ts
17428
18108
  var runHost = (command, args, cwd, interactive = false) => new Promise((done, reject) => {
17429
- const child = spawn(command, args, { cwd, shell: false, stdio: ["ignore", interactive ? 2 : "pipe", interactive ? 2 : "pipe"], windowsHide: !interactive });
18109
+ const child = spawn2(command, args, { cwd, shell: false, stdio: ["ignore", interactive ? 2 : "pipe", interactive ? 2 : "pipe"], windowsHide: !interactive });
17430
18110
  let stdout = "", stderr = "", size = 0;
17431
18111
  const collect = (kind, data) => {
17432
18112
  size += data.length;
@@ -17474,183 +18154,71 @@ async function configureMcp(agent, directory, url2, noBrowser, runner = runHost)
17474
18154
  exists3 = list.some((item) => item?.name === "editorbrain");
17475
18155
  } else {
17476
18156
  exists3 = inspect.code === 0;
17477
- if (!exists3 && !/^No MCP server named "editorbrain"[.\s]/.test(inspect.stderr + inspect.stdout)) throw new CliError("MCP_INSPECTION_FAILED", "Could not inspect Claude MCP configuration.", "Inspect it manually; setup did not overwrite it.");
17478
- }
17479
- if (exists3) return { agent, status: "preserved", next: "An existing editorbrain server was preserved. Check its endpoint and authentication, then " + next, hostVerified: false };
17480
- const added = await runner(agent, args, directory, true);
17481
- return { agent, status: added.code === 0 ? "registered" : "action_required", next, hostVerified: false };
17482
- }
17483
- async function setup(options, runner = runHost) {
17484
- const steps = {};
17485
- let selected = await options.getConfig();
17486
- let client = new Client(selected);
17487
- let me;
17488
- try {
17489
- if (!selected.apiKey) throw new CliError("AUTH_REQUIRED", "Login required.", "Run login.");
17490
- me = await client.me();
17491
- } catch (error) {
17492
- if (!(error instanceof CliError) || error.code !== "AUTH_REQUIRED") throw error;
17493
- await options.login();
17494
- selected = await options.getConfig();
17495
- client = new Client(selected);
17496
- me = await client.me();
17497
- }
17498
- if (options.workspaceId && me.workspaceId !== options.workspaceId) throw new CliError("WORKSPACE_MISMATCH", "The authorized account selected a different workspace.", "Run login for the intended workspace. No project files were changed.");
17499
- steps.account = { status: "verified", workspaceId: me.workspaceId, endpoint: client.url };
17500
- const root = await ensureRoot(resolve3(options.directory), true);
17501
- let folderReady = false, skillReady = false;
17502
- await withWorkspaceLock(root, async () => {
17503
- try {
17504
- steps.documents = { status: "ready", ...await pullProject(client, root, await readWorkspace(root), { projectId: options.projectId }) };
17505
- folderReady = true;
17506
- } catch (error) {
17507
- steps.documents = { status: error instanceof CliError && error.code === "PROJECT_SELECTION_REQUIRED" ? "selection_required" : "failed", ...errorOutput(error) };
17508
- }
17509
- try {
17510
- const result2 = await installAgentGuidance(root, options.agent);
17511
- steps.skill = { status: result2.conflicts.length ? "review_required" : "ready", ...result2 };
17512
- skillReady = !result2.conflicts.length;
17513
- } catch (error) {
17514
- steps.skill = { status: "failed", ...errorOutput(error) };
17515
- }
17516
- });
17517
- const hosts = [];
17518
- if (!options.skipMcp && skillReady) {
17519
- for (const agent of options.agent === "both" ? ["codex", "claude"] : [options.agent]) {
17520
- try {
17521
- hosts.push(await configureMcp(agent, root, client.url, !!options.noBrowser, runner));
17522
- } catch (error) {
17523
- hosts.push({ agent, status: "action_required", ...errorOutput(error), hostVerified: false });
17524
- }
17525
- }
17526
- }
17527
- steps.mcp = { status: options.skipMcp ? "skipped" : "verification_required", hosts, expectedWorkspaceId: me.workspaceId, next: "Inside your agent, call workspace_info and confirm this workspace ID. Configuration alone does not verify a connection." };
17528
- const diagnosis = await diagnose({ directory: root, agent: options.agent, config: selected });
17529
- steps.diagnostics = diagnosis;
17530
- return { schemaVersion: 1, directory: root, workspaceId: me.workspaceId, cliReady: folderReady && skillReady && diagnosis.ok, status: folderReady && skillReady && diagnosis.ok && options.skipMcp ? "ready" : "action_required", steps, renderVerified: false };
17531
- }
17532
-
17533
- // packages/cli/src/onboarding.ts
17534
- import { createServer } from "node:http";
17535
- import { randomBytes, createHash as createHash2 } from "node:crypto";
17536
- import { join as join2, dirname as dirname3 } from "node:path";
17537
- import { mkdir as mkdir3 } from "node:fs/promises";
17538
-
17539
- // packages/cli/src/production-output.ts
17540
- import { constants as constants4 } from "node:fs";
17541
- import { mkdir as mkdir2, open as open4 } from "node:fs/promises";
17542
- import { dirname as dirname2 } from "node:path";
17543
- async function productionOutputPath(root, relativePath) {
17544
- if (!relativePath.startsWith(".editorbrain-local/") || relativePath.split(/[\\/]/).some((p) => !p || p === ".." || p === ".") || /[\\\x00-\x1f\x7f]/.test(relativePath)) {
17545
- throw new CliError("LOCAL_OUTPUT_REQUIRED", "Production output must be inside .editorbrain-local/.", "Use --out .editorbrain-local/<project>/inventory.json, or omit --out for JSON stdout. These reports are not synced.");
17546
- }
17547
- return checkedPath(root, relativePath, false);
17548
- }
17549
- async function prepareSampleDirectory(root, path) {
17550
- const target = await productionOutputPath(root, path);
17551
- await mkdir2(dirname2(target), { recursive: true });
17552
- await productionOutputPath(root, path);
17553
- return target;
17554
- }
17555
- async function readProductionJson(root, path) {
17556
- const target = await checkedPath(root, path, false);
17557
- const handle = await open4(target, constants4.O_RDONLY | constants4.O_NOFOLLOW | constants4.O_NONBLOCK);
17558
- try {
17559
- const stat = await handle.stat();
17560
- if (!stat.isFile() || stat.size > 16 * 1024 * 1024) throw new CliError("INVALID_REPORT", "Expected a regular JSON file of at most 16 MiB.", "Select a local inventory, plan or check profile.");
17561
- return JSON.parse(await handle.readFile("utf8"));
17562
- } finally {
17563
- await handle.close();
17564
- }
17565
- }
17566
- async function saveProductionReport(root, path, report) {
17567
- const target = await productionOutputPath(root, path);
17568
- if (!path.endsWith(".json")) throw new CliError("INVALID_ARGUMENT", "Report output needs a .json extension.", "Use a new path under .editorbrain-local/.");
17569
- await atomicWrite(root, path, stableJson(report), { document: false, overwrite: false, mode: 384 });
17570
- return target;
17571
- }
17572
- function summarizeProductionReport(value) {
17573
- const r = value;
17574
- const base = { schemaVersion: r.schemaVersion, kind: r.kind, summary: true, mediaUploaded: false, quality: "not_assessed" };
17575
- if (r.kind === "editorbrain.media-inventory") return {
17576
- ...base,
17577
- complete: r.complete,
17578
- contentReviewed: false,
17579
- counts: { assets: r.assets.length, failures: r.failures.length, skipped: r.skipped.length },
17580
- assets: r.assets.slice(0, 8),
17581
- failures: r.failures.slice(0, 5),
17582
- omitted: { assets: Math.max(0, r.assets.length - 8), failures: Math.max(0, r.failures.length - 5), skipped: r.skipped.length },
17583
- next: "Inspect selected source content. Use media sample for specific frames. Read the saved report or rerun with --details for omitted metadata; no content was reviewed automatically."
17584
- };
17585
- if (r.kind === "editorbrain.media-samples") return { ...base, sourceSha256: r.source.sha256, frames: r.frames, contentReviewed: false, next: "View these frames to inspect composition. They do not prove motion or sound." };
17586
- if (r.kind === "editorbrain.production-plan-check") return {
17587
- ...base,
17588
- valid: r.valid,
17589
- inventoryComplete: r.inventoryComplete,
17590
- observations: r.observations,
17591
- sourceFilesReverified: false,
17592
- counts: { sources: r.coverage.length, findings: r.findings.length, repeatedPairs: r.repeatedPairs ?? 0 },
17593
- findings: [...r.findings].sort((a, b) => ({ error: 0, warning: 1, info: 2 })[a.severity] - { error: 0, warning: 1, info: 2 }[b.severity]).slice(0, 8),
17594
- omittedFindings: Math.max(0, r.findings.length - 8),
17595
- next: "Review source reuse and inspection gaps in the saved report. Observations are agent-declared, not independently certified."
17596
- };
17597
- if (r.kind === "editorbrain.render-check") return {
17598
- ...base,
17599
- complete: r.complete,
17600
- technicalStatus: r.technicalStatus,
17601
- file: r.file,
17602
- checks: r.checks,
17603
- findings: r.findings.slice(0, 8),
17604
- findingsSummary: r.findingsSummary,
17605
- omittedFindings: Math.max(0, r.findings.length - 8),
17606
- next: "Reconcile technical candidates with the brief; inspect the actual video/audio. Technical checks do not assess narrative, aesthetics or SFX audibility."
17607
- };
17608
- if (r.kind === "editorbrain.media-source-search") return {
17609
- ...base,
17610
- complete: r.complete,
17611
- results: r.results.slice(0, 5).map((item) => ({ provider: item.provider, id: item.id.slice(0, 180), title: item.title.slice(0, 180), sourceUrl: item.sourceUrl.length <= 1500 ? item.sourceUrl : null, author: item.author?.slice(0, 120) ?? null, license: { id: item.license.id, reason: item.license.reason, url: item.license.url && item.license.url.length <= 500 ? item.license.url : null }, contentReviewed: false, rightsVerified: false })),
17612
- metadataAbbreviated: true,
17613
- failures: r.failures.slice(0, 5),
17614
- counts: r.counts,
17615
- omitted: { results: Math.max(0, r.results.length - 5), failures: Math.max(0, r.failures.length - 5) },
17616
- searchExhaustive: false,
17617
- rightsVerified: false,
17618
- next: "Inspect relevant source pages and media. These archives supplement web image/video search; empty results do not prove absence on the internet."
17619
- };
17620
- if (r.kind === "editorbrain.evidence-check") return {
17621
- ...base,
17622
- valid: r.valid,
17623
- inventoryComplete: r.inventoryComplete,
17624
- observations: r.observations,
17625
- sourceFilesReverified: false,
17626
- factualTruth: "not_assessed",
17627
- reuseRights: "not_assessed",
17628
- findings: r.findings.slice(0, 8),
17629
- omitted: { findings: Math.max(0, r.findings.length - 8) }
17630
- };
17631
- if (r.kind === "editorbrain.audio-check") return {
17632
- ...base,
17633
- complete: r.complete,
17634
- technicalStatus: r.technicalStatus,
17635
- file: r.file,
17636
- fileStable: r.fileStable,
17637
- coverage: r.coverage,
17638
- measurements: r.measurements.slice(0, 8),
17639
- errorCode: r.errorCode,
17640
- creativeQuality: r.creativeQuality,
17641
- omitted: { measurements: Math.max(0, r.measurements.length - 8) },
17642
- next: "Measurements describe this exact file. Listen for intelligibility, pronunciation, SFX and sync when the modality is available."
17643
- };
17644
- if (r.kind === "editorbrain.motion-review") return {
17645
- ...base,
17646
- samples: r.samples ? { frameCount: r.samples.frameCount, fps: r.samples.fps, radiusFrames: r.samples.radiusFrames, samples: r.samples.samples.slice(0, 12), omittedSamples: Math.max(0, r.samples.samples.length - 12) } : null,
17647
- geometryReviews: r.geometryReviews.slice(0, 3).map((g) => ({ scope: g.scope, status: g.status, candidates: g.candidates.slice(0, 8), omittedCandidates: Math.max(0, g.candidates.length - 8), checks: g.checks })),
17648
- omittedGeometryReviews: Math.max(0, r.geometryReviews.length - 3),
17649
- next: "This plans samples and checks declared boxes only. Render/view those frames; read saved details for omitted candidates and intentional overlaps."
17650
- };
17651
- throw new CliError("INVALID_REPORT", "Unknown production report kind.", "Use media inventory/sample or production check/plan-check.");
18157
+ if (!exists3 && !/^No MCP server named "editorbrain"[.\s]/.test(inspect.stderr + inspect.stdout)) throw new CliError("MCP_INSPECTION_FAILED", "Could not inspect Claude MCP configuration.", "Inspect it manually; setup did not overwrite it.");
18158
+ }
18159
+ if (exists3) return { agent, status: "preserved", next: "An existing editorbrain server was preserved. Check its endpoint and authentication, then " + next, hostVerified: false };
18160
+ const added = await runner(agent, args, directory, true);
18161
+ return { agent, status: added.code === 0 ? "registered" : "action_required", next, hostVerified: false };
18162
+ }
18163
+ async function setup(options, runner = runHost) {
18164
+ const steps = {};
18165
+ let selected = await options.getConfig();
18166
+ let client = new Client(selected);
18167
+ let me;
18168
+ try {
18169
+ if (!selected.apiKey) throw new CliError("AUTH_REQUIRED", "Login required.", "Run login.");
18170
+ me = await client.me();
18171
+ } catch (error) {
18172
+ if (!(error instanceof CliError) || error.code !== "AUTH_REQUIRED") throw error;
18173
+ await options.login();
18174
+ selected = await options.getConfig();
18175
+ client = new Client(selected);
18176
+ me = await client.me();
18177
+ }
18178
+ if (options.workspaceId && me.workspaceId !== options.workspaceId) throw new CliError("WORKSPACE_MISMATCH", "The authorized account selected a different workspace.", "Run login for the intended workspace. No project files were changed.");
18179
+ steps.account = { status: "verified", workspaceId: me.workspaceId, endpoint: client.url };
18180
+ const root = await ensureRoot(resolve4(options.directory), true);
18181
+ let folderReady = false, skillReady = false;
18182
+ await withWorkspaceLock(root, async () => {
18183
+ try {
18184
+ steps.documents = { status: "ready", ...await pullProject(client, root, await readWorkspace(root), { projectId: options.projectId }) };
18185
+ folderReady = true;
18186
+ } catch (error) {
18187
+ steps.documents = { status: error instanceof CliError && error.code === "PROJECT_SELECTION_REQUIRED" ? "selection_required" : "failed", ...errorOutput(error) };
18188
+ }
18189
+ try {
18190
+ const result2 = await installAgentGuidance(root, options.agent);
18191
+ steps.skill = { status: result2.conflicts.length ? "review_required" : "ready", ...result2 };
18192
+ skillReady = !result2.conflicts.length;
18193
+ } catch (error) {
18194
+ steps.skill = { status: "failed", ...errorOutput(error) };
18195
+ }
18196
+ });
18197
+ const hosts = [];
18198
+ if (!options.skipMcp && skillReady) {
18199
+ for (const agent of options.agent === "both" ? ["codex", "claude"] : [options.agent]) {
18200
+ try {
18201
+ hosts.push(await configureMcp(agent, root, client.url, !!options.noBrowser, runner));
18202
+ } catch (error) {
18203
+ hosts.push({ agent, status: "action_required", ...errorOutput(error), hostVerified: false });
18204
+ }
18205
+ }
18206
+ }
18207
+ steps.mcp = { status: options.skipMcp ? "skipped" : "verification_required", hosts, expectedWorkspaceId: me.workspaceId, next: "Inside your agent, call workspace_info and confirm this workspace ID. Configuration alone does not verify a connection." };
18208
+ const diagnosis = await diagnose({ directory: root, agent: options.agent, config: selected });
18209
+ steps.diagnostics = diagnosis;
18210
+ return { schemaVersion: 1, directory: root, workspaceId: me.workspaceId, cliReady: folderReady && skillReady && diagnosis.ok, status: folderReady && skillReady && diagnosis.ok && options.skipMcp ? "ready" : "action_required", steps, renderVerified: false };
17652
18211
  }
17653
18212
 
18213
+ // packages/cli/src/onboarding.ts
18214
+ import { createServer } from "node:http";
18215
+ import { randomBytes, createHash as createHash4 } from "node:crypto";
18216
+ import { join as join5, dirname as dirname6 } from "node:path";
18217
+ import { mkdir as mkdir3 } from "node:fs/promises";
18218
+
18219
+ // packages/cli/src/onboarding-brand.ts
18220
+ var onboardingWordmark = '<svg xmlns="http://www.w3.org/2000/svg"\n width="873" height="131" viewBox="0 0 873.000000 131.000000"\n preserveAspectRatio="xMidYMid meet">\n<!-- Traced from approved Thought Loop lettering, excluding residual background. -->\n<g transform="translate(0.000000,131.000000) scale(0.100000,-0.100000)"\nfill="#f3ede3" stroke="none">\n<path d="M2116 1290 c-41 -13 -63 -36 -82 -87 -14 -36 -15 -50 -6 -81 31 -102\n157 -139 231 -68 50 48 58 116 23 183 -25 46 -107 72 -166 53z"/>\n<path d="M7491 1282 c-69 -34 -97 -113 -68 -185 30 -71 133 -101 204 -57 94\n57 80 202 -24 246 -43 19 -71 18 -112 -4z"/>\n<path d="M4771 1268 c-37 -13 -41 -39 -41 -314 0 -502 11 -894 27 -910 10 -10\n39 -14 97 -14 87 0 116 13 116 53 0 22 12 21 43 -3 75 -60 226 -86 338 -60\n129 30 250 123 302 233 31 66 57 171 57 230 -1 171 -105 339 -257 414 -67 33\n-85 38 -169 41 -113 5 -170 -7 -246 -53 -31 -19 -60 -35 -65 -35 -4 0 -6 84\n-5 188 5 230 5 229 -105 234 -40 2 -81 0 -92 -4z m552 -578 c59 -29 83 -51\n112 -105 32 -61 35 -157 6 -213 -25 -48 -77 -97 -123 -116 -51 -22 -153 -20\n-203 3 -145 66 -185 260 -77 375 67 72 198 97 285 56z"/>\n<path d="M1699 1243 c-11 -13 -13 -56 -11 -205 2 -114 0 -188 -6 -188 -5 0\n-35 15 -67 34 -87 53 -175 67 -285 46 -210 -40 -360 -232 -360 -461 0 -256\n193 -459 436 -459 77 0 155 25 227 74 l52 34 6 -29 c11 -52 24 -59 113 -59 44\n0 86 4 92 8 33 22 36 52 29 330 -3 152 -7 412 -10 576 -3 233 -7 301 -17 307\n-7 5 -52 9 -99 9 -69 0 -89 -3 -100 -17z m-133 -569 c137 -84 153 -274 33\n-377 -121 -104 -299 -65 -377 83 -21 38 -17 151 6 197 65 124 221 170 338 97z"/>\n<path d="M2543 1168 c-4 -7 -10 -65 -13 -128 l-5 -115 -60 -3 c-32 -2 -65 -8\n-72 -14 -9 -7 -13 -38 -13 -94 0 -103 8 -114 90 -114 l58 0 4 -232 c3 -261 9\n-284 82 -366 101 -111 320 -117 422 -11 l29 30 -35 65 c-47 89 -62 99 -111 73\n-49 -24 -80 -24 -112 1 -39 31 -47 77 -47 267 l0 171 98 4 c128 4 132 7 132\n110 0 106 -3 108 -129 108 l-101 0 0 114 c0 146 0 146 -120 146 -60 0 -91 -4\n-97 -12z"/>\n<path d="M4515 941 c-49 -12 -108 -47 -144 -85 -43 -44 -51 -45 -51 -5 0 62\n-11 69 -108 69 -55 0 -95 -5 -105 -12 -16 -12 -17 -46 -15 -431 3 -489 -8\n-447 123 -447 118 0 113 -12 117 260 3 189 6 231 21 265 39 87 122 144 212\n145 75 0 78 5 83 113 5 119 -3 137 -61 136 -23 -1 -55 -4 -72 -8z"/>\n<path d="M6218 940 c-47 -14 -135 -69 -152 -95 -23 -36 -36 -31 -36 13 0 21\n-5 43 -12 50 -16 16 -180 16 -196 0 -9 -9 -12 -119 -12 -435 0 -489 -12 -443\n118 -443 116 0 112 -8 112 241 0 238 9 286 68 351 48 53 102 78 173 78 30 0\n60 5 67 12 8 8 12 48 12 113 0 65 -4 105 -12 113 -13 13 -89 15 -130 2z"/>\n<path d="M8259 940 c-54 -9 -133 -48 -172 -84 -33 -30 -47 -26 -47 13 0 15 -5\n32 -12 39 -14 14 -166 16 -188 2 -13 -8 -15 -67 -15 -428 0 -337 3 -423 14\n-436 10 -12 32 -16 97 -16 129 0 124 -11 124 273 0 129 5 249 10 269 43 154\n261 187 343 53 l27 -46 0 -253 c0 -309 -5 -296 125 -296 135 0 127 -21 123\n343 -3 290 -4 304 -26 352 -34 74 -68 118 -115 152 -75 54 -192 80 -288 63z"/>\n<path d="M350 926 c-102 -29 -192 -95 -249 -184 -97 -153 -97 -381 0 -534 89\n-139 233 -206 422 -195 76 4 99 10 163 41 72 36 174 124 174 151 0 16 -47 57\n-107 91 l-45 27 -26 -22 c-90 -72 -101 -76 -193 -76 -81 0 -90 2 -131 30 -43\n31 -88 95 -88 128 0 16 22 17 314 17 314 0 315 0 330 22 41 58 -8 274 -79 350\n-14 15 -25 30 -25 34 0 20 -128 100 -190 118 -64 19 -205 20 -270 2z m239\n-215 c44 -23 86 -72 96 -113 l7 -28 -211 0 c-171 0 -211 3 -211 14 0 17 45 84\n73 107 58 50 170 58 246 20z"/>\n<path d="M3401 924 c-244 -65 -390 -320 -326 -567 88 -338 502 -461 774 -230\n98 84 151 205 151 348 0 50 -7 111 -15 138 -46 153 -170 271 -328 313 -66 18\n-187 17 -256 -2z m239 -242 c62 -32 115 -100 125 -163 33 -194 -166 -341 -343\n-253 -51 26 -79 55 -115 123 -22 43 -16 134 13 190 60 117 205 164 320 103z"/>\n<path d="M6723 930 c-47 -10 -146 -55 -175 -79 -11 -9 -28 -23 -37 -31 -10 -8\n-38 -45 -63 -81 -61 -90 -82 -168 -76 -296 3 -88 7 -106 41 -176 77 -164 225\n-257 407 -257 64 0 167 33 218 70 51 37 62 37 62 4 0 -42 25 -53 117 -54 73 0\n83 2 97 22 14 19 16 76 16 418 0 298 -3 401 -13 420 -12 23 -18 25 -101 28\n-95 3 -116 -5 -116 -43 0 -35 -11 -37 -48 -9 -75 58 -223 87 -329 64z m243\n-256 c73 -45 107 -100 112 -180 6 -85 -16 -141 -73 -192 -109 -95 -276 -79\n-358 36 -27 37 -32 54 -35 116 -3 48 1 84 10 106 57 136 221 190 344 114z"/>\n<path d="M2065 896 c-14 -20 -15 -76 -13 -431 3 -476 -8 -435 125 -435 123 0\n115 -32 112 447 -1 331 -4 402 -17 420 -13 21 -22 23 -103 23 -82 0 -90 -2\n-104 -24z"/>\n<path d="M7459 903 c-11 -13 -14 -94 -14 -430 0 -483 -10 -443 115 -443 119 0\n110 -35 110 447 0 482 10 443 -117 443 -63 0 -84 -4 -94 -17z"/>\n</g>\n</svg>\n';
18221
+
17654
18222
  // packages/cli/src/onboarding-artifact.ts
17655
18223
  function safeJson(value) {
17656
18224
  return JSON.stringify(value).replace(/</g, "\\u003c").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
@@ -17669,12 +18237,11 @@ function renderOnboardingArtifact({ intake, submission }) {
17669
18237
  return false;
17670
18238
  }
17671
18239
  }).map((example) => ({ ratio: example.ratio, url: example.url })) }));
17672
- const data = { intake: validateOnboardingIntake(intake), submission: submission || null, options: profileOptions, styles };
18240
+ const data = { intake: validateOnboardingIntake(intake), submission: submission || null, options: profileOptions, styles, steps: localOnboardingSteps };
17673
18241
  return `<!doctype html>
17674
18242
  <html lang="es"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta name="referrer" content="no-referrer"><title>Tu cerebro editorial \xB7 EditorBrain</title><style>${artifactCss}</style></head>
17675
- <body><div class="app"><header class="top"><a href="#main" class="product-label" aria-label="EditorBrain, ir a tus preferencias">EditorBrain</a><span class="local-badge">En tu computadora</span></header>
17676
- <div class="layout"><aside class="sidebar"><p class="eyebrow">TU CEREBRO EDITORIAL</p><h1>Tu manera de<br><em>contar las cosas.</em></h1><p>Elige lo que tengas claro. Tu agente adapta estas preferencias a cada video.</p><nav id="steps" aria-label="Pasos de tus preferencias"></nav><div class="privacy"><strong>T\xFA decides; tu agente lo aplica.</strong><p>No necesitas conectar servicios ni compartir claves. Guardar preferencias no genera im\xE1genes ni activa compras.</p></div></aside>
17677
- <main id="main" tabindex="-1"><div class="progress" aria-hidden="true"><i id="progress-fill"></i></div><div class="page-top"><span id="step-count" class="eyebrow"></span><button type="button" id="download-top" class="text-button">Descargar respuestas</button></div><h2 id="page-title" tabindex="-1"></h2><p id="page-description" class="intro"></p><form id="preferences" novalidate><div id="panel"></div></form><div id="error" class="notice error" role="alert" hidden></div><div id="status" class="notice" role="status" hidden></div><footer class="actions"><button type="button" id="previous" class="button secondary">\u2190 Atr\xE1s</button><span id="save-hint">Puedes cambiar tus preferencias despu\xE9s.</span><button type="button" id="next" class="button primary">Continuar \u2192</button></footer></main></div><footer class="bottom">EditorBrain \xB7 Tu criterio, en cada edici\xF3n.<span>Las elecciones pendientes quedan a criterio de tu agente.</span></footer></div>
18243
+ <body data-theme="paper"><div class="app"><header class="top"><a href="#main" class="product-label" aria-label="EditorBrain, ir a tus preferencias">${onboardingWordmark}</a><span class="local-badge">Tu estudio \xB7 En tu computadora</span></header>
18244
+ <nav id="steps" aria-label="Pasos de tus preferencias"></nav><div class="layout"><main id="main" tabindex="-1"><div class="page-top"><span id="step-count" class="eyebrow"></span><button type="button" id="download-top" class="text-button">Descargar respuestas</button></div><h1 id="page-title" tabindex="-1"></h1><p id="page-description" class="intro"></p><form id="preferences" novalidate><div id="panel"></div></form><div id="error" class="notice error" role="alert" hidden></div><div id="status" class="notice" role="status" hidden></div><footer class="actions"><button type="button" id="previous" class="button secondary">\u2190 Atr\xE1s</button><span id="save-hint">Puedes cambiar tus preferencias despu\xE9s.</span><button type="button" id="next" class="button primary">Continuar \u2192</button></footer></main></div><footer class="bottom">EditorBrain \xB7 Tu criterio, en cada edici\xF3n.<span>Preferencias locales. No conecta servicios ni activa compras.</span></footer></div>
17678
18245
  <dialog id="style-dialog" aria-labelledby="dialog-title"><div class="dialog-head"><div><span class="eyebrow">REFERENCIA DE APARIENCIA</span><h2 id="dialog-title"></h2></div><button type="button" id="close-dialog" class="button secondary">Cerrar</button></div><p id="dialog-description"></p><div id="dialog-examples"></div><p class="small">Ejemplos generados con IA. Orientan el tratamiento visual; no sustituyen tu marca ni acreditan hechos reales.</p></dialog>
17679
18246
  <script id="onboarding-data" type="application/json">${safeJson(data)}</script><script>${artifactScript}</script></body></html>`;
17680
18247
  }
@@ -17693,22 +18260,13 @@ h1,h2,h3,p{margin-top:0}
17693
18260
  .top{height:88px;border-bottom:1px solid var(--line);display:flex;align-items:center;justify-content:space-between;gap:18px}
17694
18261
  .product-label{font-size:17px;font-weight:600;letter-spacing:0;text-decoration:none;color:var(--accent);display:flex;align-items:center;gap:7px}
17695
18262
  .local-badge{padding:6px 11px;border:1px solid #d9c7b8;border-radius:30px;color:#7c645c;font-size:11px}
17696
- .layout{display:grid;grid-template-columns:285px minmax(0,1fr);gap:65px;padding:54px 0 75px}
17697
- .sidebar{position:sticky;top:32px;align-self:start}
17698
- .eyebrow{font-size:10px;letter-spacing:.14em;font-weight:650;color:var(--accent);text-transform:uppercase}
17699
- .sidebar h1{font:400 40px/1.08 Georgia,serif;letter-spacing:-1.7px;margin:19px 0}
17700
- .sidebar h1 em{color:var(--accent);font-weight:400}
17701
- .sidebar>p:not(.eyebrow){font-size:13px;max-width:260px;color:var(--muted)}
17702
- #steps{display:grid;gap:7px;margin-top:32px}
17703
- #steps button{display:flex;align-items:center;gap:12px;text-align:left;border:1px solid transparent;background:transparent;border-radius:10px;padding:11px 13px;font-size:13px;color:var(--muted)}
17704
- #steps button>span:first-child{border:1px solid #cfbcae;border-radius:50%;width:27px;height:27px;display:grid;place-items:center;font-size:11px;flex-shrink:0}
17705
- #steps button[aria-current=step]{color:var(--accent);background:#fff9f1;border-color:#d9c4b5;box-shadow:0 8px 24px -18px #54293675}
17706
- #steps button[aria-current=step]>span:first-child{background:var(--accent);color:#fff;border-color:var(--accent)}
17707
- .privacy{border-top:1px solid var(--line);margin-top:32px;padding-top:22px;font-size:11px;color:var(--muted)}
17708
- .privacy strong{color:#5f4a42;font-size:12px}
17709
- .privacy p{margin-top:8px}
17710
- .progress{height:3px;background:#e2d3c3;border-radius:3px;overflow:hidden;margin-bottom:23px}
17711
- .progress i{display:block;height:100%;background:var(--accent);transition:width .25s}
18263
+ .layout{max-width:1120px;margin:auto;padding:44px 0 56px}
18264
+ .eyebrow{font-size:11px;letter-spacing:.13em;font-weight:650;text-transform:uppercase}
18265
+ #steps{display:flex;gap:8px;max-width:680px;margin:30px auto 0;padding:10px 18px;border:1px solid #85696135;border-radius:99px;background:#ffffff0a}
18266
+ #steps button{flex:1;min-width:0;border:0;background:transparent;padding:7px 0;color:inherit;font-size:10px}
18267
+ .step-bar{display:block;height:8px;border:1px solid #a78f8055;border-radius:3px;margin-bottom:7px}
18268
+ #steps [aria-current=step] .step-bar{background:#e89a4a;box-shadow:0 0 14px #e89a4a35;border-color:#e89a4a}
18269
+ #steps [data-done=true] .step-bar{background:#e89a4a65}
17712
18270
  .page-top{display:flex;align-items:center;justify-content:space-between;gap:14px}
17713
18271
  h2{font:400 clamp(30px,3.7vw,45px)/1.12 Georgia,serif;letter-spacing:-1.4px;margin:20px 0 14px}
17714
18272
  h2:focus,#main:focus{outline:none}
@@ -17849,11 +18407,6 @@ dialog::backdrop{background:#291922bd;backdrop-filter:blur(5px)}
17849
18407
  @media(max-width:780px){.app{padding:0 20px}
17850
18408
  .top{height:70px}
17851
18409
  .layout{display:block;padding-top:24px}
17852
- .sidebar{position:static;margin-bottom:26px}
17853
- .sidebar>.eyebrow,.sidebar h1,.sidebar>p,.privacy{display:none}
17854
- #steps{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:5px;margin-top:0}
17855
- #steps button{padding:8px;font-size:10px;gap:7px}
17856
- #steps button>span:first-child{height:23px;width:23px;font-size:10px}
17857
18410
  .page-top{align-items:flex-start}
17858
18411
  h2{font-size:34px}
17859
18412
  .section-head{flex-wrap:wrap}
@@ -17886,6 +18439,30 @@ dialog{padding:20px}
17886
18439
  #dialog-examples{grid-template-columns:1fr}
17887
18440
  .dialog-head h2{font-size:25px}
17888
18441
  }
18442
+
18443
+ body{background:radial-gradient(ellipse at 85% 10%,#edb88420,transparent 55%),#f3ede3;transition:background-color .35s}
18444
+ body[data-theme=wine]{background-color:#351d29;color:#f3ede3}
18445
+ body[data-theme=porcelain]{background-color:#162a24;color:#f3ede3}
18446
+ body:not([data-theme=paper]) .intro,body:not([data-theme=paper]) .bottom,body:not([data-theme=paper]) .actions>span{color:#d4c4b5}
18447
+ body:not([data-theme=paper]) .eyebrow,body:not([data-theme=paper]) .text-button{color:#efb678}
18448
+ body:not([data-theme=paper]) .top,body:not([data-theme=paper]) .bottom,body:not([data-theme=paper]) .actions{border-color:#f3ede32b}
18449
+ body:not([data-theme=paper]) .local-badge{border-color:#f3ede32b;color:#f3ede3}
18450
+ .product-label svg{width:174px;height:auto}
18451
+ body[data-theme=paper] .product-label svg{filter:brightness(0)}
18452
+ #page-title{font:400 clamp(40px,5vw,70px)/1.04 Georgia,serif;letter-spacing:-.045em;max-width:850px;margin:22px 0;outline:none}
18453
+ .intro{font-size:16px;line-height:1.7;max-width:730px;margin-bottom:32px}
18454
+ #preferences{padding:clamp(20px,3vw,36px);border:1px solid #ffffffa6;border-radius:24px;background:#fbf7f0f5;color:var(--ink);box-shadow:0 24px 70px #100a0614,inset 0 1px 0 #ffffffcc}
18455
+ #panel{animation:studio-enter .45s ease-out both}
18456
+ #panel .section:last-child{margin-bottom:0}
18457
+ #panel .text-button{color:var(--accent)}
18458
+ .choice{background:#fffdf6b8;box-shadow:inset 0 1px 0 #ffffffcc;transition:transform .25s,border-color .25s,box-shadow .25s}
18459
+ .choice:hover{transform:translateY(-2px);box-shadow:0 8px 24px #682e3b10}
18460
+ .actions .button{border-radius:99px;padding:14px 24px}
18461
+ .actions .button.primary{background:#e89a4a;color:#211914;border-color:#e89a4a;font-weight:650}
18462
+ body:not([data-theme=paper]) .actions .secondary{color:#f3ede3;border-color:#f3ede32b}
18463
+ @keyframes studio-enter{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:none}}
18464
+ @media(max-width:600px){.layout{padding-top:30px}.step-label{position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%)}#steps{gap:6px;margin-top:20px}.step-bar{margin-bottom:0}.top{height:72px}.product-label svg{width:142px}.local-badge{font-size:9px}#page-title{font-size:43px}#preferences{padding:18px;border-radius:18px}.intro{font-size:14px}}
18465
+ @media(prefers-reduced-transparency:reduce){#preferences,.choice{background:#fbf7f0}}
17889
18466
  @media(prefers-reduced-motion:reduce){*,*:before,*:after{animation:none!important;transition:none!important;scroll-behavior:auto!important}
17890
18467
  }
17891
18468
 
@@ -17895,9 +18472,10 @@ var artifactScript = String.raw`
17895
18472
  'use strict';
17896
18473
  const data = JSON.parse(document.getElementById('onboarding-data').textContent), intake = structuredClone(data.intake), options = data.options, catalog = data.styles;
17897
18474
  const $ = id => document.getElementById(id), panel = $('panel'), error = $('error'), status = $('status');
18475
+ let sfxExpanded = false;
17898
18476
  let step = 0, mediaTab = 'image', showImages = false, busy = false, observer, nameEdited = false;
17899
18477
  const editedTokens = new Set();
17900
- const steps = [['Tu contenido', '¿Qué quieres contar?', 'Cuéntanos para quién creas y qué tipo de videos haces. Todo puede cambiar según el proyecto.'], ['Tu marca', 'Tu identidad, en cada escena.', 'Elige colores y tipografías si los tienes claros. Las muestras no cambian tu marca hasta que elijas.'], ['Tus gustos', 'Muéstranos qué te gusta.', 'Guarda referencias de apariencia. Tu agente adapta el concepto a tu tema, tu marca y tu video.'], ['Tu edición', 'Ritmo, imágenes y subtítulos.', 'Elige cuánto apoyo visual quieres y cómo acompañar tus ideas. Ritmo, imágenes y subtítulos son decisiones distintas.'], ['Tu sonido', 'La mezcla que va contigo.', 'Voz, música y efectos se eligen por separado. No hace falta conectar un proveedor para guardar estas preferencias.'], ['Tu cerebro', 'Listo para tu próximo video.', 'Revisa tus decisiones. Lo que dejes pendiente se decidirá con tu agente para cada proyecto.']];
18478
+ const steps = data.steps;
17901
18479
  const p = () => intake.profile || {}, delivery = () => p().editorialDelivery && typeof p().editorialDelivery === 'object' ? p().editorialDelivery : {}, audio = () => intake.audio || {};
17902
18480
  const node = (tag, cls, text) => { const el = document.createElement(tag); if (cls)
17903
18481
  el.className = cls; if (text !== undefined)
@@ -18155,7 +18733,7 @@ var artifactScript = String.raw`
18155
18733
  panel.append(music);
18156
18734
  const sfx = section('Efectos de sonido', 'Clics, acentos y transiciones cuando ayudan.', decide(!current.sfx?.density, () => setAudio('sfx', '')));
18157
18735
  const blocked = (p().avoid || []).includes('Sin efectos de sonido');
18158
- choices(sfx, [['none', 'Sin efectos', 'Nada añadido.'], ['sparse', 'Puntuales', 'Algunos momentos clave.'], ['moderate', 'Expresivos', 'Acompañan más acciones.'], ['dense', 'Muy presentes', 'Un diseño sonoro más intenso.']], blocked ? 'none' : current.sfx?.density, v => setAudio('sfx', v), { columns: 2 });
18736
+ choices(sfx, [['none', 'Sin efectos', 'Nada añadido.'], ['sparse', 'Sutiles', 'Pocos acentos en momentos clave.'], ['moderate', 'Equilibrados', 'Acompañan las acciones importantes.'], ['dense', 'Expresivos', 'Más variedad sin tapar la voz.']], blocked ? 'none' : current.sfx?.density, v => setAudio('sfx', v), { columns: 2 });
18159
18737
  if (blocked) {
18160
18738
  sfx.querySelectorAll('button').forEach(b => { if (b.dataset.value !== 'none') {
18161
18739
  b.disabled = true;
@@ -18163,6 +18741,13 @@ var artifactScript = String.raw`
18163
18741
  } });
18164
18742
  sfx.append(node('p', 'notice', 'Tienes activo «Sin efectos de sonido» en tus límites. Desactívalo antes de elegir otra densidad.'));
18165
18743
  }
18744
+ const detail = node('details', ''); detail.append(node('summary', '', 'Personalizar el carácter y la apertura'));
18745
+ detail.open = sfxExpanded; detail.addEventListener('toggle', () => { if (detail.isConnected) sfxExpanded = detail.open; });
18746
+ const fields = node('fieldset', 'fields'); fields.disabled = blocked || current.sfx?.density === 'none';
18747
+ const setSfx = (key, value) => { intake.audio = { ...effectiveAudio(), sfx: { ...effectiveAudio().sfx, [key]: value } }; changed(); };
18748
+ select(fields, 'Carácter del sonido', current.sfx?.character, [['organic', 'Natural: papel y objetos'], ['digital', 'Digital: clics e interfaces'], ['cinematic', 'Cinematográfico'], ['mixed', 'Una combinación']], v => setSfx('character', v || null));
18749
+ select(fields, 'Apertura sin efectos añadidos', current.sfx?.hookWithoutEffects === true ? 'yes' : current.sfx?.hookWithoutEffects === false ? 'no' : '', [['yes', 'Sí, dejar el hook limpio'], ['no', 'Puede usar efectos si aportan']], v => setSfx('hookWithoutEffects', v ? v === 'yes' : null));
18750
+ fields.append(node('p', 'small', 'La cantidad no es volumen. Conserva voz y ambiente; revisa los acentos en el video final.')); detail.append(fields); sfx.append(detail);
18166
18751
  panel.append(sfx, node('p', 'small', 'Seleccionar una voz en off no clona voces ni autoriza gastos. El agente verificará los archivos, las integraciones y los permisos disponibles.'));
18167
18752
  }
18168
18753
  function reviewStep() {
@@ -18177,7 +18762,7 @@ var artifactScript = String.raw`
18177
18762
  ['Marca', Object.entries(intake.tokens || {}).filter(([, v]) => v).map(([key, rule]) => key.replace('color.', '').replace('typography.', '') + ': ' + rule.value + ' · ' + ({ required: 'obligatorio', preferred: 'preferencia', example: 'ejemplo' }[rule.strength])).join('\n'), 1],
18178
18763
  ['Gustos visuales', selections.join('\n'), 2],
18179
18764
  ['Edición y recursos', [p().pace, delivery().visualSupport && 'Apoyo visual: ' + ({ sparse: 'poco', balanced: 'equilibrado', rich: 'abundante' }[delivery().visualSupport]), p().captions, sources?.allowed?.map(v => sourceNames[v]).join(' · '), sources?.preferred && 'Empezar por: ' + sourceNames[sources.preferred], delivery().ctaGoal && 'Cierre: ' + ({ adaptive: 'según el video', follow: 'seguir mi cuenta', comment: 'comentar', visit: 'visitar un enlace', contact: 'contactarme', none: 'sin llamada a la acción' }[delivery().ctaGoal]), !sources && p().assets, (p().avoid || []).join(' · ')].filter(Boolean).join('\n'), 3],
18180
- ['Narración y sonido', [delivery().narration && 'Narración: ' + ({ original: 'tu voz grabada', voiceover: 'voz en off', none: 'sin narración' }[delivery().narration]), effectiveAudio().music?.intensity && 'Música: ' + ({ none: 'sin música', subtle: 'de fondo', present: 'con presencia' }[effectiveAudio().music.intensity]), (p().avoid || []).includes('Sin efectos de sonido') ? 'Efectos: desactivados por tus límites' : effectiveAudio().sfx?.density && 'Efectos: ' + ({ none: 'sin efectos', sparse: 'puntuales', moderate: 'expresivos', dense: 'muy presentes' }[effectiveAudio().sfx.density])].filter(Boolean).join('\n'), 4],
18765
+ ['Narración y sonido', [delivery().narration && 'Narración: ' + ({ original: 'tu voz grabada', voiceover: 'voz en off', none: 'sin narración' }[delivery().narration]), effectiveAudio().music?.intensity && 'Música: ' + ({ none: 'sin música', subtle: 'de fondo', present: 'con presencia' }[effectiveAudio().music.intensity]), (p().avoid || []).includes('Sin efectos de sonido') ? 'Efectos: desactivados por tus límites' : effectiveAudio().sfx?.density && 'Efectos: ' + ({ none: 'sin efectos', sparse: 'sutiles', moderate: 'equilibrados', dense: 'expresivos' }[effectiveAudio().sfx.density]), effectiveAudio().sfx?.character && 'Carácter: ' + ({organic:'natural',digital:'digital',cinematic:'cinematográfico',mixed:'combinado'}[effectiveAudio().sfx.character]), effectiveAudio().sfx?.hookWithoutEffects === true && 'Apertura sin efectos añadidos'].filter(Boolean).join('\n'), 4],
18181
18766
  ];
18182
18767
  rows.forEach(([title, text, target]) => { const item = node('section', 'review-item'); append(item, node('h3', '', title), node('p', '', text || 'Pendiente: tu agente lo decidirá para cada proyecto.'), button('Cambiar ' + title.toLowerCase(), () => navigate(target), 'text-button')); list.append(item); });
18183
18768
  panel.append(list);
@@ -18195,13 +18780,13 @@ var artifactScript = String.raw`
18195
18780
  $('page-title').textContent = steps[step][1];
18196
18781
  $('page-description').textContent = steps[step][2];
18197
18782
  $('step-count').textContent = 'Paso ' + (step + 1) + ' de ' + steps.length;
18198
- $('progress-fill').style.width = ((step + 1) / steps.length * 100) + '%';
18783
+ document.body.dataset.theme = ['paper','paper','wine','wine','wine','porcelain'][step];
18199
18784
  $('previous').hidden = step === 0;
18200
18785
  $('next').hidden = step === steps.length - 1;
18201
18786
  $('next').textContent = step === steps.length - 2 ? 'Revisar mis preferencias →' : 'Continuar →';
18202
18787
  const nav = $('steps');
18203
18788
  nav.replaceChildren();
18204
- steps.forEach(([title], index) => { const b = button('', () => navigate(index)); append(b, node('span', '', String(index + 1)), node('span', '', title)); if (index === step)
18789
+ steps.forEach(([title], index) => { const b = button('', () => navigate(index)); append(b, node('span', 'step-bar'), node('span', 'step-label', title)); b.setAttribute('aria-label', 'Paso ' + (index + 1) + ': ' + title); b.dataset.done = String(index < step); if (index === step)
18205
18790
  b.setAttribute('aria-current', 'step'); nav.append(b); });
18206
18791
  [contentStep, brandStep, styleStep, editingStep, soundStep, reviewStep][step]();
18207
18792
  updateBrandPreview();
@@ -18341,7 +18926,7 @@ async function syncOnboardingDocuments(client, root, files, options) {
18341
18926
  // packages/cli/src/onboarding.ts
18342
18927
  var failure = (code, message, hint = "Reopen onboarding against the current files. Nothing is overwritten silently.") => new CliError(code, message, hint);
18343
18928
  function starterOutputId(brandId2) {
18344
- return `onboarding-${brandId2.slice(0, 48)}-${createHash2("sha256").update(brandId2).digest("hex").slice(0, 12)}`;
18929
+ return `onboarding-${brandId2.slice(0, 48)}-${createHash4("sha256").update(brandId2).digest("hex").slice(0, 12)}`;
18345
18930
  }
18346
18931
  function scaffold(brandId2, projectId) {
18347
18932
  const files = Object.fromEntries(Object.entries(createWorkspace({ brand: brandId2, project: projectId })).filter(([p]) => !p.startsWith(`.editorbrain/brands/${brandId2}/`)));
@@ -18397,14 +18982,14 @@ async function createOnboarding(options) {
18397
18982
  const directory = options.out ?? `.editorbrain-local/onboarding/${brandId2}-${Date.now()}-${randomBytes(3).toString("hex")}`;
18398
18983
  await productionOutputPath(root, `${directory}/session.json`);
18399
18984
  const target = await checkedPath(root, directory, false);
18400
- await mkdir3(dirname3(target), { recursive: true });
18985
+ await mkdir3(dirname6(target), { recursive: true });
18401
18986
  await mkdir3(target);
18402
18987
  const saved = await snapshots(root, brandId2, projectId), files = Object.fromEntries(Object.entries(saved).flatMap(([p, s]) => s ? [[p, s.content]] : []));
18403
18988
  const intake = onboardingIntakeFromFiles(brandId2, files);
18404
18989
  const session = { schemaVersion: 1, kind: "editorbrain.onboarding", brandId: brandId2, projectId, intake, snapshots: saved };
18405
18990
  await atomicWrite(root, `${directory}/session.json`, stableJson(session), { document: false, overwrite: false, mode: 384 });
18406
18991
  await atomicWrite(root, `${directory}/index.html`, renderOnboardingArtifact({ intake }), { document: false, overwrite: false, mode: 384 });
18407
- return { schemaVersion: 1, brandId: brandId2, projectId, artifact: join2(root, directory, "index.html"), session: `${directory}/session.json`, answers: `${directory}/answers.json`, savedToBrain: false, next: "Show the HTML artifact, or run onboarding serve --session <session>. Static artifacts download answers: put them in this workspace and run onboarding apply --session <session> --file <answers>. Never fill choices on the user\u2019s behalf." };
18992
+ return { schemaVersion: 1, brandId: brandId2, projectId, artifact: join5(root, directory, "index.html"), session: `${directory}/session.json`, answers: `${directory}/answers.json`, savedToBrain: false, next: "Show the HTML artifact, or run onboarding serve --session <session>. Static artifacts download answers: put them in this workspace and run onboarding apply --session <session> --file <answers>. Never fill choices on the user\u2019s behalf." };
18408
18993
  }
18409
18994
  async function applyOnboarding(options) {
18410
18995
  const root = await ensureRoot(options.directory);
@@ -18432,7 +19017,7 @@ async function applyOnboarding(options) {
18432
19017
  if (now !== current2[item.path]?.content) throw failure("ONBOARDING_CONFLICT", `Document changed while applying: ${item.path}`);
18433
19018
  await atomicWrite(root, item.path, item.content, { overwrite: now !== void 0 });
18434
19019
  }
18435
- await atomicWrite(root, `${dirname3(options.session)}/applied.json`, stableJson({ schemaVersion: 1, brandId: session.brandId, projectId: session.projectId, appliedAt: (/* @__PURE__ */ new Date()).toISOString(), paths: Object.keys(merged) }), { document: false, mode: 384 });
19020
+ await atomicWrite(root, `${dirname6(options.session)}/applied.json`, stableJson({ schemaVersion: 1, brandId: session.brandId, projectId: session.projectId, appliedAt: (/* @__PURE__ */ new Date()).toISOString(), paths: Object.keys(merged) }), { document: false, mode: 384 });
18436
19021
  }
18437
19022
  return { schemaVersion: 1, brandId: session.brandId, projectId: session.projectId, dryRun: !!options.dryRun, changed: pending.map((w) => w.path), documents: Object.keys(merged).length, brainGenerated: !options.dryRun, aiAnalyzed: false, cloudSynced: false, next: "Local documents are ready after apply. Install the skill if needed, authenticate, use onboarding sync for this session, then brain prepare. Review a proposal before brain accept. A brain is editorial context, not a rendered video." };
18438
19023
  }, { migrateLegacy: !options.dryRun });
@@ -18441,7 +19026,7 @@ async function syncOnboarding(client, options) {
18441
19026
  const root = await ensureRoot(options.directory);
18442
19027
  return withWorkspaceLock(root, async () => {
18443
19028
  const session = await readSession(root, options.session);
18444
- const receipt = await readProductionJson(root, `${dirname3(options.session)}/applied.json`);
19029
+ const receipt = await readProductionJson(root, `${dirname6(options.session)}/applied.json`);
18445
19030
  if (receipt.brandId !== session.brandId || receipt.projectId !== session.projectId) throw failure("ONBOARDING_NOT_APPLIED", "Apply the reviewed choices locally before syncing.");
18446
19031
  const files = {};
18447
19032
  for (const path of paths(session.brandId, session.projectId)) {
@@ -18457,7 +19042,7 @@ async function syncOnboarding(client, options) {
18457
19042
  async function serveOnboarding(options) {
18458
19043
  const root = await ensureRoot(options.directory), session = await readSession(root, options.session), seconds = options.timeoutSeconds ?? 1800;
18459
19044
  if (!Number.isInteger(seconds) || seconds < 1 || seconds > 3600) throw failure("INVALID_ARGUMENT", "Timeout must be between 1 and 3600 seconds.");
18460
- const token = randomBytes(32).toString("hex"), csrf = randomBytes(32).toString("hex"), prefix = `/${token}`, answers = `${dirname3(options.session)}/answers.json`;
19045
+ const token = randomBytes(32).toString("hex"), csrf = randomBytes(32).toString("hex"), prefix = `/${token}`, answers = `${dirname6(options.session)}/answers.json`;
18461
19046
  let origin = "", consumed = false, busy = false, server;
18462
19047
  let complete, reject;
18463
19048
  const done = new Promise((yes, no) => {
@@ -18497,7 +19082,7 @@ async function serveOnboarding(options) {
18497
19082
  await atomicWrite(root, answers, stableJson(intake), { document: false, overwrite: false, mode: 384 });
18498
19083
  consumed = true;
18499
19084
  send(200, { saved: true, path: answers });
18500
- complete({ schemaVersion: 1, saved: true, answers: join2(root, answers), session: options.session, applied: false, next: "Review saved choices and run onboarding apply with this session and answers file." });
19085
+ complete({ schemaVersion: 1, saved: true, answers: join5(root, answers), session: options.session, applied: false, next: "Review saved choices and run onboarding apply with this session and answers file." });
18501
19086
  } catch (e) {
18502
19087
  busy = false;
18503
19088
  send(400, { error: e.message.slice(0, 300) });
@@ -18517,7 +19102,7 @@ async function serveOnboarding(options) {
18517
19102
  process.once("SIGINT", stop);
18518
19103
  process.once("SIGTERM", stop);
18519
19104
  try {
18520
- options.onReady?.({ url: `${origin}${prefix}/`, answers: join2(root, answers) });
19105
+ options.onReady?.({ url: `${origin}${prefix}/`, answers: join5(root, answers) });
18521
19106
  return await done;
18522
19107
  } finally {
18523
19108
  clearTimeout(timer);
@@ -18529,11 +19114,11 @@ async function serveOnboarding(options) {
18529
19114
  }
18530
19115
 
18531
19116
  // packages/cli/src/brain.ts
18532
- var object5 = (value) => !!value && typeof value === "object" && !Array.isArray(value);
19117
+ var object6 = (value) => !!value && typeof value === "object" && !Array.isArray(value);
18533
19118
  var integer = (value, min = 0) => Number.isSafeInteger(value) && Number(value) >= min;
18534
- var text6 = (value, max) => typeof value === "string" && !!value.trim() && value.length <= max && !/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/.test(value);
18535
- var hash3 = (value) => typeof value === "string" && /^[a-f0-9]{64}$/.test(value);
18536
- var date = (value) => text6(value, 64) && Number.isFinite(Date.parse(value));
19119
+ var text7 = (value, max) => typeof value === "string" && !!value.trim() && value.length <= max && !/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/.test(value);
19120
+ var hash4 = (value) => typeof value === "string" && /^[a-f0-9]{64}$/.test(value);
19121
+ var date = (value) => text7(value, 64) && Number.isFinite(Date.parse(value));
18537
19122
  function invalidInput(message) {
18538
19123
  return new CliError("INVALID_ARGUMENT", message, "Use brain status --brand ID; read the current design.md revision before preparing. Acceptance requires the reviewed run and its source revision.");
18539
19124
  }
@@ -18546,10 +19131,10 @@ function brainRevision(value) {
18546
19131
  return Number(value);
18547
19132
  }
18548
19133
  function validateInput(input, accept = false) {
18549
- if (!object5(input) || Object.keys(input).some((key) => !(accept ? ["brandId", "expectedDesignRevision", "runId", "reviewed"] : ["brandId", "expectedDesignRevision"]).includes(key))) throw invalidInput("Only the brand, document revision and explicit reviewed run are accepted.");
19134
+ if (!object6(input) || Object.keys(input).some((key) => !(accept ? ["brandId", "expectedDesignRevision", "runId", "reviewed"] : ["brandId", "expectedDesignRevision"]).includes(key))) throw invalidInput("Only the brand, document revision and explicit reviewed run are accepted.");
18550
19135
  brandId(input.brandId);
18551
19136
  if (!integer(input.expectedDesignRevision, 1)) throw invalidInput("A positive source design revision is required.");
18552
- if (accept && (!hash3(input.runId) || input.reviewed !== true)) throw invalidInput("Accept only a specific proposal already reviewed and approved by the user; --run and --reviewed are required.");
19137
+ if (accept && (!hash4(input.runId) || input.reviewed !== true)) throw invalidInput("Accept only a specific proposal already reviewed and approved by the user; --run and --reviewed are required.");
18553
19138
  }
18554
19139
  function invalidResponse(write) {
18555
19140
  return new CliError(
@@ -18561,31 +19146,31 @@ function invalidResponse(write) {
18561
19146
  );
18562
19147
  }
18563
19148
  function brainState(value, brand, write = false) {
18564
- const invalid4 = () => {
19149
+ const invalid5 = () => {
18565
19150
  throw invalidResponse(write);
18566
19151
  };
18567
- if (!object5(value) || value.brandId !== brand || typeof value.available !== "boolean" || typeof value.configured !== "boolean" || !text6(value.notice, 2e3) || value.contextVersion !== void 0 && !hash3(value.contextVersion) || value.contextChanged !== void 0 && typeof value.contextChanged !== "boolean") return invalid4();
19152
+ if (!object6(value) || value.brandId !== brand || typeof value.available !== "boolean" || typeof value.configured !== "boolean" || !text7(value.notice, 2e3) || value.contextVersion !== void 0 && !hash4(value.contextVersion) || value.contextChanged !== void 0 && typeof value.contextChanged !== "boolean") return invalid5();
18568
19153
  let run = null;
18569
19154
  if (value.run !== null) {
18570
19155
  const row = value.run;
18571
- if (!object5(row) || !hash3(row.id) || !["queued", "running", "ready", "accepted", "failed"].includes(String(row.status)) || !integer(row.sourceDesignRevision, 1) || !(row.acceptedDesignRevision === null || integer(row.acceptedDesignRevision, 1)) || !(row.errorCode === null || typeof row.errorCode === "string" && /^[a-z][a-z0-9_]{0,119}$/.test(row.errorCode)) || !date(row.createdAt) || !date(row.updatedAt)) return invalid4();
18572
- if (row.status === "accepted" ? row.acceptedDesignRevision !== row.sourceDesignRevision + 1 : row.acceptedDesignRevision !== null) return invalid4();
19156
+ if (!object6(row) || !hash4(row.id) || !["queued", "running", "ready", "accepted", "failed"].includes(String(row.status)) || !integer(row.sourceDesignRevision, 1) || !(row.acceptedDesignRevision === null || integer(row.acceptedDesignRevision, 1)) || !(row.errorCode === null || typeof row.errorCode === "string" && /^[a-z][a-z0-9_]{0,119}$/.test(row.errorCode)) || !date(row.createdAt) || !date(row.updatedAt)) return invalid5();
19157
+ if (row.status === "accepted" ? row.acceptedDesignRevision !== row.sourceDesignRevision + 1 : row.acceptedDesignRevision !== null) return invalid5();
18573
19158
  let proposal = null;
18574
19159
  if (row.proposal !== null) {
18575
19160
  const proposed = row.proposal;
18576
- if (!object5(proposed) || !text6(proposed.summary, 700) || !Array.isArray(proposed.recommendations) || proposed.recommendations.length < 1 || proposed.recommendations.length > 5) return invalid4();
19161
+ if (!object6(proposed) || !text7(proposed.summary, 700) || !Array.isArray(proposed.recommendations) || proposed.recommendations.length < 1 || proposed.recommendations.length > 5) return invalid5();
18577
19162
  const recommendations = [];
18578
19163
  for (const item of proposed.recommendations) {
18579
- if (!object5(item) || !["images", "motion", "story", "audio"].includes(String(item.area)) || !text6(item.text, 500) || !text6(item.reason, 500) || !Array.isArray(item.sourceFields) || item.sourceFields.length < 1 || item.sourceFields.length > 5 || new Set(item.sourceFields).size !== item.sourceFields.length || item.sourceFields.some((field) => !text6(field, 400))) return invalid4();
19164
+ if (!object6(item) || !["images", "motion", "story", "audio"].includes(String(item.area)) || !text7(item.text, 500) || !text7(item.reason, 500) || !Array.isArray(item.sourceFields) || item.sourceFields.length < 1 || item.sourceFields.length > 5 || new Set(item.sourceFields).size !== item.sourceFields.length || item.sourceFields.some((field) => !text7(field, 400))) return invalid5();
18580
19165
  recommendations.push({ area: item.area, text: item.text, reason: item.reason, sourceFields: [...item.sourceFields] });
18581
19166
  }
18582
19167
  proposal = { summary: proposed.summary, recommendations };
18583
19168
  }
18584
- if ((row.status === "ready" || row.status === "accepted") && !proposal) return invalid4();
19169
+ if ((row.status === "ready" || row.status === "accepted") && !proposal) return invalid5();
18585
19170
  let usage = null;
18586
19171
  if (row.usage !== null) {
18587
19172
  const measured = row.usage;
18588
- if (!object5(measured) || !["inputTokens", "outputTokens", "totalTokens"].every((field) => measured[field] === null || integer(measured[field])) || typeof measured.durationMs !== "number" || !Number.isFinite(measured.durationMs) || measured.durationMs < 0) return invalid4();
19173
+ if (!object6(measured) || !["inputTokens", "outputTokens", "totalTokens"].every((field) => measured[field] === null || integer(measured[field])) || typeof measured.durationMs !== "number" || !Number.isFinite(measured.durationMs) || measured.durationMs < 0) return invalid5();
18589
19174
  usage = { inputTokens: measured.inputTokens, outputTokens: measured.outputTokens, totalTokens: measured.totalTokens, durationMs: measured.durationMs };
18590
19175
  }
18591
19176
  run = { id: row.id, status: row.status, proposal, usage, errorCode: row.errorCode, sourceDesignRevision: row.sourceDesignRevision, acceptedDesignRevision: row.acceptedDesignRevision, createdAt: row.createdAt, updatedAt: row.updatedAt };
@@ -18624,7 +19209,7 @@ async function acceptBrainProposal(client, input) {
18624
19209
  }
18625
19210
 
18626
19211
  // packages/cli/src/device-auth.ts
18627
- import { spawn as spawn2 } from "node:child_process";
19212
+ import { spawn as spawn3 } from "node:child_process";
18628
19213
  import { setTimeout as delay2 } from "node:timers/promises";
18629
19214
  var DEFAULT_API_URL = "https://api.editorbrain.com";
18630
19215
  var DeviceLoginError = class extends Error {
@@ -18642,12 +19227,12 @@ function browserCommand(url2, platform = process.platform) {
18642
19227
  }
18643
19228
  async function openSystemBrowser(url2) {
18644
19229
  const { command, args } = browserCommand(url2);
18645
- await new Promise((resolve8, reject) => {
18646
- const child = spawn2(command, args, { shell: false, stdio: "ignore", detached: true, windowsHide: true });
19230
+ await new Promise((resolve9, reject) => {
19231
+ const child = spawn3(command, args, { shell: false, stdio: "ignore", detached: true, windowsHide: true });
18647
19232
  child.once("error", reject);
18648
19233
  child.once("spawn", () => {
18649
19234
  child.unref();
18650
- resolve8();
19235
+ resolve9();
18651
19236
  });
18652
19237
  });
18653
19238
  }
@@ -18804,10 +19389,10 @@ async function loginWithDevice(options, overrides = {}) {
18804
19389
  }
18805
19390
 
18806
19391
  // packages/cli/src/index.ts
18807
- import { chmod, lstat as lstat10, mkdir as mkdir7, open as open9 } from "node:fs/promises";
19392
+ import { chmod as chmod2, lstat as lstat11, mkdir as mkdir8, open as open9 } from "node:fs/promises";
18808
19393
  import { constants as constants9 } from "node:fs";
18809
19394
  import { homedir } from "node:os";
18810
- import { basename as basename2, dirname as dirname8, extname as extname4, join as join4, resolve as resolve7 } from "node:path";
19395
+ import { basename as basename3, dirname as dirname11, extname as extname5, join as join8, resolve as resolve8 } from "node:path";
18811
19396
 
18812
19397
  // packages/cli/src/direction-context.ts
18813
19398
  async function prepareDirectionContext(root, client, projectId) {
@@ -18830,10 +19415,10 @@ async function prepareDirectionContext(root, client, projectId) {
18830
19415
  }
18831
19416
 
18832
19417
  // packages/cli/src/style-authoring.ts
18833
- import { createHash as createHash3 } from "node:crypto";
18834
- import { constants as constants5 } from "node:fs";
18835
- import { lstat as lstat4, mkdir as mkdir4, open as open5, readdir as readdir3, rmdir, unlink as unlink3 } from "node:fs/promises";
18836
- import { dirname as dirname4 } from "node:path";
19418
+ import { createHash as createHash5 } from "node:crypto";
19419
+ import { constants as constants6 } from "node:fs";
19420
+ import { lstat as lstat6, mkdir as mkdir4, open as open6, readdir as readdir4, rmdir, unlink as unlink3 } from "node:fs/promises";
19421
+ import { dirname as dirname7 } from "node:path";
18837
19422
  function styleId(id2) {
18838
19423
  try {
18839
19424
  return assertIdentifier(id2);
@@ -18843,7 +19428,7 @@ function styleId(id2) {
18843
19428
  }
18844
19429
  async function exists(path) {
18845
19430
  try {
18846
- await lstat4(path);
19431
+ await lstat6(path);
18847
19432
  return true;
18848
19433
  } catch (error) {
18849
19434
  if (error.code === "ENOENT") return false;
@@ -18873,11 +19458,11 @@ async function makeDirectories(root, path, created) {
18873
19458
  const target = await checkedPath(root, relative2, false);
18874
19459
  try {
18875
19460
  await mkdir4(target);
18876
- const info = await lstat4(target);
19461
+ const info = await lstat6(target);
18877
19462
  created.push({ path: relative2, dev: info.dev, ino: info.ino });
18878
19463
  } catch (error) {
18879
19464
  if (error.code !== "EEXIST") throw error;
18880
- if (!(await lstat4(await checkedPath(root, relative2, false))).isDirectory()) throw new Error(`Not a directory: ${relative2}`);
19465
+ if (!(await lstat6(await checkedPath(root, relative2, false))).isDirectory()) throw new Error(`Not a directory: ${relative2}`);
18881
19466
  }
18882
19467
  }
18883
19468
  }
@@ -18885,7 +19470,7 @@ async function cleanCreated(root, paths2, directory) {
18885
19470
  for (const item of [...paths2].reverse()) {
18886
19471
  try {
18887
19472
  const target = await checkedPath(root, item.path, false);
18888
- const info = await lstat4(target);
19473
+ const info = await lstat6(target);
18889
19474
  if (info.dev !== item.dev || info.ino !== item.ino || info.isSymbolicLink()) continue;
18890
19475
  if (directory) await rmdir(target);
18891
19476
  else await unlink3(target);
@@ -18907,9 +19492,9 @@ async function createLocalStyle(options) {
18907
19492
  const createdDirectories = [];
18908
19493
  try {
18909
19494
  for (const [path, content] of Object.entries(files)) {
18910
- await makeDirectories(root, dirname4(path), createdDirectories);
19495
+ await makeDirectories(root, dirname7(path), createdDirectories);
18911
19496
  const target = await checkedPath(root, path);
18912
- const handle = await open5(target, constants5.O_WRONLY | constants5.O_CREAT | constants5.O_EXCL | constants5.O_NOFOLLOW, 420);
19497
+ const handle = await open6(target, constants6.O_WRONLY | constants6.O_CREAT | constants6.O_EXCL | constants6.O_NOFOLLOW, 420);
18913
19498
  try {
18914
19499
  const info = await handle.stat();
18915
19500
  createdFiles.push({ path, dev: info.dev, ino: info.ino });
@@ -18942,321 +19527,124 @@ async function readStyle(root, id2) {
18942
19527
  try {
18943
19528
  const target = await checkedPath(root, path, false);
18944
19529
  if (!await exists(target)) continue;
18945
- if (!(await lstat4(target)).isDirectory()) {
19530
+ if (!(await lstat6(target)).isDirectory()) {
18946
19531
  issues.push({ path, message: "Style path is not a directory." });
18947
19532
  continue;
18948
19533
  }
18949
19534
  directories.push(path);
18950
- const entries = await readdir3(target, { withFileTypes: true });
19535
+ const entries = await readdir4(target, { withFileTypes: true });
18951
19536
  for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
18952
- if (!/\.(json|md)$/.test(entry.name)) continue;
18953
- const document2 = `${path}/${entry.name}`;
18954
- if (!entry.isFile()) {
18955
- issues.push({ path: document2, message: "Style document must be a regular file, not a symlink or directory." });
18956
- continue;
18957
- }
18958
- try {
18959
- files[document2] = await readText(root, document2);
18960
- } catch (error) {
18961
- issues.push({ path: document2, message: `Cannot safely read style document: ${error.message}` });
18962
- }
18963
- }
18964
- } catch (error) {
18965
- issues.push({ path, message: `Cannot safely read style directory: ${error.message}` });
18966
- }
18967
- }
18968
- if (directories.length > 1) issues.push({ path: `.editorbrain/styles/${id2}`, message: "Style exists in both current and legacy directories. Reconcile the duplicate before checking it." });
18969
- return { files, issues };
18970
- }
18971
- async function checkLocalResource(root, path, expected) {
18972
- safeStyleResourcePath(path);
18973
- const target = await checkedPath(root, path, false);
18974
- const handle = await open5(target, constants5.O_RDONLY | constants5.O_NOFOLLOW | constants5.O_NONBLOCK);
18975
- try {
18976
- const before = await handle.stat();
18977
- if (!before.isFile()) throw new Error("Resource is not a regular file.");
18978
- if (!expected) return {};
18979
- const hash4 = createHash3("sha256");
18980
- for await (const chunk of handle.createReadStream({ autoClose: false })) hash4.update(chunk);
18981
- const after = await handle.stat();
18982
- const current2 = await lstat4(await checkedPath(root, path, false));
18983
- if (before.size !== after.size || before.mtimeMs !== after.mtimeMs || before.ctimeMs !== after.ctimeMs || current2.dev !== before.dev || current2.ino !== before.ino) {
18984
- throw new Error("Resource changed while its hash was being checked; check it again.");
18985
- }
18986
- return { actualSha256: hash4.digest("hex") };
18987
- } finally {
18988
- await handle.close();
18989
- }
18990
- }
18991
- async function checkLocalStyle(options) {
18992
- const id2 = styleId(options.id);
18993
- const root = await ensureRoot(options.directory);
18994
- const local = await readStyle(root, id2);
18995
- const structural = checkStyle(local.files, id2);
18996
- const localIssues = [...local.issues];
18997
- const warnings = [...structural.warnings];
18998
- const resources = [];
18999
- const resourcePath = Object.keys(local.files).find((path) => path.endsWith("/resources.json"));
19000
- let document2;
19001
- if (resourcePath) {
19002
- try {
19003
- const parsed = JSON.parse(local.files[resourcePath]);
19004
- if (validateStyleResources(parsed, id2).valid) document2 = parsed;
19005
- } catch {
19006
- }
19007
- }
19008
- for (const resource of document2?.resources ?? []) {
19009
- const result2 = { id: resource.id, source: resource.source.type, required: resource.required, status: "unverified" };
19010
- resources.push(result2);
19011
- if (resource.source.type !== "local") {
19012
- warnings.push(`Resource ${resource.id}: ${resource.source.type === "asset" ? "remote asset availability and access" : "installed or external requirement"} not verified by this local check.`);
19013
- continue;
19014
- }
19015
- result2.path = resource.source.path;
19016
- result2.expectedSha256 = resource.source.sha256;
19017
- let message;
19018
- try {
19019
- const checked = await checkLocalResource(root, resource.source.path, resource.source.sha256);
19020
- result2.actualSha256 = checked.actualSha256;
19021
- result2.status = resource.source.sha256 ? "verified" : "present";
19022
- if (resource.source.sha256 && checked.actualSha256 !== resource.source.sha256) {
19023
- result2.status = "hash_mismatch";
19024
- message = "Resource SHA-256 does not match resources.json.";
19025
- } else if (!resource.source.sha256) warnings.push(`Resource ${resource.id}: local file is present, but its content hash is not pinned.`);
19026
- } catch (error) {
19027
- result2.status = error.code === "ENOENT" ? "missing" : "invalid";
19028
- message = result2.status === "missing" ? "Local resource is missing." : `Cannot safely verify resource: ${error.message}`;
19029
- }
19030
- if (message) {
19031
- const detail = `Resource ${resource.id}: ${message}`;
19032
- if (resource.required) localIssues.push({ path: resource.source.path, message: detail });
19033
- else warnings.push(`Optional ${detail}`);
19034
- }
19035
- }
19036
- const issues = [...structural.issues, ...localIssues];
19037
- return {
19038
- root,
19039
- id: id2,
19040
- ...structural,
19041
- valid: issues.length === 0,
19042
- issues,
19043
- warnings,
19044
- localIssues,
19045
- resources,
19046
- mediaUploaded: false,
19047
- quality: "not_assessed"
19048
- };
19049
- }
19050
-
19051
- // packages/cli/src/media-runtime.ts
19052
- import { spawn as spawn3 } from "node:child_process";
19053
- import { createHash as createHash4 } from "node:crypto";
19054
- import { constants as constants6 } from "node:fs";
19055
- import { lstat as lstat5, open as open6, realpath as realpath2 } from "node:fs/promises";
19056
- import { dirname as dirname5, extname, parse, resolve as resolve4 } from "node:path";
19057
- var MediaToolError = class extends CliError {
19058
- constructor(code, message) {
19059
- super(code, message, code === "MEDIA_TOOL_NOT_FOUND" ? "Install FFmpeg/FFprobe separately and make them available on PATH; no account or model is needed." : "Review the local input and reported limit. An incomplete check does not validate the media.");
19060
- this.name = "MediaToolError";
19061
- }
19062
- };
19063
- var SUPPORTED_MEDIA_EXTENSIONS = /* @__PURE__ */ new Set([
19064
- ".mp4",
19065
- ".mov",
19066
- ".m4v",
19067
- ".mkv",
19068
- ".webm",
19069
- ".avi",
19070
- ".mpg",
19071
- ".mpeg",
19072
- ".mts",
19073
- ".m2ts",
19074
- ".ts",
19075
- ".ogv",
19076
- ".wmv",
19077
- ".wav",
19078
- ".mp3",
19079
- ".m4a",
19080
- ".aac",
19081
- ".flac",
19082
- ".ogg",
19083
- ".opus",
19084
- ".aif",
19085
- ".aiff",
19086
- ".ac3",
19087
- ".eac3",
19088
- ".jpg",
19089
- ".jpeg",
19090
- ".png",
19091
- ".webp",
19092
- ".bmp",
19093
- ".tif",
19094
- ".tiff",
19095
- ".gif",
19096
- ".avif",
19097
- ".heic",
19098
- ".heif"
19099
- ]);
19100
- var PROTOCOLS = "file,pipe";
19101
- var FORMATS = "mov,matroska,webm,avi,mpeg,mpegts,ogg,wav,flac,mp3,aac,aiff,ac3,eac3,asf,jpeg_pipe,png_pipe,webp_pipe,bmp_pipe,tiff_pipe,gif,heif,avif";
19102
- var DEFAULT_TIMEOUT = 3e4;
19103
- var DEFAULT_OUTPUT_LIMIT = 2 * 1024 * 1024;
19104
- function resolveLocalMediaPath(value) {
19105
- if (!value || value.includes("\0") || /[\r\n]/.test(value) || /^[a-z][a-z\d+.-]*:/i.test(value) && !(process.platform === "win32" && /^[a-z]:[\\/]/i.test(value)) || value.startsWith("\\\\")) {
19106
- throw new MediaToolError("MEDIA_LOCAL_PATH_REQUIRED", "Provide a local filesystem path, not a URL, protocol or network share.");
19107
- }
19108
- return resolve4(value);
19109
- }
19110
- async function assertNoMediaSymlinks(value) {
19111
- const path = resolveLocalMediaPath(value);
19112
- let cursor = path;
19113
- while (cursor !== parse(cursor).root) {
19114
- const info = await lstat5(cursor);
19115
- if (info.isSymbolicLink()) {
19116
- const systemAlias = process.platform === "darwin" && (cursor === "/tmp" || cursor === "/var") && await realpath2(cursor) === `/private${cursor}`;
19117
- if (!systemAlias) throw new MediaToolError("MEDIA_SYMLINK", "Symlink media paths are not followed. Use the original local path.");
19118
- }
19119
- cursor = dirname5(cursor);
19120
- }
19121
- return path;
19122
- }
19123
- async function assertLocalMediaFile(value) {
19124
- const path = await assertNoMediaSymlinks(value);
19125
- const info = await lstat5(path);
19126
- if (!info.isFile()) throw new MediaToolError("MEDIA_NOT_REGULAR", "The media input must be a regular local file.");
19127
- if (!SUPPORTED_MEDIA_EXTENSIONS.has(extname(path).toLowerCase())) throw new MediaToolError("MEDIA_FORMAT_UNSUPPORTED", "This file extension is not supported for local media inspection.");
19128
- return path;
19129
- }
19130
- function localInputArgs(path) {
19131
- return ["-protocol_whitelist", PROTOCOLS, "-format_whitelist", FORMATS, "-i", resolveLocalMediaPath(path)];
19132
- }
19133
- async function runMediaTool(tool, args, options = {}) {
19134
- if (tool !== "ffmpeg" && tool !== "ffprobe") throw new MediaToolError("MEDIA_TOOL_INVALID", "Unsupported media executable.");
19135
- const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT;
19136
- const maxOutputBytes = options.maxOutputBytes ?? DEFAULT_OUTPUT_LIMIT;
19137
- if (!Number.isInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 10 * 6e4 || !Number.isInteger(maxOutputBytes) || maxOutputBytes < 1 || maxOutputBytes > 16 * 1024 * 1024) {
19138
- throw new MediaToolError("MEDIA_TOOL_LIMITS_INVALID", "Media runtime limits are outside the supported range.");
19139
- }
19140
- if (!Array.isArray(args) || args.length > 200 || args.some((arg) => typeof arg !== "string" || arg.length > 8192 || arg.includes("\0"))) {
19141
- throw new MediaToolError("MEDIA_TOOL_ARGUMENTS_INVALID", "Invalid media tool arguments.");
19142
- }
19143
- for (let i = 0; i < args.length; i++) {
19144
- const arg = args[i];
19145
- if (/(?:^|[=,;\s])(?:https?|ftp|sftp|tcp|udp|rtsp|rtmp|smb|data|concat|subfile|crypto):/i.test(arg) || arg === "-safe" && args[i + 1] === "0" || arg === "-enable_drefs" && args[i + 1] !== "0") {
19146
- throw new MediaToolError("MEDIA_REMOTE_INPUT_FORBIDDEN", "Only self-contained local media inputs are allowed.");
19147
- }
19148
- if (arg === "-protocol_whitelist" && args[i + 1] !== PROTOCOLS || arg === "-format_whitelist" && args[i + 1] !== FORMATS) {
19149
- throw new MediaToolError("MEDIA_TOOL_ARGUMENTS_INVALID", "The local media allowlists cannot be widened.");
19150
- }
19151
- if (arg === "-i") {
19152
- await assertLocalMediaFile(args[i + 1] ?? "");
19153
- if (args[i - 4] !== "-protocol_whitelist" || args[i - 3] !== PROTOCOLS || args[i - 2] !== "-format_whitelist" || args[i - 1] !== FORMATS) {
19154
- throw new MediaToolError("MEDIA_TOOL_ARGUMENTS_INVALID", "Every media input must use localInputArgs.");
19537
+ if (!/\.(json|md)$/.test(entry.name)) continue;
19538
+ const document2 = `${path}/${entry.name}`;
19539
+ if (!entry.isFile()) {
19540
+ issues.push({ path: document2, message: "Style document must be a regular file, not a symlink or directory." });
19541
+ continue;
19542
+ }
19543
+ try {
19544
+ files[document2] = await readText(root, document2);
19545
+ } catch (error) {
19546
+ issues.push({ path: document2, message: `Cannot safely read style document: ${error.message}` });
19547
+ }
19155
19548
  }
19549
+ } catch (error) {
19550
+ issues.push({ path, message: `Cannot safely read style directory: ${error.message}` });
19156
19551
  }
19157
19552
  }
19158
- return new Promise((resolveResult, reject) => {
19159
- const stdout = [], stderr = [];
19160
- let size = 0, done = false;
19161
- const child = spawn3(tool, ["-hide_banner", ...tool === "ffmpeg" ? ["-nostdin"] : [], ...args], { shell: false, stdio: ["ignore", "pipe", "pipe"], windowsHide: true });
19162
- const fail2 = (error) => {
19163
- if (done) return;
19164
- done = true;
19165
- clearTimeout(timer);
19166
- child.kill("SIGKILL");
19167
- reject(error);
19168
- };
19169
- const timer = setTimeout(() => fail2(new MediaToolError("MEDIA_TOOL_TIMEOUT", "The local media check exceeded its time limit. No complete result is available.")), timeoutMs);
19170
- const collect = (target) => (chunk) => {
19171
- if (done) return;
19172
- size += chunk.length;
19173
- if (size > maxOutputBytes) return fail2(new MediaToolError("MEDIA_TOOL_OUTPUT_LIMIT", "The local media check exceeded its output limit. No complete result is available."));
19174
- target.push(chunk);
19175
- };
19176
- child.stdout.on("data", collect(stdout));
19177
- child.stderr.on("data", collect(stderr));
19178
- child.on("error", (error) => fail2(new MediaToolError(error.code === "ENOENT" ? "MEDIA_TOOL_NOT_FOUND" : "MEDIA_TOOL_FAILED", error.code === "ENOENT" ? `${tool} is not installed or available on PATH. Install it separately to use local media checks.` : "The local media executable could not run.")));
19179
- child.on("close", (code) => {
19180
- if (done) return;
19181
- done = true;
19182
- clearTimeout(timer);
19183
- if (code !== 0) return reject(new MediaToolError("MEDIA_TOOL_FAILED", "The local media tool could not inspect or decode this input. No complete result is available."));
19184
- resolveResult({ stdout: Buffer.concat(stdout).toString("utf8"), stderr: Buffer.concat(stderr).toString("utf8") });
19185
- });
19186
- });
19553
+ if (directories.length > 1) issues.push({ path: `.editorbrain/styles/${id2}`, message: "Style exists in both current and legacy directories. Reconcile the duplicate before checking it." });
19554
+ return { files, issues };
19187
19555
  }
19188
- var finite2 = (value) => {
19189
- if (typeof value !== "number" && typeof value !== "string" || value === "") return null;
19190
- const n = Number(value);
19191
- return Number.isFinite(n) ? n : null;
19192
- };
19193
- var nonnegative2 = (value) => {
19194
- const n = finite2(value);
19195
- return n !== null && n >= 0 ? n : null;
19196
- };
19197
- var positiveInteger = (value) => {
19198
- const n = finite2(value);
19199
- return n !== null && Number.isInteger(n) && n > 0 ? n : void 0;
19200
- };
19201
- var rate2 = (value) => {
19202
- if (typeof value !== "string" || !/^\d+(?:\/\d+)?$/.test(value)) return null;
19203
- const [n, d = "1"] = value.split("/");
19204
- const result2 = Number(n) / Number(d);
19205
- return Number.isFinite(result2) && result2 > 0 ? result2 : null;
19206
- };
19207
- var sameFile = (a, b) => a.dev === b.dev && a.ino === b.ino && a.size === b.size && a.mtimeMs === b.mtimeMs && a.ctimeMs === b.ctimeMs;
19208
- async function probeLocalMedia(value) {
19209
- const path = await assertLocalMediaFile(value);
19210
- const handle = await open6(path, constants6.O_RDONLY | (constants6.O_NOFOLLOW ?? 0));
19556
+ async function checkLocalResource(root, path, expected) {
19557
+ safeStyleResourcePath(path);
19558
+ const target = await checkedPath(root, path, false);
19559
+ const handle = await open6(target, constants6.O_RDONLY | constants6.O_NOFOLLOW | constants6.O_NONBLOCK);
19211
19560
  try {
19212
19561
  const before = await handle.stat();
19213
- if (!before.isFile()) throw new MediaToolError("MEDIA_NOT_REGULAR", "The media input must be a regular local file.");
19214
- const entries = "format=duration,start_time:stream=index,codec_type,codec_name,width,height,avg_frame_rate,sample_rate,channels,duration:stream_disposition=attached_pic";
19215
- const { stdout } = await runMediaTool("ffprobe", ["-v", "error", "-show_entries", entries, "-of", "json", ...localInputArgs(path)]);
19216
- const hash4 = createHash4("sha256");
19217
- for await (const chunk of handle.createReadStream({ autoClose: false })) hash4.update(chunk);
19562
+ if (!before.isFile()) throw new Error("Resource is not a regular file.");
19563
+ if (!expected) return {};
19564
+ const hash6 = createHash5("sha256");
19565
+ for await (const chunk of handle.createReadStream({ autoClose: false })) hash6.update(chunk);
19218
19566
  const after = await handle.stat();
19219
- await assertNoMediaSymlinks(path);
19220
- const current2 = await lstat5(path);
19221
- if (!sameFile(before, after) || !sameFile(after, current2)) throw new MediaToolError("MEDIA_FILE_CHANGED", "The media file changed while it was being inspected. Retry with a stable source.");
19222
- let raw;
19223
- try {
19224
- raw = JSON.parse(stdout);
19225
- } catch {
19226
- throw new MediaToolError("MEDIA_PROBE_INVALID", "The media probe returned invalid metadata.");
19567
+ const current2 = await lstat6(await checkedPath(root, path, false));
19568
+ if (before.size !== after.size || before.mtimeMs !== after.mtimeMs || before.ctimeMs !== after.ctimeMs || current2.dev !== before.dev || current2.ino !== before.ino) {
19569
+ throw new Error("Resource changed while its hash was being checked; check it again.");
19227
19570
  }
19228
- if (!raw || !Array.isArray(raw.streams) || raw.streams.length > 256 || raw.streams.some((stream) => !stream || typeof stream !== "object" || Array.isArray(stream))) throw new MediaToolError("MEDIA_PROBE_INVALID", "The media probe returned invalid or excessive stream metadata.");
19229
- const streams = raw.streams.map((stream, offset) => {
19230
- const type = typeof stream.codec_type === "string" && /^(video|audio|subtitle|data|attachment|unknown)$/.test(stream.codec_type) ? stream.codec_type : "unknown";
19231
- const codec = typeof stream.codec_name === "string" && /^[a-zA-Z0-9_]{1,64}$/.test(stream.codec_name) ? stream.codec_name : null;
19232
- const index = finite2(stream.index);
19233
- return {
19234
- index: index !== null && Number.isInteger(index) && index >= 0 ? index : offset,
19235
- type,
19236
- codec,
19237
- durationSeconds: nonnegative2(stream.duration),
19238
- ...type === "video" ? { width: positiveInteger(stream.width), height: positiveInteger(stream.height), fps: rate2(stream.avg_frame_rate), attachedPicture: stream.disposition?.attached_pic === 1 } : {},
19239
- ...type === "audio" ? { sampleRate: nonnegative2(stream.sample_rate), channels: positiveInteger(stream.channels) ?? null } : {}
19240
- };
19241
- });
19242
- return { path, sha256: hash4.digest("hex"), bytes: after.size, durationSeconds: nonnegative2(raw.format?.duration), startTimeSeconds: finite2(raw.format?.start_time), streams };
19571
+ return { actualSha256: hash6.digest("hex") };
19243
19572
  } finally {
19244
19573
  await handle.close();
19245
19574
  }
19246
19575
  }
19576
+ async function checkLocalStyle(options) {
19577
+ const id2 = styleId(options.id);
19578
+ const root = await ensureRoot(options.directory);
19579
+ const local = await readStyle(root, id2);
19580
+ const structural = checkStyle(local.files, id2);
19581
+ const localIssues = [...local.issues];
19582
+ const warnings = [...structural.warnings];
19583
+ const resources = [];
19584
+ const resourcePath = Object.keys(local.files).find((path) => path.endsWith("/resources.json"));
19585
+ let document2;
19586
+ if (resourcePath) {
19587
+ try {
19588
+ const parsed = JSON.parse(local.files[resourcePath]);
19589
+ if (validateStyleResources(parsed, id2).valid) document2 = parsed;
19590
+ } catch {
19591
+ }
19592
+ }
19593
+ for (const resource of document2?.resources ?? []) {
19594
+ const result2 = { id: resource.id, source: resource.source.type, required: resource.required, status: "unverified" };
19595
+ resources.push(result2);
19596
+ if (resource.source.type !== "local") {
19597
+ warnings.push(`Resource ${resource.id}: ${resource.source.type === "asset" ? "remote asset availability and access" : "installed or external requirement"} not verified by this local check.`);
19598
+ continue;
19599
+ }
19600
+ result2.path = resource.source.path;
19601
+ result2.expectedSha256 = resource.source.sha256;
19602
+ let message;
19603
+ try {
19604
+ const checked = await checkLocalResource(root, resource.source.path, resource.source.sha256);
19605
+ result2.actualSha256 = checked.actualSha256;
19606
+ result2.status = resource.source.sha256 ? "verified" : "present";
19607
+ if (resource.source.sha256 && checked.actualSha256 !== resource.source.sha256) {
19608
+ result2.status = "hash_mismatch";
19609
+ message = "Resource SHA-256 does not match resources.json.";
19610
+ } else if (!resource.source.sha256) warnings.push(`Resource ${resource.id}: local file is present, but its content hash is not pinned.`);
19611
+ } catch (error) {
19612
+ result2.status = error.code === "ENOENT" ? "missing" : "invalid";
19613
+ message = result2.status === "missing" ? "Local resource is missing." : `Cannot safely verify resource: ${error.message}`;
19614
+ }
19615
+ if (message) {
19616
+ const detail = `Resource ${resource.id}: ${message}`;
19617
+ if (resource.required) localIssues.push({ path: resource.source.path, message: detail });
19618
+ else warnings.push(`Optional ${detail}`);
19619
+ }
19620
+ }
19621
+ const issues = [...structural.issues, ...localIssues];
19622
+ return {
19623
+ root,
19624
+ id: id2,
19625
+ ...structural,
19626
+ valid: issues.length === 0,
19627
+ issues,
19628
+ warnings,
19629
+ localIssues,
19630
+ resources,
19631
+ mediaUploaded: false,
19632
+ quality: "not_assessed"
19633
+ };
19634
+ }
19247
19635
 
19248
19636
  // packages/cli/src/audio-check.ts
19249
19637
  function invalid3() {
19250
19638
  throw new CliError("AUDIO_PROFILE_INVALID", "Invalid audio check profile.", "Use explicit numeric loudness targets and bounded runtime limits.");
19251
19639
  }
19252
- function object6(value, keys2) {
19640
+ function object7(value, keys2) {
19253
19641
  if (!value || typeof value !== "object" || Array.isArray(value)) invalid3();
19254
19642
  const result2 = value;
19255
19643
  if (Object.keys(result2).some((key) => !keys2.includes(key))) invalid3();
19256
19644
  return result2;
19257
19645
  }
19258
19646
  function validateAudioCheckProfile(value) {
19259
- const input = object6(value, ["schemaVersion", "loudness", "limits"]);
19647
+ const input = object7(value, ["schemaVersion", "loudness", "limits"]);
19260
19648
  if (input.schemaVersion !== 1) invalid3();
19261
19649
  let loudness;
19262
19650
  try {
@@ -19264,7 +19652,7 @@ function validateAudioCheckProfile(value) {
19264
19652
  } catch {
19265
19653
  invalid3();
19266
19654
  }
19267
- const limits = input.limits === void 0 ? {} : object6(input.limits, ["timeoutMs", "maxOutputBytes"]);
19655
+ const limits = input.limits === void 0 ? {} : object7(input.limits, ["timeoutMs", "maxOutputBytes"]);
19268
19656
  const timeoutMs = limits.timeoutMs === void 0 ? 6e5 : limits.timeoutMs;
19269
19657
  const maxOutputBytes = limits.maxOutputBytes === void 0 ? 4 * 1024 * 1024 : limits.maxOutputBytes;
19270
19658
  if (typeof timeoutMs !== "number" || !Number.isInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 6e5 || typeof maxOutputBytes !== "number" || !Number.isInteger(maxOutputBytes) || maxOutputBytes < 1024 || maxOutputBytes > 16 * 1024 * 1024) invalid3();
@@ -19396,11 +19784,11 @@ var record10 = (value) => value !== null && typeof value === "object" && !Array.
19396
19784
  var array2 = (value) => Array.isArray(value) ? value : [];
19397
19785
  function plain(value, max = 2e3) {
19398
19786
  if (typeof value !== "string" || value.length > 16e3) return null;
19399
- const text7 = value.replace(/<[^>]*>/g, " ").replace(/&#(x[0-9a-f]+|[0-9]+);/gi, (_, number2) => {
19787
+ const text9 = value.replace(/<[^>]*>/g, " ").replace(/&#(x[0-9a-f]+|[0-9]+);/gi, (_, number2) => {
19400
19788
  const code = number2[0].toLowerCase() === "x" ? parseInt(number2.slice(1), 16) : Number(number2);
19401
19789
  return code > 0 && code <= 1114111 ? String.fromCodePoint(code) : "";
19402
19790
  }).replace(/&(amp|quot|apos|lt|gt|nbsp);/g, (_, entity) => ({ amp: "&", quot: '"', apos: "'", lt: "<", gt: ">", nbsp: " " })[entity]).replace(/[\u0000-\u001f\u007f-\u009f\u202a-\u202e\u2066-\u2069]/g, " ").replace(/\s+/g, " ").trim();
19403
- return text7 ? text7.slice(0, max) : null;
19791
+ return text9 ? text9.slice(0, max) : null;
19404
19792
  }
19405
19793
  function texts(value) {
19406
19794
  return (Array.isArray(value) ? value.slice(0, 8) : [value]).map((item) => plain(item)).filter((item) => item !== null);
@@ -19599,7 +19987,7 @@ async function searchProvider(provider, query, limit, client, report) {
19599
19987
  item.author = firstText(metadata.photographer, metadata.secondary_creator);
19600
19988
  item.credit = firstText(metadata.center);
19601
19989
  item.dates.created = firstText(metadata.date_created);
19602
- item.previewUrl = array2(entry.links).map(record10).filter((link3) => link3.rel === "preview").map((link3) => safeUrl(link3.href, HOSTS[provider])).find(Boolean) ?? null;
19990
+ item.previewUrl = array2(entry.links).map(record10).filter((link5) => link5.rel === "preview").map((link5) => safeUrl(link5.href, HOSTS[provider])).find(Boolean) ?? null;
19603
19991
  }
19604
19992
  add(item);
19605
19993
  }
@@ -19720,9 +20108,9 @@ async function searchMediaSources(options, dependencies = {}) {
19720
20108
  }
19721
20109
 
19722
20110
  // packages/cli/src/media-inventory.ts
19723
- import { createHash as createHash5 } from "node:crypto";
19724
- import { link as link2, lstat as lstat6, mkdir as mkdir5, mkdtemp, opendir, readFile as readFile2, rm } from "node:fs/promises";
19725
- import { basename, dirname as dirname6, extname as extname2, join as join3 } from "node:path";
20111
+ import { createHash as createHash6 } from "node:crypto";
20112
+ import { link as link4, lstat as lstat7, mkdir as mkdir5, mkdtemp as mkdtemp3, opendir, readFile as readFile3, rm as rm3 } from "node:fs/promises";
20113
+ import { basename as basename2, dirname as dirname8, extname as extname3, join as join6 } from "node:path";
19726
20114
  var GENERATED_DIRECTORIES = /* @__PURE__ */ new Set(["node_modules", "dist", "build", "out", "output", "outputs", "renders", "render", "cache", "caches", "proxies", "proxy", "samples", "thumbnails", "coverage", "venv", "__pycache__"]);
19727
20115
  function mediaFailure(error) {
19728
20116
  if (error instanceof MediaToolError) return { code: error.code, message: error.message };
@@ -19748,19 +20136,19 @@ async function inventoryMedia(options) {
19748
20136
  }
19749
20137
  report.scannedEntries++;
19750
20138
  try {
19751
- const stat = await lstat6(path);
20139
+ const stat = await lstat7(path);
19752
20140
  if (stat.isSymbolicLink()) {
19753
20141
  if (explicit) throw new MediaToolError("MEDIA_SYMLINK", "Symlink inputs are not followed. Use the original local path.");
19754
20142
  skip(path, "symlink_not_followed");
19755
20143
  return;
19756
20144
  }
19757
- if (!explicit && basename(path).startsWith(".")) {
20145
+ if (!explicit && basename2(path).startsWith(".")) {
19758
20146
  skip(path, "hidden_entry");
19759
20147
  return;
19760
20148
  }
19761
20149
  await assertNoMediaSymlinks(path);
19762
20150
  if (stat.isDirectory()) {
19763
- if (!explicit && GENERATED_DIRECTORIES.has(basename(path).toLowerCase())) {
20151
+ if (!explicit && GENERATED_DIRECTORIES.has(basename2(path).toLowerCase())) {
19764
20152
  skip(path, "generated_directory");
19765
20153
  return;
19766
20154
  }
@@ -19774,12 +20162,12 @@ async function inventoryMedia(options) {
19774
20162
  skip(path, "remaining_entries_not_scanned", true);
19775
20163
  break;
19776
20164
  }
19777
- await visit(join3(path, entry.name), depth + 1);
20165
+ await visit(join6(path, entry.name), depth + 1);
19778
20166
  }
19779
20167
  } else if (!stat.isFile()) {
19780
20168
  if (explicit) throw new MediaToolError("MEDIA_NOT_REGULAR", "The explicit media input must be a regular local file or directory.");
19781
20169
  skip(path, "not_regular_file");
19782
- } else if (!SUPPORTED_MEDIA_EXTENSIONS.has(extname2(path).toLowerCase())) {
20170
+ } else if (!SUPPORTED_MEDIA_EXTENSIONS.has(extname3(path).toLowerCase())) {
19783
20171
  if (explicit) throw new MediaToolError("MEDIA_FORMAT_UNSUPPORTED", "This explicit input is not a supported media format.");
19784
20172
  skip(path, "unsupported_extension");
19785
20173
  } else if (candidates.length >= maxFiles) skip(path, "file_limit_reached", true);
@@ -19809,35 +20197,35 @@ async function sampleMedia(options) {
19809
20197
  if (!source.streams.some((stream) => stream.type === "video")) throw new MediaToolError("MEDIA_VIDEO_REQUIRED", "Frame sampling requires a video or image stream.");
19810
20198
  if (source.durationSeconds !== null && options.times.some((time) => time >= source.durationSeconds)) throw new MediaToolError("MEDIA_SAMPLE_OUT_OF_RANGE", "A requested sample is outside the known media duration.");
19811
20199
  const outputDirectory = resolveLocalMediaPath(options.outputDirectory);
19812
- await assertNoMediaSymlinks(dirname6(outputDirectory));
20200
+ await assertNoMediaSymlinks(dirname8(outputDirectory));
19813
20201
  try {
19814
20202
  await mkdir5(outputDirectory);
19815
20203
  } catch (error) {
19816
20204
  if (error.code !== "EEXIST") throw error;
19817
20205
  }
19818
20206
  await assertNoMediaSymlinks(outputDirectory);
19819
- if (!(await lstat6(outputDirectory)).isDirectory()) throw new MediaToolError("MEDIA_OUTPUT_DIRECTORY_INVALID", "Frame output must be a local directory.");
19820
- const temp = await mkdtemp(join3(outputDirectory, ".editorbrain-samples-"));
19821
- const tempStat = await lstat6(temp);
20207
+ if (!(await lstat7(outputDirectory)).isDirectory()) throw new MediaToolError("MEDIA_OUTPUT_DIRECTORY_INVALID", "Frame output must be a local directory.");
20208
+ const temp = await mkdtemp3(join6(outputDirectory, ".editorbrain-samples-"));
20209
+ const tempStat = await lstat7(temp);
19822
20210
  const frames = [];
19823
20211
  const created = [];
19824
20212
  try {
19825
20213
  for (let index = 0; index < options.times.length; index++) {
19826
20214
  const time = options.times[index];
19827
20215
  const filename = `${source.sha256.slice(0, 12)}-${String(index + 1).padStart(2, "0")}-${Math.round(time * 1e3)}ms.jpg`;
19828
- const temporary = join3(temp, filename), output2 = join3(outputDirectory, filename);
20216
+ const temporary = join6(temp, filename), output2 = join6(outputDirectory, filename);
19829
20217
  await runMediaTool("ffmpeg", ["-v", "error", "-n", "-ss", String(time), ...localInputArgs(file), "-map", "0:v:0", "-frames:v", "1", "-an", "-sn", "-dn", "-vf", "scale=960:960:force_original_aspect_ratio=decrease", "-q:v", "3", "-update", "1", temporary], { timeoutMs: 6e4 });
19830
- const bytes = await readFile2(temporary);
20218
+ const bytes = await readFile3(temporary);
19831
20219
  if (!bytes.length) throw new MediaToolError("MEDIA_SAMPLE_EMPTY", "The requested frame did not produce a usable image.");
19832
20220
  try {
19833
- await link2(temporary, output2);
20221
+ await link4(temporary, output2);
19834
20222
  } catch (error) {
19835
20223
  if (error.code === "EEXIST") throw new MediaToolError("MEDIA_OUTPUT_EXISTS", "A sample output already exists. Choose a new output directory; existing files are never overwritten.");
19836
20224
  throw error;
19837
20225
  }
19838
- const outputStat = await lstat6(output2);
20226
+ const outputStat = await lstat7(output2);
19839
20227
  created.push({ path: output2, ino: outputStat.ino, dev: outputStat.dev, size: outputStat.size, mtimeMs: outputStat.mtimeMs, ctimeMs: outputStat.ctimeMs });
19840
- frames.push({ path: output2, sha256: createHash5("sha256").update(bytes).digest("hex"), bytes: bytes.length, requestedTimeSeconds: time, actualPresentationTimeSeconds: null });
20228
+ frames.push({ path: output2, sha256: createHash6("sha256").update(bytes).digest("hex"), bytes: bytes.length, requestedTimeSeconds: time, actualPresentationTimeSeconds: null });
19841
20229
  }
19842
20230
  const after = await probeLocalMedia(file);
19843
20231
  if (source.sha256 !== after.sha256) throw new MediaToolError("MEDIA_FILE_CHANGED", "The source changed while frames were being sampled. Retry with a stable source.");
@@ -19846,8 +20234,8 @@ async function sampleMedia(options) {
19846
20234
  for (const file2 of created) {
19847
20235
  try {
19848
20236
  await assertNoMediaSymlinks(file2.path);
19849
- const current2 = await lstat6(file2.path);
19850
- if (current2.ino === file2.ino && current2.dev === file2.dev && current2.size === file2.size && current2.mtimeMs === file2.mtimeMs && current2.ctimeMs === file2.ctimeMs) await rm(file2.path, { force: true });
20237
+ const current2 = await lstat7(file2.path);
20238
+ if (current2.ino === file2.ino && current2.dev === file2.dev && current2.size === file2.size && current2.mtimeMs === file2.mtimeMs && current2.ctimeMs === file2.ctimeMs) await rm3(file2.path, { force: true });
19851
20239
  } catch {
19852
20240
  }
19853
20241
  }
@@ -19855,8 +20243,8 @@ async function sampleMedia(options) {
19855
20243
  } finally {
19856
20244
  try {
19857
20245
  await assertNoMediaSymlinks(temp);
19858
- const current2 = await lstat6(temp);
19859
- if (current2.ino === tempStat.ino && current2.dev === tempStat.dev) await rm(temp, { recursive: true, force: true });
20246
+ const current2 = await lstat7(temp);
20247
+ if (current2.ino === tempStat.ino && current2.dev === tempStat.dev) await rm3(temp, { recursive: true, force: true });
19860
20248
  } catch {
19861
20249
  }
19862
20250
  }
@@ -19867,21 +20255,21 @@ import { resolve as resolve5 } from "node:path";
19867
20255
  function invalidProfile(message) {
19868
20256
  throw new CliError("RENDER_PROFILE_INVALID", message, "Use a schemaVersion: 1 render profile with explicit technical requirements.");
19869
20257
  }
19870
- function object7(value, path, keys2) {
20258
+ function object8(value, path, keys2) {
19871
20259
  if (!value || typeof value !== "object" || Array.isArray(value)) invalidProfile(`${path} must be an object.`);
19872
20260
  const result2 = value;
19873
20261
  for (const key of Object.keys(result2)) if (!keys2.includes(key)) invalidProfile(`${path} contains unsupported field ${key}.`);
19874
20262
  return result2;
19875
20263
  }
19876
20264
  function validateRenderCheckProfile(value) {
19877
- const root = object7(value, "profile", ["schemaVersion", "expected", "tolerance", "audio", "silence", "black", "peak", "limits"]);
20265
+ const root = object8(value, "profile", ["schemaVersion", "expected", "tolerance", "audio", "silence", "black", "peak", "limits"]);
19878
20266
  if (root.schemaVersion !== 1) invalidProfile("profile.schemaVersion must be 1.");
19879
- const expected = root.expected === void 0 ? {} : object7(root.expected, "expected", ["width", "height", "fps", "durationSeconds"]);
19880
- const tolerance = root.tolerance === void 0 ? {} : object7(root.tolerance, "tolerance", ["fps", "durationSeconds"]);
19881
- const silence = root.silence === void 0 ? {} : object7(root.silence, "silence", ["enabled", "minDurationSeconds", "thresholdDb"]);
19882
- const black = root.black === void 0 ? {} : object7(root.black, "black", ["enabled", "minDurationSeconds", "pixelThreshold", "pictureBlackRatio"]);
19883
- const peak = root.peak === void 0 ? {} : object7(root.peak, "peak", ["enabled", "thresholdDbFS"]);
19884
- const limits = root.limits === void 0 ? {} : object7(root.limits, "limits", ["timeoutMs", "maxOutputBytes"]);
20267
+ const expected = root.expected === void 0 ? {} : object8(root.expected, "expected", ["width", "height", "fps", "durationSeconds"]);
20268
+ const tolerance = root.tolerance === void 0 ? {} : object8(root.tolerance, "tolerance", ["fps", "durationSeconds"]);
20269
+ const silence = root.silence === void 0 ? {} : object8(root.silence, "silence", ["enabled", "minDurationSeconds", "thresholdDb"]);
20270
+ const black = root.black === void 0 ? {} : object8(root.black, "black", ["enabled", "minDurationSeconds", "pixelThreshold", "pictureBlackRatio"]);
20271
+ const peak = root.peak === void 0 ? {} : object8(root.peak, "peak", ["enabled", "thresholdDbFS"]);
20272
+ const limits = root.limits === void 0 ? {} : object8(root.limits, "limits", ["timeoutMs", "maxOutputBytes"]);
19885
20273
  const number2 = (v, name, min, max, fallback, integer2 = false) => {
19886
20274
  if (v === void 0 && fallback !== void 0) return fallback;
19887
20275
  if (typeof v !== "number" || !Number.isFinite(v) || v < min || v > max || integer2 && !Number.isInteger(v)) invalidProfile(`${name} must be ${integer2 ? "an integer" : "a finite number"} between ${min} and ${max}.`);
@@ -20128,8 +20516,8 @@ async function checkRender(options) {
20128
20516
 
20129
20517
  // packages/cli/src/integrations.ts
20130
20518
  import { constants as constants7 } from "node:fs";
20131
- import { lstat as lstat7, open as open7 } from "node:fs/promises";
20132
- function object8(value) {
20519
+ import { lstat as lstat8, open as open7 } from "node:fs/promises";
20520
+ function object9(value) {
20133
20521
  return value !== null && typeof value === "object" && !Array.isArray(value);
20134
20522
  }
20135
20523
  function invalidState(write) {
@@ -20142,13 +20530,13 @@ function invalidState(write) {
20142
20530
  );
20143
20531
  }
20144
20532
  function integrationState(value, write = false) {
20145
- if (!object8(value) || !["member", "connection"].includes(value.scope) || !Number.isSafeInteger(value.revision) || value.revision < 0 || !(value.updatedAt === null || typeof value.updatedAt === "string" && value.updatedAt.length <= 64 && Number.isFinite(Date.parse(value.updatedAt))) || !Array.isArray(value.entries) || !Array.isArray(value.catalog)) throw invalidState(write);
20533
+ if (!object9(value) || !["member", "connection"].includes(value.scope) || !Number.isSafeInteger(value.revision) || value.revision < 0 || !(value.updatedAt === null || typeof value.updatedAt === "string" && value.updatedAt.length <= 64 && Number.isFinite(Date.parse(value.updatedAt))) || !Array.isArray(value.entries) || !Array.isArray(value.catalog)) throw invalidState(write);
20146
20534
  const providers = /* @__PURE__ */ new Set();
20147
20535
  for (const entry of value.entries) {
20148
- if (!object8(entry) || typeof entry.providerId !== "string" || !entry.providerId || entry.providerId.length > 120 || providers.has(entry.providerId) || typeof entry.enabled !== "boolean" || typeof entry.accessMethod !== "string" || !entry.accessMethod || entry.accessMethod.length > 64 || !object8(entry.preferences)) throw invalidState(write);
20536
+ if (!object9(entry) || typeof entry.providerId !== "string" || !entry.providerId || entry.providerId.length > 120 || providers.has(entry.providerId) || typeof entry.enabled !== "boolean" || typeof entry.accessMethod !== "string" || !entry.accessMethod || entry.accessMethod.length > 64 || !object9(entry.preferences)) throw invalidState(write);
20149
20537
  providers.add(entry.providerId);
20150
20538
  }
20151
- if (value.catalog.some((provider) => !object8(provider) || typeof provider.id !== "string" || !provider.id)) throw invalidState(write);
20539
+ if (value.catalog.some((provider) => !object9(provider) || typeof provider.id !== "string" || !provider.id)) throw invalidState(write);
20152
20540
  return {
20153
20541
  scope: value.scope,
20154
20542
  revision: value.revision,
@@ -20161,13 +20549,13 @@ async function readIntegrationSettings(file) {
20161
20549
  const invalidFile = () => new CliError("INVALID_ARGUMENT", "Choose a regular integration settings JSON file under 16 KiB.", "Store provider credentials in the provider connection, not this file.");
20162
20550
  const changedFile = () => new CliError("INVALID_ARGUMENT", "Integration settings changed while being read.", "Choose a stable regular settings file and try again.");
20163
20551
  const limit = 16 * 1024;
20164
- const before = await lstat7(file);
20552
+ const before = await lstat8(file);
20165
20553
  if (!before.isFile() || before.isSymbolicLink() || before.size > limit) throw invalidFile();
20166
20554
  const unchanged2 = (info) => info.isFile() && !info.isSymbolicLink() && info.dev === before.dev && info.ino === before.ino && info.size === before.size && info.mtimeMs === before.mtimeMs && info.ctimeMs === before.ctimeMs;
20167
20555
  const handle = await open7(file, constants7.O_RDONLY | constants7.O_NOFOLLOW | constants7.O_NONBLOCK);
20168
20556
  try {
20169
20557
  const opened = await handle.stat();
20170
- const current2 = await lstat7(file);
20558
+ const current2 = await lstat8(file);
20171
20559
  if (!unchanged2(opened) || !unchanged2(current2)) throw changedFile();
20172
20560
  const bytes = Buffer.alloc(limit + 1);
20173
20561
  let length = 0;
@@ -20177,7 +20565,7 @@ async function readIntegrationSettings(file) {
20177
20565
  length += bytesRead;
20178
20566
  }
20179
20567
  const after = await handle.stat();
20180
- const finalPath = await lstat7(file);
20568
+ const finalPath = await lstat8(file);
20181
20569
  if (length > limit || length !== opened.size || !unchanged2(after) || !unchanged2(finalPath)) throw changedFile();
20182
20570
  try {
20183
20571
  return JSON.parse(bytes.subarray(0, length).toString("utf8"));
@@ -20235,10 +20623,10 @@ async function setIntegration(client, providerId, settings, expectedRevision) {
20235
20623
  }
20236
20624
 
20237
20625
  // packages/cli/src/motion-components.ts
20238
- import { createHash as createHash6 } from "node:crypto";
20626
+ import { createHash as createHash7 } from "node:crypto";
20239
20627
  import { constants as constants8 } from "node:fs";
20240
- import { lstat as lstat8, mkdir as mkdir6, open as open8, rmdir as rmdir2, unlink as unlink4 } from "node:fs/promises";
20241
- import { dirname as dirname7, extname as extname3, resolve as resolve6 } from "node:path";
20628
+ import { lstat as lstat9, mkdir as mkdir6, open as open8, rmdir as rmdir2, unlink as unlink4 } from "node:fs/promises";
20629
+ import { dirname as dirname9, extname as extname4, resolve as resolve6 } from "node:path";
20242
20630
 
20243
20631
  // packages/core/src/motion-liquid.ts
20244
20632
  function createLiquidRenderer(config2, context) {
@@ -20418,8 +20806,8 @@ function createLiquidRenderer(config2, context) {
20418
20806
  context.strokeStyle = rgba(palette.foreground, 0.14);
20419
20807
  context.lineWidth = Math.max(0.5, min * 1e-3);
20420
20808
  context.stroke();
20421
- const text7 = [{ key: "title", text: config2.title, x: w * 0.065, y: h * 0.055, width: w * 0.87, height: h * (tall ? 0.095 : 0.12), size: min * 0.062, weight: 760 }];
20422
- if (config2.subtitle) text7.push({ key: "subtitle", text: config2.subtitle, x: w * 0.067, y: h * (tall ? 0.159 : 0.176), width: w * 0.85, height: h * (tall ? 0.057 : 0.065), size: min * 0.025, opacity: 0.79 });
20809
+ const text9 = [{ key: "title", text: config2.title, x: w * 0.065, y: h * 0.055, width: w * 0.87, height: h * (tall ? 0.095 : 0.12), size: min * 0.062, weight: 760 }];
20810
+ if (config2.subtitle) text9.push({ key: "subtitle", text: config2.subtitle, x: w * 0.067, y: h * (tall ? 0.159 : 0.176), width: w * 0.85, height: h * (tall ? 0.057 : 0.065), size: min * 0.025, opacity: 0.79 });
20423
20811
  const active = config2.motion === "reduced" ? items.length - 1 : Math.min(items.length - 1, Math.floor(p * items.length));
20424
20812
  items.forEach((item, index) => {
20425
20813
  const col = wide ? 0 : index % 2, row = wide ? index : Math.floor(index / 2);
@@ -20441,11 +20829,11 @@ function createLiquidRenderer(config2, context) {
20441
20829
  context.lineWidth = min * 3e-3;
20442
20830
  context.stroke();
20443
20831
  }
20444
- text7.push({ key: `number-${index}`, text: String(index + 1).padStart(2, "0"), x, y, width: min * 0.049, height: min * 0.04, size: min * 0.022, weight: 700, opacity: 0.65 });
20445
- text7.push({ key: `label-${index}`, text: item.label, x: x + min * 0.06, y, width: width - min * 0.06, height: h * (wide ? 0.062 : tall ? 0.045 : 0.055), size: min * 0.03, weight: 720, opacity: active === index || p >= 0.88 ? 1 : 0.75 });
20446
- if (item.detail) text7.push({ key: `detail-${index}`, text: item.detail, x, y: y + h * (wide ? 0.065 : tall ? 0.048 : 0.06), width, height: h * (wide ? 0.067 : tall ? 0.072 : 0.065), size: min * 0.022, opacity: 0.72 });
20832
+ text9.push({ key: `number-${index}`, text: String(index + 1).padStart(2, "0"), x, y, width: min * 0.049, height: min * 0.04, size: min * 0.022, weight: 700, opacity: 0.65 });
20833
+ text9.push({ key: `label-${index}`, text: item.label, x: x + min * 0.06, y, width: width - min * 0.06, height: h * (wide ? 0.062 : tall ? 0.045 : 0.055), size: min * 0.03, weight: 720, opacity: active === index || p >= 0.88 ? 1 : 0.75 });
20834
+ if (item.detail) text9.push({ key: `detail-${index}`, text: item.detail, x, y: y + h * (wide ? 0.065 : tall ? 0.048 : 0.06), width, height: h * (wide ? 0.067 : tall ? 0.072 : 0.065), size: min * 0.022, opacity: 0.72 });
20447
20835
  });
20448
- return text7;
20836
+ return text9;
20449
20837
  } };
20450
20838
  }
20451
20839
 
@@ -20493,7 +20881,7 @@ function createGlassRenderer(config2, context) {
20493
20881
  if (!Number.isFinite(time)) throw new Error("Glass time must be finite.");
20494
20882
  const p = config2.motion === "reduced" ? 1 : clamp(time / config2.duration);
20495
20883
  const phase = config2.motion === "reduced" ? 1.85 : p * Math.PI * 1.7;
20496
- const text7 = [{
20884
+ const text9 = [{
20497
20885
  key: "title",
20498
20886
  text: config2.title,
20499
20887
  x: w * 0.065,
@@ -20503,7 +20891,7 @@ function createGlassRenderer(config2, context) {
20503
20891
  size: min * 0.066,
20504
20892
  weight: 550
20505
20893
  }];
20506
- if (config2.subtitle) text7.push({
20894
+ if (config2.subtitle) text9.push({
20507
20895
  key: "subtitle",
20508
20896
  text: config2.subtitle,
20509
20897
  x: w * 0.067,
@@ -20603,8 +20991,8 @@ function createGlassRenderer(config2, context) {
20603
20991
  context.moveTo(x, y - min * 0.014);
20604
20992
  context.lineTo(x + width * (0.07 + 0.18 * settle), y - min * 0.014);
20605
20993
  stroke(color(palette.accent, 0.85), Math.max(1, min * 2e-3));
20606
- text7.push({ key: `label-${index}`, text: item.label, x, y, width, height: h * (wide ? 0.044 : tall ? 0.043 : 0.048), size: min * 0.031, weight: 620, opacity: 0.74 + settle * 0.26 });
20607
- if (item.detail) text7.push({
20994
+ text9.push({ key: `label-${index}`, text: item.label, x, y, width, height: h * (wide ? 0.044 : tall ? 0.043 : 0.048), size: min * 0.031, weight: 620, opacity: 0.74 + settle * 0.26 });
20995
+ if (item.detail) text9.push({
20608
20996
  key: `detail-${index}`,
20609
20997
  text: item.detail,
20610
20998
  x,
@@ -20615,7 +21003,7 @@ function createGlassRenderer(config2, context) {
20615
21003
  opacity: 0.72
20616
21004
  });
20617
21005
  });
20618
- return text7;
21006
+ return text9;
20619
21007
  } };
20620
21008
  }
20621
21009
 
@@ -20700,8 +21088,8 @@ function createPixelRenderer(config2, context) {
20700
21088
  const p = clamp(stepped / config2.duration), final = p >= 0.9;
20701
21089
  const travel = clamp((p - 0.09) / 0.72) * (items.length - 1), frame = Math.floor(stepped * 10);
20702
21090
  const camera = Math.floor(Math.min(p, 0.9) * 24);
20703
- const text7 = [{ key: "title", text: config2.title, x: w * 0.065, y: h * 0.055, width: w * 0.87, height: h * (tall ? 0.095 : 0.12), size: min * 0.06, weight: 750 }];
20704
- if (config2.subtitle) text7.push({ key: "subtitle", text: config2.subtitle, x: w * 0.067, y: h * (tall ? 0.161 : 0.181), width: w * 0.85, height: h * (tall ? 0.057 : 0.065), size: min * 0.025, opacity: 0.8 });
21091
+ const text9 = [{ key: "title", text: config2.title, x: w * 0.065, y: h * 0.055, width: w * 0.87, height: h * (tall ? 0.095 : 0.12), size: min * 0.06, weight: 750 }];
21092
+ if (config2.subtitle) text9.push({ key: "subtitle", text: config2.subtitle, x: w * 0.067, y: h * (tall ? 0.161 : 0.181), width: w * 0.85, height: h * (tall ? 0.057 : 0.065), size: min * 0.025, opacity: 0.8 });
20705
21093
  const sunX = Math.round(gw * 0.77 - camera * 0.08), sunY = world.top + 8;
20706
21094
  for (let y = -10; y <= 10; y++) {
20707
21095
  const half = Math.floor(Math.sqrt(Math.max(0, 100 - y * y)));
@@ -20765,7 +21153,7 @@ function createPixelRenderer(config2, context) {
20765
21153
  sprite(["aaaaaaa", "aaaaaaa", "aaaaaa.", "aaaa..."], markerX + 1, y - 18 + (wave === 2 ? 1 : 0), visited ? 1 : 0.26);
20766
21154
  rect2(markerX - 2, y - 1, 5, 1, palette.foreground, 0.5);
20767
21155
  }
20768
- text7.push({ key: `milestone-${index}`, text: String(index + 1).padStart(2, "0"), x: (node.x - 5) * unit, y: (node.y + 19) * unit, width: unit * 10, height: unit * 7, size: min * 0.022, align: "center", weight: 750, opacity: visited ? 1 : 0.4 });
21156
+ text9.push({ key: `milestone-${index}`, text: String(index + 1).padStart(2, "0"), x: (node.x - 5) * unit, y: (node.y + 19) * unit, width: unit * 10, height: unit * 7, size: min * 0.022, align: "center", weight: 750, opacity: visited ? 1 : 0.4 });
20769
21157
  });
20770
21158
  const fromIndex = Math.min(items.length - 2, Math.floor(travel)), amount = travel - fromIndex;
20771
21159
  const from = nodes[fromIndex], to = nodes[fromIndex + 1];
@@ -20782,11 +21170,11 @@ function createPixelRenderer(config2, context) {
20782
21170
  const boxW = cellW - w * 0.025, visited = travel >= index;
20783
21171
  rect2(x / unit, (y - min * 0.018) / unit, boxW / unit, 1, palette.foreground, 0.16);
20784
21172
  rect2(x / unit, (y - min * 0.018) / unit, Math.min(boxW / unit, visited ? 14 : 3), 1, visited ? palette.accent : palette.foreground, visited ? 1 : 0.3);
20785
- text7.push({ key: `label-${index}`, text: item.label, x, y, width: boxW, height: h * (wide ? 0.054 : tall ? 0.036 : 0.049), size: min * 0.029, weight: 700, opacity: visited ? 1 : 0.6 });
20786
- if (item.detail) text7.push({ key: `detail-${index}`, text: item.detail, x, y: y + h * (wide ? 0.058 : tall ? 0.039 : 0.053), width: boxW, height: h * (wide ? 0.075 : tall ? 0.058 : 0.09), size: min * 0.022, opacity: visited ? 0.82 : 0.52 });
21173
+ text9.push({ key: `label-${index}`, text: item.label, x, y, width: boxW, height: h * (wide ? 0.054 : tall ? 0.036 : 0.049), size: min * 0.029, weight: 700, opacity: visited ? 1 : 0.6 });
21174
+ if (item.detail) text9.push({ key: `detail-${index}`, text: item.detail, x, y: y + h * (wide ? 0.058 : tall ? 0.039 : 0.053), width: boxW, height: h * (wide ? 0.075 : tall ? 0.058 : 0.09), size: min * 0.022, opacity: visited ? 0.82 : 0.52 });
20787
21175
  });
20788
21176
  context.globalAlpha = 1;
20789
- return text7;
21177
+ return text9;
20790
21178
  } };
20791
21179
  }
20792
21180
 
@@ -20868,14 +21256,14 @@ function visualDocumentBoot(config2, factory) {
20868
21256
  ctx.fillStyle = config2.palette.background;
20869
21257
  ctx.fillRect(0, 0, W, H);
20870
21258
  ctx.save();
20871
- let text7;
21259
+ let text9;
20872
21260
  try {
20873
- text7 = renderer.draw(current2);
21261
+ text9 = renderer.draw(current2);
20874
21262
  } finally {
20875
21263
  ctx.restore();
20876
21264
  }
20877
21265
  for (const node of nodes.values()) node.style.display = "none";
20878
- text7.forEach(textNode);
21266
+ text9.forEach(textNode);
20879
21267
  scrubber.value = String(current2);
20880
21268
  timeLabel.textContent = current2.toFixed(1) + " / " + config2.duration.toFixed(1) + " s";
20881
21269
  }
@@ -21032,8 +21420,8 @@ function createIsometricRenderer(config2, context) {
21032
21420
  return { draw(time) {
21033
21421
  if (!Number.isFinite(time)) throw new Error("Isometric time must be finite.");
21034
21422
  const p = config2.motion === "reduced" ? 1 : clamp(time / config2.duration);
21035
- const text7 = [{ key: "title", text: config2.title, x: w * 0.065, y: h * 0.055, width: w * 0.87, height: h * (tall ? 0.095 : 0.12), size: min * (wide ? 0.059 : 0.06), weight: 750 }];
21036
- if (config2.subtitle) text7.push({ key: "subtitle", text: config2.subtitle, x: w * 0.067, y: h * (tall ? 0.159 : 0.176), width: w * 0.85, height: h * (tall ? 0.057 : 0.065), size: min * 0.025, opacity: 0.78 });
21423
+ const text9 = [{ key: "title", text: config2.title, x: w * 0.065, y: h * 0.055, width: w * 0.87, height: h * (tall ? 0.095 : 0.12), size: min * (wide ? 0.059 : 0.06), weight: 750 }];
21424
+ if (config2.subtitle) text9.push({ key: "subtitle", text: config2.subtitle, x: w * 0.067, y: h * (tall ? 0.159 : 0.176), width: w * 0.85, height: h * (tall ? 0.057 : 0.065), size: min * 0.025, opacity: 0.78 });
21037
21425
  const tiles = [];
21038
21426
  for (let ix = 0; ix < 6; ix++) for (let iy = 0; iy < 6; iy++) tiles.push({ x: -1.75 + ix * 0.7, y: -1.6 + iy * 0.7, depth: ix + iy });
21039
21427
  tiles.sort((a, b) => a.depth - b.depth).forEach((tile) => {
@@ -21077,7 +21465,7 @@ function createIsometricRenderer(config2, context) {
21077
21465
  polygon([project(x - 0.24, y - 0.24, z + 4e-3), project(x + 0.24, y - 0.24, z + 4e-3), project(x + 0.24, y + 0.24, z + 4e-3), project(x - 0.24, y + 0.24, z + 4e-3)], rgba(palette.background, 0.22));
21078
21466
  }
21079
21467
  const anchor = project(x, y, roofHeights[index] + 0.47);
21080
- text7.push({ key: `node-${index}`, text: String(index + 1).padStart(2, "0"), x: anchor[0] - min * 0.035, y: anchor[1] - min * 0.017, width: min * 0.07, height: min * 0.035, size: min * 0.024, opacity: smooth((p - start2 - 0.11) / 0.11), align: "center", weight: 750 });
21468
+ text9.push({ key: `node-${index}`, text: String(index + 1).padStart(2, "0"), x: anchor[0] - min * 0.035, y: anchor[1] - min * 0.017, width: min * 0.07, height: min * 0.035, size: min * 0.024, opacity: smooth((p - start2 - 0.11) / 0.11), align: "center", weight: 750 });
21081
21469
  });
21082
21470
  items.forEach((item, index) => {
21083
21471
  const columns = wide ? 1 : 2, col = index % columns, row = Math.floor(index / columns);
@@ -21088,11 +21476,11 @@ function createIsometricRenderer(config2, context) {
21088
21476
  line([[x, y - min * 0.012], [x + width, y - min * 0.012]], rgba(palette.foreground, 0.17), strokeWidth);
21089
21477
  const bar = ease((p - 0.12 - index * 0.105) / 0.19);
21090
21478
  line([[x, y - min * 0.012], [x + width * bar, y - min * 0.012]], rgba(palette.accent, 0.95), strokeWidth * 2.1);
21091
- text7.push({ key: `index-${index}`, text: String(index + 1).padStart(2, "0"), x, y, width: min * 0.049, height: min * 0.04, size: min * 0.023, opacity: 0.72 + reveal * 0.28, weight: 650 });
21092
- text7.push({ key: `label-${index}`, text: item.label, x: x + min * 0.06, y, width: width - min * 0.06, height: h * (wide ? 0.062 : tall ? 0.045 : 0.055), size: min * 0.03, weight: 700, opacity: 0.58 + reveal * 0.42 });
21093
- if (item.detail) text7.push({ key: `detail-${index}`, text: item.detail, x, y: y + h * (wide ? 0.063 : tall ? 0.048 : 0.06), width, height: h * (wide ? 0.067 : tall ? 0.072 : 0.065), size: min * 0.022, opacity: 0.52 + reveal * 0.25 });
21479
+ text9.push({ key: `index-${index}`, text: String(index + 1).padStart(2, "0"), x, y, width: min * 0.049, height: min * 0.04, size: min * 0.023, opacity: 0.72 + reveal * 0.28, weight: 650 });
21480
+ text9.push({ key: `label-${index}`, text: item.label, x: x + min * 0.06, y, width: width - min * 0.06, height: h * (wide ? 0.062 : tall ? 0.045 : 0.055), size: min * 0.03, weight: 700, opacity: 0.58 + reveal * 0.42 });
21481
+ if (item.detail) text9.push({ key: `detail-${index}`, text: item.detail, x, y: y + h * (wide ? 0.063 : tall ? 0.048 : 0.06), width, height: h * (wide ? 0.067 : tall ? 0.072 : 0.065), size: min * 0.022, opacity: 0.52 + reveal * 0.25 });
21094
21482
  });
21095
- return text7;
21483
+ return text9;
21096
21484
  } };
21097
21485
  }
21098
21486
 
@@ -21216,7 +21604,7 @@ function createSketchRenderer(config2, context) {
21216
21604
  const phase = reduced ? 1 : clampedTime / config2.duration;
21217
21605
  const frame = reduced ? 0 : Math.floor(clampedTime * 8);
21218
21606
  const titleOpacity = reduced ? 1 : 0.25 + 0.75 * ease(phase / 0.045);
21219
- const text7 = [
21607
+ const text9 = [
21220
21608
  { key: "title", text: config2.title, x: W * 0.065, y: H * 0.055, width: W * 0.87, height: H * 0.145, size: S * 0.081, weight: 600, opacity: titleOpacity },
21221
21609
  { key: "subtitle", text: config2.subtitle || "", x: W * 0.067, y: H * 0.216, width: W * 0.84, height: H * 0.052, size: S * 0.026, weight: 400, opacity: titleOpacity }
21222
21610
  ];
@@ -21249,9 +21637,9 @@ function createSketchRenderer(config2, context) {
21249
21637
  context.restore();
21250
21638
  }
21251
21639
  const numberSize = Math.min(S * 0.039, node.radius * 0.57);
21252
- text7.push({ key: `number-${index}`, text: String(index + 1).padStart(2, "0"), x: node.x - node.radius * 0.52, y: node.y - numberSize * 0.64, width: node.radius * 1.04, height: numberSize * 1.4, size: numberSize, weight: 400, align: "center", opacity: appear });
21253
- text7.push({ key: `label-${index}`, text: config2.items[index].label, ...node.label, size: S * (vertical ? 0.043 : 0.046), weight: 600, align: node.align, opacity: appear });
21254
- text7.push({ key: `detail-${index}`, text: config2.items[index].detail || "", ...node.detail, size: S * (portrait ? 0.028 : 0.024), weight: 400, align: node.align, opacity: appear });
21640
+ text9.push({ key: `number-${index}`, text: String(index + 1).padStart(2, "0"), x: node.x - node.radius * 0.52, y: node.y - numberSize * 0.64, width: node.radius * 1.04, height: numberSize * 1.4, size: numberSize, weight: 400, align: "center", opacity: appear });
21641
+ text9.push({ key: `label-${index}`, text: config2.items[index].label, ...node.label, size: S * (vertical ? 0.043 : 0.046), weight: 600, align: node.align, opacity: appear });
21642
+ text9.push({ key: `detail-${index}`, text: config2.items[index].detail || "", ...node.detail, size: S * (portrait ? 0.028 : 0.024), weight: 400, align: node.align, opacity: appear });
21255
21643
  const underline = ease((phase - nodeStart(index) - 0.2) / 0.12);
21256
21644
  const lineY = node.label.y + node.label.height + S * 4e-3;
21257
21645
  const lineWidth = node.label.width * (vertical ? 0.43 : 0.66);
@@ -21263,7 +21651,7 @@ function createSketchRenderer(config2, context) {
21263
21651
  stroke(ellipse(last.x, last.y, last.radius * 1.13, last.radius * 1.115, -0.66, Math.PI * 1.76), finalEmphasis, frame, 66, palette.accent, S * 26e-4, 0.85);
21264
21652
  stroke([{ x: W * 0.067, y: H * 0.94 }, { x: W * 0.933, y: H * 0.94 }], 1, frame, 96, palette.foreground, S * 13e-4, 0.14, false);
21265
21653
  stroke([{ x: W * 0.067, y: H * 0.94 }, { x: W * 0.933, y: H * 0.94 }], reduced ? 1 : clamp(phase / 0.88), frame, 95, palette.accent, S * 24e-4, 0.8, false);
21266
- return text7;
21654
+ return text9;
21267
21655
  }
21268
21656
  };
21269
21657
  }
@@ -21534,10 +21922,10 @@ function motionDocumentBoot(config2) {
21534
21922
  return p * p * p * (p * (p * 6 - 15) + 10);
21535
21923
  };
21536
21924
  const lerp = (a, b, p) => a + (b - a) * p;
21537
- const element = (tag, className, parent = stage, text7) => {
21925
+ const element = (tag, className, parent = stage, text9) => {
21538
21926
  const node = document.createElement(tag);
21539
21927
  node.className = className;
21540
- if (text7 !== void 0) node.textContent = text7;
21928
+ if (text9 !== void 0) node.textContent = text9;
21541
21929
  parent.appendChild(node);
21542
21930
  return node;
21543
21931
  };
@@ -21608,9 +21996,9 @@ function motionDocumentBoot(config2) {
21608
21996
  }
21609
21997
  config2.items.forEach((item, index) => {
21610
21998
  const pos = layout.overview[index];
21611
- const object10 = element("div", "sticker-object", world);
21612
- stickerObjects.push(object10);
21613
- box(object10, pos.x - pos.size / 2, pos.y - pos.size / 2, pos.size, pos.size);
21999
+ const object11 = element("div", "sticker-object", world);
22000
+ stickerObjects.push(object11);
22001
+ box(object11, pos.x - pos.size / 2, pos.y - pos.size / 2, pos.size, pos.size);
21614
22002
  if (item.image) {
21615
22003
  const image2 = document.createElement("img");
21616
22004
  image2.src = item.image;
@@ -21619,7 +22007,7 @@ function motionDocumentBoot(config2) {
21619
22007
  image2.draggable = false;
21620
22008
  image2.dataset.stickerIndex = String(index);
21621
22009
  image2.style.visibility = "hidden";
21622
- object10.appendChild(image2);
22010
+ object11.appendChild(image2);
21623
22011
  images.push(image2);
21624
22012
  } else {
21625
22013
  const illustration = svgNode("svg", { viewBox: "-125 -125 250 250", class: "illustration", role: "img", "aria-label": "Illustration: " + item.label });
@@ -21627,7 +22015,7 @@ function motionDocumentBoot(config2) {
21627
22015
  illustration.appendChild(svgNode("path", { d: pathOf(contours[index]), fill: "none", stroke: "var(--fg)", "stroke-width": "1.4", "stroke-linejoin": "round" }));
21628
22016
  const glyph = svgNode("path", { d: index % 2 ? "M-24,0 L-6,18 L30,-22" : "M-28,0 H28 M0,-28 V28", fill: "none", stroke: "var(--fg)", "stroke-width": "6", "stroke-linecap": "round", "stroke-linejoin": "round" });
21629
22017
  illustration.appendChild(glyph);
21630
- object10.appendChild(illustration);
22018
+ object11.appendChild(illustration);
21631
22019
  }
21632
22020
  const overviewLabel = element("div", "overview-label measured", world, item.label);
21633
22021
  box(overviewLabel, pos.x - pos.labelWidth / 2, pos.y + pos.size / 2 + s * 0.02, pos.labelWidth, pos.labelHeight);
@@ -21702,9 +22090,9 @@ function motionDocumentBoot(config2) {
21702
22090
  const ca = cameraFor(a.index), cb = cameraFor(b.index), focus = lerp(ca.focus, cb.focus, blend);
21703
22091
  const x = lerp(ca.x, cb.x, blend), y = lerp(ca.y, cb.y, blend), z = lerp(ca.z, cb.z, blend);
21704
22092
  stickerPaint.clearRect(0, 0, W, H);
21705
- stickerObjects.forEach((object10, index) => {
22093
+ stickerObjects.forEach((object11, index) => {
21706
22094
  const pos = layout.overview[index];
21707
- box(object10, x + (pos.x - pos.size / 2) * z, y + (pos.y - pos.size / 2) * z, pos.size * z, pos.size * z);
22095
+ box(object11, x + (pos.x - pos.size / 2) * z, y + (pos.y - pos.size / 2) * z, pos.size * z, pos.size * z);
21708
22096
  });
21709
22097
  overviewLabels.forEach((node, index) => {
21710
22098
  const pos = layout.overview[index];
@@ -21785,7 +22173,7 @@ function motionDocumentBoot(config2) {
21785
22173
  const width = ink.width / scale, height = ink.height / scale;
21786
22174
  return { text: node.textContent, x, y, width, height, fontSize: parseFloat(getComputedStyle(node).fontSize), container: { x: (rect2.x - stageRect.x) / scale, y: (rect2.y - stageRect.y) / scale, width: rect2.width / scale, height: rect2.height / scale }, outOfBounds: x < -1 || y < -1 || x + width > W + 1 || y + height > H + 1, overflow: node.scrollWidth > node.clientWidth + 1 || node.scrollHeight > node.clientHeight + 1 };
21787
22175
  });
21788
- return { ready: runtime.__ready, error: runtime.__motionError || null, time: currentTime, format: layout.format, activeItem: currentIndex, overview: overviewState, textBounds, outOfBounds: textBounds.some((text7) => text7.outOfBounds || text7.overflow), illustrationFallbacks: config2.items.map((item, index) => !item.image && config2.componentId === "sticker-explainer" ? index : -1).filter((index) => index >= 0), fontLoaded: document.fonts.check("16px EditorBrainEmbedded"), imagesDecoded: images.every((image2) => image2.complete && image2.naturalWidth > 0) };
22176
+ return { ready: runtime.__ready, error: runtime.__motionError || null, time: currentTime, format: layout.format, activeItem: currentIndex, overview: overviewState, textBounds, outOfBounds: textBounds.some((text9) => text9.outOfBounds || text9.overflow), illustrationFallbacks: config2.items.map((item, index) => !item.image && config2.componentId === "sticker-explainer" ? index : -1).filter((index) => index >= 0), fontLoaded: document.fonts.check("16px EditorBrainEmbedded"), imagesDecoded: images.every((image2) => image2.complete && image2.naturalWidth > 0) };
21789
22177
  };
21790
22178
  let playing = false, frame = 0, playStart = 0, timeStart = 0;
21791
22179
  const pause = () => {
@@ -21932,18 +22320,18 @@ ${motionSceneLayout.toString()}
21932
22320
 
21933
22321
  // packages/cli/src/motion-components.ts
21934
22322
  var fail = (message) => new CliError("MOTION_COMPONENT_INVALID", message, "Use a pinned component, explicit resolved token bindings, a local WOFF2/TTF font and 2\u20134 plain-text items.");
21935
- function object9(value, name, allowed) {
22323
+ function object10(value, name, allowed) {
21936
22324
  if (!value || typeof value !== "object" || Array.isArray(value) || Object.keys(value).some((key) => !allowed.includes(key))) throw fail(`${name} has invalid or unsupported fields.`);
21937
22325
  }
21938
22326
  function inputDocument(value) {
21939
- object9(value, "Input", ["componentId", "componentVersion", "bindings", "fontFile", "title", "subtitle", "duration", "items", "motion"]);
22327
+ object10(value, "Input", ["componentId", "componentVersion", "bindings", "fontFile", "title", "subtitle", "duration", "items", "motion"]);
21940
22328
  if (typeof value.componentId !== "string" || value.componentVersion !== "1.0.0" || !getMotionComponent(value.componentId, value.componentVersion)) throw fail("Unknown component or unsupported pinned componentVersion.");
21941
- object9(value.bindings, "bindings", ["background", "foreground", "accent", "fontFamily"]);
22329
+ object10(value.bindings, "bindings", ["background", "foreground", "accent", "fontFamily"]);
21942
22330
  for (const role of ["background", "foreground", "accent", "fontFamily"]) if (typeof value.bindings[role] !== "string" || !/^[a-z][a-z0-9_.-]{0,79}$/.test(value.bindings[role])) throw fail(`bindings.${role} must name an explicit semantic token key.`);
21943
22331
  if (typeof value.fontFile !== "string" || !value.fontFile) throw fail("fontFile is required.");
21944
22332
  if (!Array.isArray(value.items) || value.items.length < 2 || value.items.length > 4) throw fail("Input requires 2\u20134 items.");
21945
22333
  for (const item of value.items) {
21946
- object9(item, "item", ["label", "detail", "imageFile", "shape"]);
22334
+ object10(item, "item", ["label", "detail", "imageFile", "shape"]);
21947
22335
  if (item.imageFile !== void 0 && (typeof item.imageFile !== "string" || !item.imageFile)) throw fail("imageFile must be an explicit local image path.");
21948
22336
  }
21949
22337
  return value;
@@ -21967,22 +22355,22 @@ async function sourceFile(path, role, max) {
21967
22355
  }
21968
22356
  const after = await handle.stat();
21969
22357
  await assertNoMediaSymlinks(path);
21970
- const current2 = await lstat8(path);
22358
+ const current2 = await lstat9(path);
21971
22359
  if (before.size !== after.size || before.mtimeMs !== after.mtimeMs || before.ctimeMs !== after.ctimeMs || current2.dev !== before.dev || current2.ino !== before.ino || current2.size !== after.size || current2.mtimeMs !== after.mtimeMs) throw fail(`${role} changed during reading.`);
21972
- return { source: { role, path, bytes: content.length, sha256: createHash6("sha256").update(content).digest("hex") }, content };
22360
+ return { source: { role, path, bytes: content.length, sha256: createHash7("sha256").update(content).digest("hex") }, content };
21973
22361
  } finally {
21974
22362
  await handle.close();
21975
22363
  }
21976
22364
  }
21977
22365
  function embed(content, path, kind) {
21978
22366
  const formats = kind === "font" ? { ".woff2": "font/woff2", ".ttf": "font/ttf" } : { ".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".webp": "image/webp" };
21979
- const mime = formats[extname3(path).toLowerCase()];
22367
+ const mime = formats[extname4(path).toLowerCase()];
21980
22368
  if (!mime) throw fail(`${kind} file type is unsupported. Use ${kind === "font" ? "WOFF2/TTF" : "PNG/JPEG/WebP"}.`);
21981
22369
  return `data:${mime};base64,${content.toString("base64")}`;
21982
22370
  }
21983
22371
  async function exists2(path) {
21984
22372
  try {
21985
- await lstat8(path);
22373
+ await lstat9(path);
21986
22374
  return true;
21987
22375
  } catch (error) {
21988
22376
  if (error.code === "ENOENT") return false;
@@ -21996,18 +22384,18 @@ async function makeParents(root, path, created) {
21996
22384
  const target = await checkedPath(root, relative2, false);
21997
22385
  try {
21998
22386
  await mkdir6(target, { mode: 448 });
21999
- const info = await lstat8(target);
22387
+ const info = await lstat9(target);
22000
22388
  created.push({ path: relative2, dev: info.dev, ino: info.ino, directory: true });
22001
22389
  } catch (error) {
22002
22390
  if (error.code !== "EEXIST") throw error;
22003
- if (!(await lstat8(await checkedPath(root, relative2, false))).isDirectory()) throw fail("Output parent must be a directory.");
22391
+ if (!(await lstat9(await checkedPath(root, relative2, false))).isDirectory()) throw fail("Output parent must be a directory.");
22004
22392
  }
22005
22393
  }
22006
22394
  }
22007
22395
  async function cleanCreated2(root, created) {
22008
22396
  for (const item of [...created].reverse()) {
22009
22397
  try {
22010
- const path = await checkedPath(root, item.path, false), info = await lstat8(path);
22398
+ const path = await checkedPath(root, item.path, false), info = await lstat9(path);
22011
22399
  if (info.dev !== item.dev || info.ino !== item.ino || info.isSymbolicLink()) continue;
22012
22400
  if (item.directory) await rmdir2(path);
22013
22401
  else await unlink4(path);
@@ -22035,14 +22423,14 @@ async function prepareMotionComponent(options) {
22035
22423
  if (typeof value !== "string" || !value.trim()) throw fail(`Required binding ${role} references missing or non-string token ${key}. No demo default was applied.`);
22036
22424
  return [role, value];
22037
22425
  }));
22038
- const fontPath = inputPath(dirname7(input.source.path), document2.fontFile);
22426
+ const fontPath = inputPath(dirname9(input.source.path), document2.fontFile);
22039
22427
  const font = await sourceFile(fontPath, "font", MOTION_INPUT_LIMITS.fontBytes);
22040
22428
  const sources = [input.source, font.source];
22041
22429
  const items = [];
22042
22430
  for (const [index, item] of document2.items.entries()) {
22043
22431
  let image2;
22044
22432
  if (item.imageFile) {
22045
- const path = inputPath(dirname7(input.source.path), item.imageFile);
22433
+ const path = inputPath(dirname9(input.source.path), item.imageFile);
22046
22434
  const file = await sourceFile(path, `items[${index}].image`, MOTION_INPUT_LIMITS.imageBytes);
22047
22435
  image2 = embed(file.content, path, "image");
22048
22436
  sources.push(file.source);
@@ -22083,7 +22471,7 @@ async function prepareMotionComponent(options) {
22083
22471
  resolvedBindings: Object.fromEntries(Object.entries(document2.bindings).map(([role, key]) => [role, { key, value: bound[role], source: resolved.tokenSources[key] }])),
22084
22472
  sources,
22085
22473
  context: { sourceHashes: resolved.lock.sourceHashes, output: resolved.output, mediaPreferences: resolved.mediaPreferences },
22086
- files: { "config.json": { sha256: createHash6("sha256").update(configJson).digest("hex") }, "index.html": { sha256: createHash6("sha256").update(html).digest("hex") } },
22474
+ files: { "config.json": { sha256: createHash7("sha256").update(configJson).digest("hex") }, "index.html": { sha256: createHash7("sha256").update(html).digest("hex") } },
22087
22475
  warnings: resolved.warnings
22088
22476
  };
22089
22477
  const readme = `# EditorBrain motion component
@@ -22103,9 +22491,9 @@ Preparation is not a rendered video or a quality certification. Inspect text fit
22103
22491
  if (await exists2(await productionOutputPath(root, options.outDirectory))) throw new CliError("MOTION_OUTPUT_EXISTS", "The output directory already exists; nothing was overwritten.", "Choose a new directory under .editorbrain-local/.");
22104
22492
  const created = [];
22105
22493
  try {
22106
- await makeParents(root, dirname7(options.outDirectory), created);
22494
+ await makeParents(root, dirname9(options.outDirectory), created);
22107
22495
  await mkdir6(await productionOutputPath(root, options.outDirectory), { mode: 448 });
22108
- const info = await lstat8(output2);
22496
+ const info = await lstat9(output2);
22109
22497
  created.push({ path: options.outDirectory, dev: info.dev, ino: info.ino, directory: true });
22110
22498
  for (const [name, content] of Object.entries(files)) {
22111
22499
  const path = `${options.outDirectory}/${name}`;
@@ -22141,12 +22529,12 @@ Preparation is not a rendered video or a quality certification. Inspect text fit
22141
22529
  }
22142
22530
 
22143
22531
  // packages/cli/src/authored-motion.ts
22144
- import { lstat as lstat9 } from "node:fs/promises";
22532
+ import { lstat as lstat10 } from "node:fs/promises";
22145
22533
  async function downloadAuthoredSource(client, pin, root, out, fetcher = fetch) {
22146
22534
  if (!/^[a-z0-9][a-z0-9-]{0,79}$/.test(pin.id) || !/^\d+\.\d+\.\d+$/.test(pin.version) || !/^[a-f0-9]{64}$/.test(pin.sha256) || !Number.isSafeInteger(pin.bytes) || pin.bytes < 1 || pin.bytes > 16 * 1024 * 1024) throw new CliError("INVALID_ARGUMENT", "Invalid pinned scene bundle.", "Retrieve one authored scene from the catalog.");
22147
22535
  const target = await checkedPath(root, out, false);
22148
22536
  try {
22149
- await lstat9(target);
22537
+ await lstat10(target);
22150
22538
  throw new CliError("FILE_EXISTS", "The output already exists.", "Choose a new archive path.");
22151
22539
  } catch (error) {
22152
22540
  if (error.code !== "ENOENT") throw error;
@@ -22177,6 +22565,294 @@ async function downloadAuthoredSource(client, pin, root, out, fetcher = fetch) {
22177
22565
  return { id: pin.id, version: pin.version, path: target, sha256: pin.sha256, bytes: size, extracted: false, executed: false, rendered: false, next: "Inspect bundle README and selected card; extract into a new local directory. Bind current project colors, fonts, language, content, timing and sound before rendering. The demo is not your finished edit." };
22178
22566
  }
22179
22567
 
22568
+ // packages/cli/src/production-verify.ts
22569
+ import { createHash as createHash8, randomUUID as randomUUID3 } from "node:crypto";
22570
+ import { mkdir as mkdir7, readFile as readFile4 } from "node:fs/promises";
22571
+ import { dirname as dirname10, join as join7, resolve as resolve7 } from "node:path";
22572
+ var digest2 = (value) => createHash8("sha256").update(value).digest("hex");
22573
+ var errorCode2 = (error) => error && typeof error === "object" && "code" in error && typeof error.code === "string" && /^[A-Z0-9_]+$/.test(error.code) ? error.code : "PRODUCTION_VERIFY_FAILED";
22574
+ function productionVerificationRequirements(options) {
22575
+ const { resolved } = options;
22576
+ const voice = resolved.audioProduction?.voice;
22577
+ const loudness = resolved.audioProduction?.loudness ?? null;
22578
+ if (loudness) validateAudioCheckProfile({ schemaVersion: 1, loudness });
22579
+ const voiceRequired = !!voice && ["original", "catalog", "cloned"].includes(voice.mode);
22580
+ const audio = options.audio ?? (voiceRequired || loudness ? "required" : "optional");
22581
+ if (audio === "forbidden" && loudness) throw new CliError("PRODUCTION_REQUIREMENTS_CONFLICT", "Forbidden audio conflicts with the project loudness target.", "Resolve the explicit project audio requirements before verification.");
22582
+ const renderProfile = {
22583
+ schemaVersion: 1,
22584
+ expected: { width: resolved.output.width, height: resolved.output.height, fps: resolved.output.fps, ...options.expectedDurationSeconds !== void 0 ? { durationSeconds: options.expectedDurationSeconds } : {} },
22585
+ audio
22586
+ };
22587
+ validateRenderCheckProfile(renderProfile);
22588
+ return { renderProfile, audioBasis: options.audio !== void 0 ? "explicit_verification_requirement" : voiceRequired ? "configured_voice" : loudness ? "configured_loudness" : "no_explicit_track_requirement", loudness, durationBasis: options.expectedDurationSeconds !== void 0 ? "explicit" : "unspecified" };
22589
+ }
22590
+ function parseVerificationSamplePts(stderr) {
22591
+ const match = /\[[^\]\r\n]*showinfo[^\]\r\n]*\]\s+n:\s*0\s+pts:\s*-?\d+\s+pts_time:([-\d.e+]+)/i.exec(stderr);
22592
+ const value = match ? Number(match[1]) : NaN;
22593
+ return Number.isFinite(value) ? value : null;
22594
+ }
22595
+ async function sampleFinalArtifact(file, probe, directory, requested) {
22596
+ const video = probe.streams.find((stream) => stream.type === "video" && !stream.attachedPicture);
22597
+ const result2 = { status: "not_available", streamIndex: video?.index ?? null, items: [], failures: [] };
22598
+ if (!video) return result2;
22599
+ const duration = video.durationSeconds ?? probe.durationSeconds;
22600
+ const times = requested ? [...new Set(requested)] : duration && duration > 0 ? [.../* @__PURE__ */ new Set([0, duration / 2, Math.max(0, duration - Math.min(0.25, duration / 4))])] : [0];
22601
+ for (const time of times) {
22602
+ if (duration !== null && duration !== void 0 && time >= duration) {
22603
+ result2.failures.push({ requestedTimeSeconds: time, code: "MEDIA_SAMPLE_OUT_OF_RANGE" });
22604
+ continue;
22605
+ }
22606
+ const path = join7(directory, `sample-${String(result2.items.length + result2.failures.length + 1).padStart(2, "0")}.png`);
22607
+ try {
22608
+ const output2 = await runMediaTool("ffmpeg", [
22609
+ "-loglevel",
22610
+ "info",
22611
+ "-nostats",
22612
+ "-n",
22613
+ "-copyts",
22614
+ "-start_at_zero",
22615
+ ...localInputArgs(file),
22616
+ "-map",
22617
+ `0:${video.index}`,
22618
+ "-an",
22619
+ "-sn",
22620
+ "-dn",
22621
+ "-vf",
22622
+ `select=gte(t\\,${time}),showinfo,scale='min(1920,iw)':'min(1920,ih)':force_original_aspect_ratio=decrease`,
22623
+ "-frames:v",
22624
+ "1",
22625
+ "-fps_mode",
22626
+ "passthrough",
22627
+ "-update",
22628
+ "1",
22629
+ path
22630
+ ], { timeoutMs: 6e4, maxOutputBytes: 1024 * 1024 });
22631
+ const bytes = await readFile4(path);
22632
+ if (!bytes.length) throw new CliError("MEDIA_SAMPLE_EMPTY", "The selected frame produced no image.", "Review the final video directly.");
22633
+ result2.items.push({ path, sha256: digest2(bytes), bytes: bytes.length, requestedTimeSeconds: time, actualPresentationTimeSeconds: parseVerificationSamplePts(output2.stderr), timestampBasis: "seconds_from_container_start", contentReviewed: false });
22634
+ } catch (error) {
22635
+ result2.failures.push({ requestedTimeSeconds: time, code: errorCode2(error) });
22636
+ }
22637
+ }
22638
+ result2.status = result2.failures.length ? "incomplete" : "complete";
22639
+ return result2;
22640
+ }
22641
+ function pendingReviews(resolved) {
22642
+ const items = [
22643
+ { code: "EDITORIAL_REVIEW", message: "Watch the delivered video for message, timing, continuity and closure. Technical checks do not establish editorial quality." },
22644
+ { code: "VISUAL_REVIEW", message: "View samples and final output at delivery size for text, captions, crop, safe areas and visual defects. Samples are bounded previews, not continuous playback evidence." },
22645
+ { code: "BRAND_REVIEW", message: "Verify actual fonts, logo, palette and style in the rendered output; configured tokens do not prove rendered identity." },
22646
+ { code: "AUDIO_REVIEW", message: "If audio exists, listen to the final mix for voice identity, intelligibility, synchronization, music and SFX. Track presence and loudness do not identify or approve these contents." }
22647
+ ];
22648
+ if (resolved.audioProduction?.music?.intensity === "none" || resolved.audioProduction?.sfx?.density === "none") items.push({ code: "AUDIO_EXCLUSIONS", message: "Verify the requested music/SFX exclusions by listening. These preferences do not forbid original voice or establish absence of mixed sounds." });
22649
+ return items.map((item) => ({ ...item, status: "pending" }));
22650
+ }
22651
+ function summarizeProductionVerification(report) {
22652
+ return {
22653
+ schemaVersion: report.schemaVersion,
22654
+ kind: report.kind,
22655
+ summary: true,
22656
+ projectId: report.projectId,
22657
+ contextSha256: report.context.sha256,
22658
+ contextVersion: report.context.version,
22659
+ artifact: report.artifact,
22660
+ complete: report.complete,
22661
+ technicalStatus: report.technicalStatus,
22662
+ technicalPass: report.technicalPass,
22663
+ perceptualPending: true,
22664
+ creativeQuality: report.creativeQuality,
22665
+ checks: report.render?.checks ?? [],
22666
+ loudness: report.audio ? { status: report.audio.technicalStatus, streams: report.audio.measurements.slice(0, 4), omitted: Math.max(0, report.audio.measurements.length - 4) } : { status: "not_requested" },
22667
+ samples: report.samples,
22668
+ issues: report.issues,
22669
+ findingCounts: report.render?.findingsSummary ?? null,
22670
+ findings: report.render?.findings.slice(0, 5) ?? [],
22671
+ omittedFindings: Math.max(0, (report.render?.findings.length ?? 0) - 5),
22672
+ pendingReviews: report.pendingReviews.map((item) => item.code),
22673
+ agentInstructions: report.agentInstructions,
22674
+ mediaUploaded: false,
22675
+ mediaModified: false,
22676
+ modelCalls: 0
22677
+ };
22678
+ }
22679
+ async function verifyProduction(options) {
22680
+ const root = await ensureRoot(options.root);
22681
+ assertIdentifier(options.resolved.project.id);
22682
+ const requirements = productionVerificationRequirements(options);
22683
+ if (options.contextVersion !== void 0 && (typeof options.contextVersion !== "string" || !options.contextVersion || options.contextVersion.length > 256 || /[\x00-\x1f\x7f]/.test(options.contextVersion))) throw new CliError("INVALID_CONTEXT_VERSION", "Invalid context version.", "Pass the current project context version.");
22684
+ if (options.sampleTimesSeconds !== void 0 && (!Array.isArray(options.sampleTimesSeconds) || options.sampleTimesSeconds.length < 1 || options.sampleTimesSeconds.length > 6 || Array.from(options.sampleTimesSeconds).some((time) => typeof time !== "number" || !Number.isFinite(time) || time < 0 || time > 86400))) throw new CliError("INVALID_SAMPLE_TIMES", "Provide one to six finite sample times between 0 and 86400 seconds.", "Select a few timestamps that answer a review question.");
22685
+ const relativeDirectory = options.outDirectory ?? `.editorbrain-local/${options.resolved.project.id}/verify-${randomUUID3()}`;
22686
+ const directory = await productionOutputPath(root, relativeDirectory);
22687
+ await mkdir7(dirname10(directory), { recursive: true, mode: 448 });
22688
+ await productionOutputPath(root, relativeDirectory);
22689
+ try {
22690
+ await mkdir7(directory, { mode: 448 });
22691
+ } catch (error) {
22692
+ if (error.code === "EEXIST") throw new CliError("PRODUCTION_OUTPUT_EXISTS", "Verification output already exists; nothing was overwritten.", "Choose a new directory under .editorbrain-local/.");
22693
+ throw error;
22694
+ }
22695
+ const file = resolve7(root, options.file);
22696
+ const report = {
22697
+ schemaVersion: 1,
22698
+ kind: "editorbrain.production-verification",
22699
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
22700
+ projectId: options.resolved.project.id,
22701
+ context: { sha256: digest2(stableJson(options.resolved)), version: options.contextVersion ?? null, sourceHashes: { ...options.resolved.lock.sourceHashes }, validAtStart: null, validAtEnd: null },
22702
+ artifact: { path: file, sha256: null, bytes: null, stableAcrossVerification: null },
22703
+ requirements,
22704
+ complete: false,
22705
+ technicalStatus: "incomplete",
22706
+ technicalPass: false,
22707
+ perceptualPending: true,
22708
+ render: null,
22709
+ audio: null,
22710
+ audioMeasurement: "not_requested",
22711
+ samples: { status: "not_available", streamIndex: null, items: [], failures: [] },
22712
+ issues: [],
22713
+ pendingReviews: pendingReviews(options.resolved),
22714
+ agentInstructions: [
22715
+ "Open the saved sample images and actual final file before making visual or audio claims. Samples have not been viewed automatically.",
22716
+ "Reconcile black, silence and peak candidates with the brief. A technical pass never approves storytelling, readability, music/SFX, fonts or synchronization.",
22717
+ "Keep human approval separate. A changed artifact or project context needs a new verification; reuse unchanged source observations instead of reloading every document."
22718
+ ],
22719
+ mediaUploaded: false,
22720
+ mediaModified: false,
22721
+ modelCalls: 0,
22722
+ creativeQuality: "not_assessed"
22723
+ };
22724
+ const contextCheck = async (phase) => {
22725
+ if (!options.validateContext) return true;
22726
+ try {
22727
+ const valid = await options.validateContext();
22728
+ if (typeof valid !== "boolean") throw new Error("Invalid context validator result.");
22729
+ report.context[phase] = valid;
22730
+ if (!valid) report.issues.push({ code: "PRODUCTION_CONTEXT_CHANGED", severity: "error", message: "Project context changed; this verification is stale for the current project." });
22731
+ return valid;
22732
+ } catch {
22733
+ report.issues.push({ code: "PRODUCTION_CONTEXT_UNVERIFIED", severity: "incomplete", message: "Current project context could not be revalidated." });
22734
+ return false;
22735
+ }
22736
+ };
22737
+ if (await contextCheck("validAtStart")) {
22738
+ report.render = await checkRender({ file, profile: requirements.renderProfile });
22739
+ report.artifact = { ...report.render.file, stableAcrossVerification: null };
22740
+ const probe = report.render.measurements.probe;
22741
+ if (requirements.loudness) {
22742
+ report.audioMeasurement = "checked";
22743
+ report.audio = await checkAudio({ file, profile: { schemaVersion: 1, loudness: requirements.loudness } });
22744
+ }
22745
+ if (probe && report.render.checks.find((check) => check.id === "decode")?.status === "pass") report.samples = await sampleFinalArtifact(file, probe, directory, options.sampleTimesSeconds);
22746
+ if (probe) {
22747
+ try {
22748
+ const final = await probeLocalMedia(file);
22749
+ const stable = final.sha256 === probe.sha256 && final.bytes === probe.bytes && (!report.audio || report.audio.file.sha256 === probe.sha256 && report.audio.file.bytes === probe.bytes);
22750
+ report.artifact.stableAcrossVerification = stable;
22751
+ if (!stable) report.issues.push({ code: "PRODUCTION_ARTIFACT_CHANGED", severity: "error", message: "The final artifact changed between technical checks or samples; evidence cannot validate the current file." });
22752
+ } catch {
22753
+ report.issues.push({ code: "PRODUCTION_ARTIFACT_UNVERIFIED", severity: "incomplete", message: "The final artifact fingerprint could not be reverified." });
22754
+ }
22755
+ }
22756
+ await contextCheck("validAtEnd");
22757
+ }
22758
+ report.complete = !!report.render?.complete && (!report.audio || report.audio.complete) && report.artifact.stableAcrossVerification === true && report.samples.status === "complete" && !report.issues.length;
22759
+ const failed = report.issues.some((issue) => issue.severity === "error") || report.render?.technicalStatus === "fail" || report.audio?.technicalStatus === "fail";
22760
+ report.technicalStatus = failed ? "fail" : !report.complete ? "incomplete" : report.render?.technicalStatus === "warning" ? "warning" : "pass";
22761
+ report.technicalPass = report.complete && !failed;
22762
+ const reportPath = await saveProductionReport(root, `${relativeDirectory}/report.json`, report);
22763
+ const reportSha256 = digest2(stableJson(report));
22764
+ return { report, summary: { ...summarizeProductionVerification(report), reportPath, reportSha256 }, reportPath, reportSha256 };
22765
+ }
22766
+
22767
+ // packages/cli/src/production-review-client.ts
22768
+ var productionReviewCommands = ["reviews", "sample", "approve", "change", "feedback", "resolve-feedback", "revoke-sample"];
22769
+ var invalid4 = (message) => {
22770
+ throw new CliError("INVALID_ARGUMENT", message, "Read editorbrain help and references/production-continuity.md.");
22771
+ };
22772
+ var uuid = (value, name) => {
22773
+ if (typeof value !== "string" || !/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/.test(value)) invalid4(`${name} must be a revision or feedback UUID.`);
22774
+ return value;
22775
+ };
22776
+ var hash5 = (value, name) => {
22777
+ if (typeof value !== "string" || !/^[a-f0-9]{64}$/.test(value)) invalid4(`${name} must be an exact SHA-256 hash.`);
22778
+ return value;
22779
+ };
22780
+ var text8 = (value, name, max, optional = false) => {
22781
+ if (optional && (value === void 0 || value === null)) return "";
22782
+ if (typeof value !== "string" || value.length > max || !optional && !value.trim()) invalid4(`${name} must contain ${optional ? "0" : "1"}\u2013${max} characters.`);
22783
+ return value.trim();
22784
+ };
22785
+ function reviewBody(value, command) {
22786
+ const fields = command === "sample" ? ["assetId", "contextVersion", "title", "notes", "parentRevisionId", "revisionId"] : ["resultRevisionId", "contextVersion", "resultNote", "preservedElements"];
22787
+ if (!value || typeof value !== "object" || Array.isArray(value) || Object.keys(value).some((key) => !fields.includes(key))) invalid4("Use only documented review fields; project and feedback IDs come from command flags, never the JSON body.");
22788
+ const body = value;
22789
+ hash5(body.contextVersion, "contextVersion");
22790
+ if (command === "sample") {
22791
+ hash5(body.assetId, "assetId");
22792
+ text8(body.title, "title", 120);
22793
+ text8(body.notes, "notes", 2e3, true);
22794
+ for (const key of ["parentRevisionId", "revisionId"]) if (body[key] !== void 0) uuid(body[key], key);
22795
+ } else {
22796
+ uuid(body.resultRevisionId, "resultRevisionId");
22797
+ text8(body.resultNote, "resultNote", 1500);
22798
+ text8(body.preservedElements, "preservedElements", 2e3);
22799
+ }
22800
+ if (Buffer.byteLength(JSON.stringify(body)) > 8192) invalid4("Review metadata exceeds the 8 KiB service limit.");
22801
+ return body;
22802
+ }
22803
+ async function productionReviewCommand(client, root, command, flags) {
22804
+ if (!productionReviewCommands.includes(command)) invalid4("Unknown production review command.");
22805
+ const required = (name) => {
22806
+ const value = flags[name];
22807
+ if (typeof value !== "string" || !value) throw new CliError("INVALID_ARGUMENT", `--${name} is required.`, "Read editorbrain help.");
22808
+ return value;
22809
+ };
22810
+ const allowed = {
22811
+ reviews: ["after"],
22812
+ feedback: ["status", "revision", "after"],
22813
+ sample: ["file"],
22814
+ approve: ["revision", "context-version", "user-confirmed"],
22815
+ change: ["revision", "context-version", "text", "at", "id"],
22816
+ "resolve-feedback": ["id", "file"],
22817
+ "revoke-sample": ["revision"]
22818
+ };
22819
+ for (const name of Object.keys(flags)) if (!["project", "dir", "url", ...allowed[command] ?? []].includes(name)) throw new CliError("INVALID_ARGUMENT", `--${name} is not supported for production ${command}.`, "Read editorbrain help.");
22820
+ const projectId = required("project");
22821
+ assertIdentifier(projectId);
22822
+ const base = `/v1/projects/${encodeURIComponent(projectId)}`;
22823
+ const revision = () => encodeURIComponent(uuid(required("revision"), "--revision"));
22824
+ const contextVersion = () => hash5(required("context-version"), "--context-version");
22825
+ const after = flags.after === void 0 ? void 0 : uuid(required("after"), "--after");
22826
+ if (command === "reviews") return client.request(`${base}/production-reviews${after ? "?" + new URLSearchParams({ after }) : ""}`);
22827
+ if (command === "feedback") {
22828
+ const query = new URLSearchParams();
22829
+ if (flags.status !== void 0) {
22830
+ const status = required("status");
22831
+ if (!["pending", "resolved"].includes(status)) invalid4("--status must be pending or resolved.");
22832
+ query.set("status", status);
22833
+ }
22834
+ if (flags.revision !== void 0) query.set("revisionId", uuid(required("revision"), "--revision"));
22835
+ if (after) query.set("after", after);
22836
+ return client.request(`${base}/production-feedback?${query}`);
22837
+ }
22838
+ if (command === "sample" || command === "resolve-feedback") {
22839
+ const feedbackId = command === "resolve-feedback" ? uuid(required("id"), "--id") : void 0;
22840
+ const body = reviewBody(await readProductionJson(root, required("file")), command);
22841
+ return client.request(command === "sample" ? `${base}/production-reviews` : `${base}/production-feedback/${encodeURIComponent(feedbackId)}/resolve`, { method: "POST", body });
22842
+ }
22843
+ if (command === "approve") {
22844
+ if (flags["user-confirmed"] !== true) throw new CliError("USER_CONFIRMATION_REQUIRED", "Record approval only after the user accepted this exact sample.", "Use --user-confirmed to record existing explicit approval, never to bypass a requested review.");
22845
+ return client.request(`${base}/production-reviews/${revision()}/approve`, { method: "POST", body: { contextVersion: contextVersion(), userConfirmed: true } });
22846
+ }
22847
+ if (command === "revoke-sample") return client.request(`${base}/production-reviews/${revision()}`, { method: "DELETE" });
22848
+ if (command === "change") {
22849
+ const times = flags.at === void 0 ? [] : required("at").split(",");
22850
+ if (times.length > 2 || times.some((t) => !/^(?:\d+(?:\.\d*)?|\.\d+)$/.test(t) || !Number.isFinite(Number(t)) || Number(t) > 36e3) || times.length === 2 && Number(times[1]) < Number(times[0])) throw new CliError("INVALID_ARGUMENT", "--at accepts one second or a start,end range between 0 and 36000 seconds.", "Example: --at 12,15.5. Omit it for the whole sample.");
22851
+ return client.request(`${base}/production-reviews/${revision()}/changes`, { method: "POST", body: { contextVersion: contextVersion(), text: text8(required("text"), "--text", 1500), target: times.length ? { startMs: Math.round(Number(times[0]) * 1e3), ...times.length === 2 ? { endMs: Math.round(Number(times[1]) * 1e3) } : {} } : {}, ...flags.id !== void 0 ? { feedbackId: uuid(required("id"), "--id") } : {} } });
22852
+ }
22853
+ throw new CliError("INVALID_ARGUMENT", "Unknown production review command.", "Read editorbrain help.");
22854
+ }
22855
+
22180
22856
  // packages/cli/src/index.ts
22181
22857
  var HELP = `EditorBrain \u2014 portable editorial context for AI video agents
22182
22858
 
@@ -22184,6 +22860,7 @@ editorbrain version
22184
22860
  editorbrain setup --dir folder --agent codex|claude|both [--project id] [--workspace id] [--no-browser] [--skip-mcp]
22185
22861
  editorbrain doctor [--dir folder] [--agent codex|claude|both] [--offline]
22186
22862
  editorbrain init [--dir folder] [--brand id] [--style id] [--project id] [--topic text] [--goal text] [--agent codex|claude|both]
22863
+ editorbrain onboarding guide
22187
22864
  editorbrain onboarding create --brand ID [--project ID] [--dir folder] [--out .editorbrain-local/onboarding/new]
22188
22865
  editorbrain onboarding serve --session path [--dir folder] [--timeout seconds] [--no-browser]
22189
22866
  editorbrain onboarding apply --session path --file answers.json [--dir folder] [--dry-run]
@@ -22216,6 +22893,13 @@ editorbrain review list --project ID
22216
22893
  editorbrain review create --project ID --asset SHA --title TEXT --kind sample|render --days 7 --share-authorized
22217
22894
  editorbrain review revoke ID
22218
22895
  editorbrain workflow check [--project id] [--previous-version hash] [--previous-context-version hash]
22896
+ editorbrain studio prepare --file source.mp4 --out .editorbrain-local/studio/audio.wav [--audio-stream N]
22897
+ editorbrain studio catalog
22898
+ editorbrain billing status
22899
+ editorbrain studio quote --file request.json [--dir folder]
22900
+ editorbrain studio run QUOTE_ID --max-credits N --user-confirmed
22901
+ editorbrain studio get JOB_ID
22902
+ editorbrain studio jobs
22219
22903
  editorbrain integrations catalog
22220
22904
  editorbrain integrations list [--details]
22221
22905
  editorbrain integrations set <provider-id> --file settings.json --expected-revision N
@@ -22231,6 +22915,10 @@ editorbrain styles create <style-id> [--name name] [--dir folder]
22231
22915
  editorbrain styles check <style-id> [--dir folder]
22232
22916
  editorbrain media styles [--category image|motion]
22233
22917
  editorbrain media style <style-id> [--style-version 1.0.0]
22918
+ editorbrain sfx index --input folder --bank ID [--dir folder]
22919
+ editorbrain sfx search --project ID --query text [--bank ID] [--limit 6] [--scene-role hook|body|cta] [--local --dir folder --include-unverified]
22920
+ editorbrain sfx get SOUND_ID --project ID --bank ID --catalog-sha256 HASH [--scene-role hook|body|cta] [--local --dir folder --include-unverified]
22921
+ editorbrain sfx check --project ID --plan .editorbrain-local/sfx-plan.json [--dir folder]
22234
22922
  editorbrain motion scenes [query] [--limit 6] [--after cursor]
22235
22923
  editorbrain motion scene <scene-id> [--scene-version <version>]
22236
22924
  editorbrain motion preview <scene-id> --scene-version <version> --ratio wide|square|tall [--preview-kind video|poster|first|last] [--preset <name>]
@@ -22241,6 +22929,14 @@ editorbrain motion prepare --file scene.json --project ID --out .editorbrain-loc
22241
22929
  editorbrain media inventory --input file-or-folder [--max-files 200] [--dir folder] [--out .editorbrain-local/report.json] [--details]
22242
22930
  editorbrain media sample --file source --at 0,5,12 --out .editorbrain-local/frames [--dir folder] [--details]
22243
22931
  editorbrain production check --file render [--profile local-profile.json] [--dir folder] [--out .editorbrain-local/report.json] [--details]
22932
+ editorbrain production verify --project ID --file final.mp4 [--duration seconds] [--at 0,5,12] [--dir folder] [--out .editorbrain-local/new-check] [--offline] [--details]
22933
+ editorbrain production reviews --project ID [--after UUID]
22934
+ editorbrain production sample --project ID --file sample.json [--dir folder]
22935
+ editorbrain production approve --project ID --revision ID --context-version HASH --user-confirmed
22936
+ editorbrain production change --project ID --revision ID --context-version HASH --text TEXT [--at start,end] [--id UUID]
22937
+ editorbrain production feedback --project ID [--revision ID] [--status pending|resolved] [--after UUID]
22938
+ editorbrain production resolve-feedback --project ID --id UUID --file correction.json [--dir folder]
22939
+ editorbrain production revoke-sample --project ID --revision ID
22244
22940
  editorbrain media search --query text [--provider all|wikimedia|nasa|loc|internet-archive] [--limit 10] [--licenses public-domain,cc0,cc-by] [--include-unknown] [--out .editorbrain-local/sources.json] [--dir folder] [--details]
22245
22941
  editorbrain production audio-check --file final.mp4 [--profile local-profile.json] [--dir folder] [--out .editorbrain-local/audio.json] [--details]
22246
22942
  editorbrain production evidence-check --inventory local-inventory.json --manifest local-evidence.json [--dir folder] [--out .editorbrain-local/evidence.json] [--details]
@@ -22249,7 +22945,10 @@ editorbrain production plan-check --inventory local-inventory.json --plan local-
22249
22945
  editorbrain styles recommend <project-id> [--assets tag,tag] [--capabilities tag,tag] [--style pack-id] [--duration seconds]
22250
22946
  editorbrain direction draft <project-id> --style pack-id [--assets tag,tag] [--capabilities tag,tag] [--duration seconds] [--out new-folder]
22251
22947
  editorbrain assets list [--limit 200] [--cursor opaque-cursor]
22252
- editorbrain assets upload <file> [--mime supported-media-type]
22948
+ editorbrain media preview --file source.mp4 --out .editorbrain-local/previews/review.mp4 [--quality 720|1080] [--audio-stream N]
22949
+ editorbrain storage status
22950
+ editorbrain storage cleanup --ids sha256,sha256 --confirm
22951
+ editorbrain assets upload <file> [--mime supported-media-type] [--purpose original|preview|final|temporary] [--source-sha256 hash]
22253
22952
  editorbrain assets download <sha256> --out file
22254
22953
  editorbrain keys list
22255
22954
  editorbrain keys create --label name [--role reader|editor|owner]
@@ -22265,14 +22964,16 @@ Motion list/get discover reusable components; prepare creates an editable HTML s
22265
22964
  Media inspection/production checks run locally without installation or media upload. Media search sends only your explicit query to public archive APIs; it does not replace the agent's web image/video search or verify rights.
22266
22965
  Integrations store personal preferences, not credentials. Declared access is checked by your agent before use.
22267
22966
  Production output is compact by default; --details expands it. --out saves full private reports without overwriting.
22967
+ Production verify uses local project requirements and rechecks the web context when logged in; --offline explicitly checks local documents only. Media stays local. Technical success is not audiovisual approval.
22968
+ Production reviews/sample/feedback preserve exact revisions privately in the workspace. Approval requires existing user confirmation. They do not render, change brand preferences or create public sharing links.
22268
22969
  Keys create stores the returned secret in the private user config; stdout has no key.
22269
22970
  Output is JSON except help; login progress/code goes to stderr. --url overrides the configured endpoint.
22270
22971
  `;
22271
22972
  function parseArgs(argv) {
22272
22973
  const positional = [];
22273
22974
  const flags = {};
22274
- const booleans = /* @__PURE__ */ new Set(["help", "version", "offline", "dry-run", "key-stdin", "invite-stdin", "no-browser", "skip-mcp", "all", "share-authorized", "details", "include-unknown", "reviewed"]);
22275
- const known = /* @__PURE__ */ new Set(["manifest", "query", "provider", "licenses", "input", "max-files", "at", "profile", "inventory", "plan", "dir", "brand", "style", "project", "topic", "goal", "name", "adapter", "out", "url", "document", "mime", "label", "role", "limit", "cursor", "timeout", "assets", "capabilities", "duration", "agent", "workspace", "prefix", "sha256", "kind", "channel", "epistemic-status", "event-offset", "relation-offset", "script", "video", "asset", "requirements", "mode", "review", "previous-version", "previous-context-version", "category", "style-version", "file", "revision", "expected-revision", "before", "scope", "id", "text", "conditions", "status", "index", "title", "days", ...booleans]);
22975
+ const booleans = /* @__PURE__ */ new Set(["local", "include-unverified", "help", "version", "offline", "dry-run", "key-stdin", "invite-stdin", "no-browser", "skip-mcp", "all", "share-authorized", "details", "include-unknown", "reviewed", "user-confirmed", "confirm"]);
22976
+ const known = /* @__PURE__ */ new Set(["bank", "scene-role", "catalog-sha256", "quality", "purpose", "source-sha256", "ids", "max-credits", "audio-stream", "manifest", "query", "provider", "licenses", "input", "max-files", "at", "profile", "inventory", "plan", "dir", "brand", "style", "project", "topic", "goal", "name", "adapter", "out", "url", "document", "mime", "label", "role", "limit", "cursor", "timeout", "assets", "capabilities", "duration", "agent", "workspace", "prefix", "sha256", "kind", "channel", "epistemic-status", "event-offset", "relation-offset", "script", "video", "asset", "requirements", "mode", "review", "previous-version", "previous-context-version", "category", "style-version", "file", "revision", "expected-revision", "before", "scope", "id", "text", "conditions", "status", "index", "title", "days", ...booleans]);
22276
22977
  known.add("session");
22277
22978
  known.add("run");
22278
22979
  known.add("component-version");
@@ -22281,6 +22982,7 @@ function parseArgs(argv) {
22281
22982
  known.add("ratio");
22282
22983
  known.add("preview-kind");
22283
22984
  known.add("preset");
22985
+ known.add("context-version");
22284
22986
  for (let i = 0; i < argv.length; i++) {
22285
22987
  const arg = argv[i];
22286
22988
  if (!arg.startsWith("--")) {
@@ -22306,16 +23008,16 @@ function output(value) {
22306
23008
  process.stdout.write(stableJson(value));
22307
23009
  }
22308
23010
  async function configRoot(create = false) {
22309
- let path = process.env.EDITORBRAIN_CONFIG_DIR ?? process.env.EDITORHARNESS_CONFIG_DIR ?? join4(homedir(), ".config", "editorbrain");
23011
+ let path = process.env.EDITORBRAIN_CONFIG_DIR ?? process.env.EDITORHARNESS_CONFIG_DIR ?? join8(homedir(), ".config", "editorbrain");
22310
23012
  if (!create && !process.env.EDITORBRAIN_CONFIG_DIR && !process.env.EDITORHARNESS_CONFIG_DIR) {
22311
23013
  try {
22312
- await lstat10(path);
23014
+ await lstat11(path);
22313
23015
  } catch (error) {
22314
23016
  if (error.code !== "ENOENT") throw error;
22315
- path = join4(homedir(), ".config", "editorharness");
23017
+ path = join8(homedir(), ".config", "editorharness");
22316
23018
  }
22317
23019
  }
22318
- if (create) await mkdir7(path, { recursive: true, mode: 448 });
23020
+ if (create) await mkdir8(path, { recursive: true, mode: 448 });
22319
23021
  return ensureRoot(path);
22320
23022
  }
22321
23023
  async function loadConfig() {
@@ -22332,7 +23034,7 @@ async function loadConfig() {
22332
23034
  async function saveConfig(config2) {
22333
23035
  const root = await configRoot(true);
22334
23036
  await atomicWrite(root, "config.json", stableJson(config2), { document: false, mode: 384 });
22335
- await chmod(join4(root, "config.json"), 384);
23037
+ await chmod2(join8(root, "config.json"), 384);
22336
23038
  }
22337
23039
  async function config(args, requireKey = true) {
22338
23040
  const stored = await loadConfig();
@@ -22360,7 +23062,7 @@ async function init(args) {
22360
23062
  for (const path of Object.keys(files)) {
22361
23063
  const target = await checkedPath(root, path);
22362
23064
  try {
22363
- await lstat10(target);
23065
+ await lstat11(target);
22364
23066
  throw new Error(`Refusing to replace existing file: ${path}`);
22365
23067
  } catch (error) {
22366
23068
  if (error.code !== "ENOENT") throw error;
@@ -22372,13 +23074,13 @@ async function init(args) {
22372
23074
  for (const path of directories) {
22373
23075
  const target = await checkedPath(root, path, false);
22374
23076
  try {
22375
- if (!(await lstat10(target)).isDirectory()) throw new Error(`Not a directory: ${path}`);
23077
+ if (!(await lstat11(target)).isDirectory()) throw new Error(`Not a directory: ${path}`);
22376
23078
  } catch (error) {
22377
23079
  if (error.code !== "ENOENT") throw error;
22378
23080
  }
22379
23081
  }
22380
23082
  for (const [path, content] of Object.entries(files)) await atomicWrite(root, path, content, { overwrite: false });
22381
- for (const path of directories) await mkdir7(await checkedPath(root, path, false), { recursive: true });
23083
+ for (const path of directories) await mkdir8(await checkedPath(root, path, false), { recursive: true });
22382
23084
  const guidance = await installGuidance();
22383
23085
  const entryPoints = guidance.readInstructions.filter((item) => item.entryPointStatus === "created").map((item) => item.entryPoint);
22384
23086
  output({ root, created: Object.keys(files), entryPoints, guidance, next: `Edit brand tokens and brief, then run editorbrain resolve ${project}` });
@@ -22433,7 +23135,7 @@ async function main() {
22433
23135
  return;
22434
23136
  }
22435
23137
  if (args.flags["dry-run"] && !["push", "pull"].includes(command) && !(command === "onboarding" && ["apply", "sync"].includes(subcommand))) throw new CliError("INVALID_ARGUMENT", "--dry-run is supported by push, pull and onboarding apply/sync.", "Read editorbrain help.");
22436
- if (args.flags.offline && command !== "doctor") throw new CliError("INVALID_ARGUMENT", "--offline is supported only by doctor.", "Read editorbrain help.");
23138
+ if (args.flags.offline && command !== "doctor" && !(command === "production" && subcommand === "verify")) throw new CliError("INVALID_ARGUMENT", "--offline is supported by doctor and production verify.", "Read editorbrain help.");
22437
23139
  if (!command || command === "help" || args.flags.help) {
22438
23140
  process.stdout.write(HELP);
22439
23141
  return;
@@ -22441,9 +23143,13 @@ async function main() {
22441
23143
  if (args.flags.details && !["media", "production", "integrations"].includes(command) && !(command === "search" && subcommand === "brief")) throw new CliError("INVALID_ARGUMENT", "--details is supported only by media, production, integrations list and search brief.", "Read editorbrain help.");
22442
23144
  if (command === "onboarding") {
22443
23145
  const allowed = new Set(subcommand === "create" ? ["brand", "project", "dir", "out"] : subcommand === "serve" ? ["session", "dir", "timeout", "no-browser"] : subcommand === "apply" ? ["session", "file", "dir", "dry-run"] : subcommand === "sync" ? ["session", "dir", "dry-run", "url"] : []);
22444
- if (!["create", "serve", "apply", "sync"].includes(subcommand) || args.positional.length !== 2) throw new Error("Use onboarding create, serve, apply or sync.");
23146
+ if (!["guide", "create", "serve", "apply", "sync"].includes(subcommand) || args.positional.length !== 2) throw new Error("Use onboarding guide, create, serve, apply or sync.");
22445
23147
  for (const flag of Object.keys(args.flags)) if (!allowed.has(flag)) throw new Error(`--${flag} is not supported here.`);
22446
- const directory = resolve7(str(args, "dir") ?? process.cwd());
23148
+ if (subcommand === "guide") {
23149
+ output(getOnboardingGuide());
23150
+ return;
23151
+ }
23152
+ const directory = resolve8(str(args, "dir") ?? process.cwd());
22447
23153
  if (subcommand === "create") {
22448
23154
  const brandId2 = str(args, "brand");
22449
23155
  if (!brandId2) throw new Error("--brand is required.");
@@ -22491,6 +23197,38 @@ async function main() {
22491
23197
  output(await acceptBrainProposal(client, { brandId: brandId2, runId, expectedDesignRevision: brainRevision(revision), reviewed: true }));
22492
23198
  return;
22493
23199
  }
23200
+ if (command === "sfx") {
23201
+ const allowed = new Set(subcommand === "index" ? ["input", "bank", "dir"] : subcommand === "check" ? ["project", "plan", "dir"] : subcommand === "search" ? ["project", "query", "bank", "limit", "scene-role", "local", "dir", "include-unverified", "url"] : subcommand === "get" ? ["project", "bank", "catalog-sha256", "scene-role", "local", "dir", "include-unverified", "url"] : []);
23202
+ if (!["index", "search", "get", "check"].includes(subcommand)) throw new Error("Use sfx index, search, get or check.");
23203
+ for (const key of Object.keys(args.flags)) if (!allowed.has(key)) throw new Error(`--${key} is not supported for sfx ${subcommand}.`);
23204
+ if (args.positional.length !== (subcommand === "get" ? 3 : 2)) throw new Error("Unexpected SFX arguments.");
23205
+ const directory = str(args, "dir") ?? process.cwd(), bankId = str(args, "bank"), projectId = str(args, "project");
23206
+ if (subcommand === "index") {
23207
+ const input = str(args, "input");
23208
+ if (!input || !bankId) throw new Error("Provide --input and --bank.");
23209
+ output(await indexSfx(directory, input, bankId));
23210
+ return;
23211
+ }
23212
+ if (!projectId) throw new Error("--project is required to resolve audio preferences.");
23213
+ if (subcommand === "check") {
23214
+ const plan = str(args, "plan");
23215
+ if (!plan) throw new Error("--plan is required.");
23216
+ const report = await checkLocalSfx(directory, projectId, plan);
23217
+ output(report);
23218
+ if (!report.passed) process.exitCode = 1;
23219
+ return;
23220
+ }
23221
+ const sceneRole = str(args, "scene-role") ?? "body", query = str(args, "query") ?? "", limit = str(args, "limit");
23222
+ if (!["hook", "body", "cta"].includes(sceneRole) || limit !== void 0 && (!/^\d+$/.test(limit) || Number(limit) < 1 || Number(limit) > 20)) throw new Error("Invalid scene role or limit.");
23223
+ if (subcommand === "get" && (!argument || !str(args, "catalog-sha256"))) throw new Error("Provide sound ID and --catalog-sha256 from search.");
23224
+ if (args.flags.local) {
23225
+ output(await localSfx({ directory, projectId, bankId, query, limit: limit ? Number(limit) : void 0, sceneRole, includeUnverified: !!args.flags["include-unverified"], soundId: subcommand === "get" ? argument : void 0, catalogSha256: str(args, "catalog-sha256") }));
23226
+ return;
23227
+ }
23228
+ if (args.flags["include-unverified"] || args.flags.dir) throw new Error("--dir and --include-unverified require --local.");
23229
+ output(await remoteSfx(new Client(await config(args)), subcommand, { projectId, query, sceneRole, ...bankId ? { bankId } : {}, ...limit ? { limit } : {}, ...subcommand === "get" ? { soundId: argument, catalogSha256: str(args, "catalog-sha256") } : {} }));
23230
+ return;
23231
+ }
22494
23232
  if (command === "motion" && ["scenes", "scene", "source", "preview"].includes(subcommand)) {
22495
23233
  const allowed = new Set(subcommand === "scenes" ? ["limit", "after"] : subcommand === "scene" ? ["scene-version"] : subcommand === "preview" ? ["scene-version", "ratio", "preview-kind", "preset", "url"] : ["scene-version", "out", "dir", "url"]);
22496
23234
  for (const key of Object.keys(args.flags)) if (!allowed.has(key)) throw new CliError("INVALID_ARGUMENT", `--${key} is not supported here.`, "Read editorbrain help.");
@@ -22542,6 +23280,48 @@ async function main() {
22542
23280
  }
22543
23281
  return;
22544
23282
  }
23283
+ if (command === "billing") {
23284
+ if (subcommand !== "status" || argument || Object.keys(args.flags).some((k) => !["url", "dir"].includes(k))) throw new CliError("INVALID_ARGUMENT", "Use editorbrain billing status.", "Manage payments at https://editorbrain.com/app/billing.");
23285
+ output(await new Client(await config(args)).request("/v1/billing/status"));
23286
+ return;
23287
+ }
23288
+ if (command === "media" && subcommand === "preview") {
23289
+ const allowed = /* @__PURE__ */ new Set(["file", "out", "quality", "audio-stream", "dir"]);
23290
+ if (argument || Object.keys(args.flags).some((f) => !allowed.has(f)) || !str(args, "file") || !str(args, "out")) throw new CliError("INVALID_ARGUMENT", "Use media preview --file source --out .editorbrain-local/previews/review.mp4.", "This command creates a local copy only.");
23291
+ const quality = Number(str(args, "quality") ?? 720), audio = str(args, "audio-stream");
23292
+ if (![720, 1080].includes(quality) || audio !== void 0 && (!Number.isInteger(Number(audio)) || Number(audio) < 0)) throw new CliError("INVALID_ARGUMENT", "Invalid quality or audio stream.", "Use quality 720 or 1080.");
23293
+ const root2 = await ensureRoot(str(args, "dir") ?? process.cwd());
23294
+ output(await prepareMediaPreview(root2, resolve8(root2, str(args, "file")), str(args, "out"), quality, audio === void 0 ? void 0 : Number(audio)));
23295
+ return;
23296
+ }
23297
+ if (command === "storage") {
23298
+ const allowed = new Set(subcommand === "status" ? ["url", "dir"] : ["url", "dir", "ids", "confirm"]);
23299
+ if (argument || Object.keys(args.flags).some((f) => !allowed.has(f))) throw new CliError("INVALID_ARGUMENT", "Unsupported storage argument.", "Use storage status or storage cleanup --ids sha256,sha256 --confirm.");
23300
+ const client = new Client(await config(args));
23301
+ if (subcommand === "status") {
23302
+ output(await client.request("/v1/storage"));
23303
+ return;
23304
+ }
23305
+ if (subcommand === "cleanup" && args.flags.confirm === true && str(args, "ids")) {
23306
+ output(await client.request("/v1/storage/cleanup", { method: "POST", body: { assetIds: str(args, "ids").split(","), userConfirmed: true } }));
23307
+ return;
23308
+ }
23309
+ throw new CliError("INVALID_ARGUMENT", "Use storage status, or storage cleanup --ids sha256,sha256 --confirm.", "Review the exact unreferenced previews/temporaries first; deletion cannot be undone.");
23310
+ }
23311
+ if (command === "studio" && subcommand === "prepare") {
23312
+ const allowed = /* @__PURE__ */ new Set(["file", "out", "audio-stream", "dir"]);
23313
+ if (argument || Object.keys(args.flags).some((flag) => !allowed.has(flag)) || !str(args, "file") || !str(args, "out")) throw new CliError("INVALID_ARGUMENT", "Use studio prepare --file source --out .editorbrain-local/audio.wav.", "Preparation is local and does not need login.");
23314
+ const stream = str(args, "audio-stream");
23315
+ if (stream !== void 0 && !/^\d+$/.test(stream)) throw new CliError("INVALID_ARGUMENT", "--audio-stream must be an integer.", "Use media inventory to inspect available tracks.");
23316
+ const root2 = resolve8(str(args, "dir") ?? process.cwd());
23317
+ output(await prepareStudioAudio(root2, resolve8(root2, str(args, "file")), str(args, "out"), stream === void 0 ? void 0 : Number(stream)));
23318
+ return;
23319
+ }
23320
+ if (command === "studio") {
23321
+ if (args.positional.length > 3) throw new CliError("INVALID_ARGUMENT", "Too many studio arguments.", "Read editorbrain help.");
23322
+ output(await studioCommand(new Client(await config(args)), resolve8(str(args, "dir") ?? process.cwd()), subcommand, argument, args.flags));
23323
+ return;
23324
+ }
22545
23325
  if (command === "integrations") {
22546
23326
  const allowed = new Set(subcommand === "catalog" ? [] : subcommand === "list" ? ["url", "details"] : subcommand === "set" ? ["url", "file", "expected-revision", "dir"] : []);
22547
23327
  if (!["catalog", "list", "set"].includes(subcommand) || args.positional.length > (subcommand === "set" ? 3 : 2)) throw new CliError("INVALID_ARGUMENT", "Use integrations catalog, list or set <provider-id>.", "Read editorbrain help.");
@@ -22554,7 +23334,7 @@ async function main() {
22554
23334
  const file = str(args, "file");
22555
23335
  if (!argument || !file) throw new CliError("INVALID_ARGUMENT", "integrations set requires a provider ID and --file.", "Use integrations list first, then supply --expected-revision.");
22556
23336
  const revision = integrationRevision(str(args, "expected-revision"));
22557
- const settings = await readIntegrationSettings(resolve7(str(args, "dir") ?? process.cwd(), file));
23337
+ const settings = await readIntegrationSettings(resolve8(str(args, "dir") ?? process.cwd(), file));
22558
23338
  output(await setIntegration(new Client(await config(args)), argument, settings, revision));
22559
23339
  } else output(await listIntegrations(new Client(await config(args)), !!args.flags.details));
22560
23340
  return;
@@ -22574,6 +23354,34 @@ async function main() {
22574
23354
  }
22575
23355
  return;
22576
23356
  }
23357
+ if (command === "production" && productionReviewCommands.includes(subcommand)) {
23358
+ if (argument) throw new CliError("INVALID_ARGUMENT", "Use named flags for production reviews.", "Read editorbrain help.");
23359
+ output(await productionReviewCommand(new Client(await config(args)), await ensureRoot(str(args, "dir") ?? process.cwd()), subcommand, args.flags));
23360
+ return;
23361
+ }
23362
+ if (command === "production" && subcommand === "verify") {
23363
+ if (argument) throw new CliError("INVALID_ARGUMENT", "Use named flags for production verify.", "Read editorbrain help.");
23364
+ const allowed = /* @__PURE__ */ new Set(["project", "file", "duration", "at", "dir", "out", "offline", "details", "url"]);
23365
+ for (const flag of Object.keys(args.flags)) if (!allowed.has(flag)) throw new CliError("INVALID_ARGUMENT", `--${flag} is not supported for production verify.`, "Read editorbrain help.");
23366
+ const projectId = str(args, "project"), file = str(args, "file");
23367
+ if (!projectId || !file) throw new CliError("INVALID_ARGUMENT", "--project and --file are required.", "Read editorbrain help.");
23368
+ const times = str(args, "at")?.split(",");
23369
+ if (times?.some((time) => !/^(?:\d+(?:\.\d*)?|\.\d+)$/.test(time))) throw new CliError("INVALID_ARGUMENT", "--at requires comma-separated nonnegative seconds.", "Example: --at 0,5.5,12");
23370
+ if (times && (times.length > 6 || times.some((time) => !Number.isFinite(Number(time)) || Number(time) > 86400))) throw new CliError("INVALID_ARGUMENT", "--at accepts up to six times within 86400 seconds.", "Choose only the moments needed for review.");
23371
+ const duration = str(args, "duration");
23372
+ if (duration !== void 0 && (!duration.trim() || !Number.isFinite(Number(duration)) || Number(duration) <= 0 || Number(duration) > 864e4)) throw new CliError("INVALID_ARGUMENT", "--duration must be a finite positive duration in seconds.", "Use the required final duration, not an estimate.");
23373
+ const root2 = await ensureRoot(str(args, "dir") ?? process.cwd());
23374
+ const credentials = args.flags.offline ? void 0 : await config(args, false);
23375
+ const client = credentials?.apiKey ? new Client(credentials) : void 0;
23376
+ await withWorkspaceLock(root2, async () => {
23377
+ const context = await prepareProductionContext(root2, projectId, client);
23378
+ const result2 = await verifyProduction({ root: root2, file, ...context, ...str(args, "out") ? { outDirectory: str(args, "out") } : {}, ...str(args, "duration") !== void 0 ? { expectedDurationSeconds: Number(str(args, "duration")) } : {}, ...times ? { sampleTimesSeconds: times.map(Number) } : {} });
23379
+ output({ ...args.flags.details ? result2.report : result2.summary, contextScope: context.contextScope, reportPath: result2.reportPath, reportSha256: result2.reportSha256 });
23380
+ if (result2.report.technicalStatus === "fail") process.exitCode = 1;
23381
+ else if (!result2.report.complete) process.exitCode = 2;
23382
+ }, { migrateLegacy: false });
23383
+ return;
23384
+ }
22577
23385
  if (command === "media" || command === "production") {
22578
23386
  const required = (name) => {
22579
23387
  const value = str(args, name);
@@ -22590,17 +23398,17 @@ async function main() {
22590
23398
  let report;
22591
23399
  if (command === "media" && subcommand === "inventory") {
22592
23400
  const limit = str(args, "max-files");
22593
- report = await inventoryMedia({ input: resolve7(root2, required("input")), ...limit ? { maxFiles: Number(limit) } : {} });
23401
+ report = await inventoryMedia({ input: resolve8(root2, required("input")), ...limit ? { maxFiles: Number(limit) } : {} });
22594
23402
  if (!report.complete) process.exitCode = 2;
22595
23403
  } else if (command === "media" && subcommand === "sample") {
22596
23404
  const times = required("at").split(",");
22597
23405
  if (times.some((t) => !t.trim() || !/^(?:\d+(?:\.\d*)?|\.\d+)$/.test(t.trim()))) throw new CliError("INVALID_ARGUMENT", "--at requires comma-separated nonnegative seconds.", "For example: --at 0,5.5,12");
22598
23406
  const directory = await prepareSampleDirectory(root2, required("out"));
22599
- report = await sampleMedia({ file: resolve7(root2, required("file")), times: times.map(Number), outputDirectory: directory });
23407
+ report = await sampleMedia({ file: resolve8(root2, required("file")), times: times.map(Number), outputDirectory: directory });
22600
23408
  } else if (command === "production" && subcommand === "check") {
22601
23409
  const profilePath = str(args, "profile");
22602
23410
  const profile = profilePath ? validateRenderCheckProfile(await readProductionJson(root2, profilePath)) : void 0;
22603
- report = await checkRender({ file: resolve7(root2, required("file")), profile });
23411
+ report = await checkRender({ file: resolve8(root2, required("file")), profile });
22604
23412
  if (report.technicalStatus === "fail") process.exitCode = 1;
22605
23413
  else if (!report.complete) process.exitCode = 2;
22606
23414
  } else if (command === "media" && subcommand === "search") {
@@ -22608,7 +23416,7 @@ async function main() {
22608
23416
  if (!report.complete) process.exitCode = 2;
22609
23417
  } else if (command === "production" && subcommand === "audio-check") {
22610
23418
  const profilePath = str(args, "profile");
22611
- report = await checkAudio({ file: resolve7(root2, required("file")), ...profilePath ? { profile: validateAudioCheckProfile(await readProductionJson(root2, profilePath)) } : {} });
23419
+ report = await checkAudio({ file: resolve8(root2, required("file")), ...profilePath ? { profile: validateAudioCheckProfile(await readProductionJson(root2, profilePath)) } : {} });
22612
23420
  if (report.technicalStatus === "fail") process.exitCode = 1;
22613
23421
  else if (!report.complete) process.exitCode = 2;
22614
23422
  } else if (command === "production" && subcommand === "evidence-check") {
@@ -22716,10 +23524,10 @@ async function main() {
22716
23524
  const destination = str(args, "out");
22717
23525
  if (destination) {
22718
23526
  if (command !== "direction" || !result2.files) throw new Error("--out is supported only for direction draft.");
22719
- const outRoot = await ensureRoot(resolve7(str(args, "dir") ?? process.cwd(), destination), true);
23527
+ const outRoot = await ensureRoot(resolve8(str(args, "dir") ?? process.cwd(), destination), true);
22720
23528
  for (const path of Object.keys(result2.files)) {
22721
23529
  try {
22722
- await lstat10(await checkedPath(outRoot, path, false));
23530
+ await lstat11(await checkedPath(outRoot, path, false));
22723
23531
  throw new Error(`Refusing to replace existing proposal: ${path}`);
22724
23532
  } catch (error) {
22725
23533
  if (error.code !== "ENOENT") throw error;
@@ -22745,7 +23553,7 @@ async function main() {
22745
23553
  if (!/^[a-zA-Z0-9_-]{1,100}$/.test(result2.id) || typeof result2.apiKey !== "string") throw new Error("Invalid key response");
22746
23554
  const file = `keys/${result2.id}.json`;
22747
23555
  await atomicWrite(root2, file, stableJson({ url: client.url, apiKey: result2.apiKey, id: result2.id, label, role }), { document: false, mode: 384, overwrite: false });
22748
- output({ id: result2.id, label, role, credentialFile: join4(root2, file) });
23556
+ output({ id: result2.id, label, role, credentialFile: join8(root2, file) });
22749
23557
  return;
22750
23558
  }
22751
23559
  if (subcommand === "revoke" && argument) {
@@ -22832,7 +23640,7 @@ async function main() {
22832
23640
  return;
22833
23641
  }
22834
23642
  if (subcommand === "upload" && argument) {
22835
- const target = resolve7(str(args, "dir") ?? process.cwd(), argument);
23643
+ const target = resolve8(str(args, "dir") ?? process.cwd(), argument);
22836
23644
  const handle = await open9(target, constants9.O_RDONLY | constants9.O_NOFOLLOW);
22837
23645
  let bytes;
22838
23646
  try {
@@ -22843,24 +23651,27 @@ async function main() {
22843
23651
  await handle.close();
22844
23652
  }
22845
23653
  const types = { ".mp4": "video/mp4", ".mov": "video/quicktime", ".webm": "video/webm", ".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".webp": "image/webp", ".mp3": "audio/mpeg", ".wav": "audio/wav", ".m4a": "audio/mp4", ".ogg": "audio/ogg" };
22846
- const mimeType = str(args, "mime") ?? types[extname4(target).toLowerCase()];
23654
+ const mimeType = str(args, "mime") ?? types[extname5(target).toLowerCase()];
22847
23655
  if (!mimeType) throw new Error("Unsupported extension; specify --mime for a supported media format");
22848
- const hash4 = await sha256(bytes);
22849
- const result2 = await client.media(`/v1/assets/${hash4}?name=${encodeURIComponent(basename2(target))}`, { content: bytes, mimeType });
23656
+ const hash6 = await sha256(bytes);
23657
+ const params = new URLSearchParams({ name: basename3(target) });
23658
+ if (str(args, "purpose")) params.set("purpose", str(args, "purpose"));
23659
+ if (str(args, "source-sha256")) params.set("sourceSha256", str(args, "source-sha256"));
23660
+ const result2 = await client.media(`/v1/assets/${hash6}?${params}`, { content: bytes, mimeType });
22850
23661
  const body = await result2.json();
22851
- if (body.sha256 !== hash4) throw new Error("Upload response hash mismatch");
23662
+ if (body.sha256 !== hash6) throw new Error("Upload response hash mismatch");
22852
23663
  output(body);
22853
23664
  return;
22854
23665
  }
22855
23666
  if (subcommand === "download" && argument && str(args, "out")) {
22856
23667
  if (!/^[a-f0-9]{64}$/.test(argument)) throw new Error("Asset ID must be its SHA-256");
22857
- const destination = resolve7(str(args, "dir") ?? process.cwd(), str(args, "out"));
22858
- const root2 = await ensureRoot(dirname8(destination), true);
22859
- await checkedPath(root2, basename2(destination), false);
23668
+ const destination = resolve8(str(args, "dir") ?? process.cwd(), str(args, "out"));
23669
+ const root2 = await ensureRoot(dirname11(destination), true);
23670
+ await checkedPath(root2, basename3(destination), false);
22860
23671
  const response = await client.media(`/v1/assets/${argument}`);
22861
23672
  const bytes = new Uint8Array(await response.arrayBuffer());
22862
23673
  if (await sha256(bytes) !== argument) throw new Error("Downloaded asset failed SHA-256 verification");
22863
- await atomicWrite(root2, basename2(destination), bytes, { document: false, overwrite: false });
23674
+ await atomicWrite(root2, basename3(destination), bytes, { document: false, overwrite: false });
22864
23675
  output({ path: destination, sha256: argument, bytes: bytes.length });
22865
23676
  return;
22866
23677
  }
@@ -22890,7 +23701,7 @@ async function main() {
22890
23701
  if (command === "search" && subcommand === "brief") {
22891
23702
  const path = str(args, "file");
22892
23703
  if (!path) throw new Error("Provide --file with the editorial brief JSON after inspecting the actual material.");
22893
- const handle = await open9(resolve7(path), constants9.O_RDONLY | constants9.O_NOFOLLOW);
23704
+ const handle = await open9(resolve8(path), constants9.O_RDONLY | constants9.O_NOFOLLOW);
22894
23705
  let brief;
22895
23706
  try {
22896
23707
  const stat = await handle.stat();
@@ -22912,7 +23723,7 @@ async function main() {
22912
23723
  const scriptPath = str(args, "script"), videoPath = str(args, "video");
22913
23724
  let script, assetId = str(args, "asset");
22914
23725
  const readInput = async (path, max) => {
22915
- const handle = await open9(resolve7(path), constants9.O_RDONLY | constants9.O_NOFOLLOW);
23726
+ const handle = await open9(resolve8(path), constants9.O_RDONLY | constants9.O_NOFOLLOW);
22916
23727
  try {
22917
23728
  const stat = await handle.stat();
22918
23729
  if (!stat.isFile() || stat.size < 1 || stat.size > max) throw new Error(`Input must be a regular file of at most ${max} bytes.`);
@@ -22932,10 +23743,10 @@ async function main() {
22932
23743
  if (assetId && !/^[a-f0-9]{64}$/.test(assetId)) throw new Error("--asset requires an owned production asset SHA-256.");
22933
23744
  if (videoPath) {
22934
23745
  const bytes = await readInput(videoPath, 12 * 1024 * 1024);
22935
- const mimeType = { ".mp4": "video/mp4", ".mov": "video/quicktime", ".webm": "video/webm" }[extname4(videoPath).toLowerCase()];
23746
+ const mimeType = { ".mp4": "video/mp4", ".mov": "video/quicktime", ".webm": "video/webm" }[extname5(videoPath).toLowerCase()];
22936
23747
  if (!mimeType) throw new Error("Use MP4, MOV or WebM.");
22937
23748
  assetId = await sha256(bytes);
22938
- const uploaded = await client.media(`/v1/assets/${assetId}?name=${encodeURIComponent(basename2(videoPath))}`, { content: bytes, mimeType });
23749
+ const uploaded = await client.media(`/v1/assets/${assetId}?name=${encodeURIComponent(basename3(videoPath))}`, { content: bytes, mimeType });
22939
23750
  if ((await uploaded.json()).sha256 !== assetId) throw new Error("Upload hash mismatch.");
22940
23751
  }
22941
23752
  if (!script && !assetId && !str(args, "project")) throw new Error("Provide --script, --video, --asset or --project with a saved script.");
@@ -22961,20 +23772,20 @@ async function main() {
22961
23772
  }
22962
23773
  if (subcommand === "get" && argument) {
22963
23774
  const document2 = str(args, "document");
22964
- const hash4 = str(args, "sha256");
22965
- if (hash4 && !document2) throw new Error("--sha256 requires --document for library get.");
22966
- const result2 = await client.request(`/v1/library/${encodeURIComponent(argument)}${document2 ? `/documents?${new URLSearchParams({ path: document2, ...hash4 ? { sha256: hash4 } : {} })}` : ""}`);
22967
- if (document2 && (typeof result2.content !== "string" || await sha256(result2.content) !== result2.sha256 || hash4 && result2.sha256 !== hash4)) throw new CliError("HASH_MISMATCH", "Reference document verification failed.", "Do not use this evidence; retry the pinned version.");
23775
+ const hash6 = str(args, "sha256");
23776
+ if (hash6 && !document2) throw new Error("--sha256 requires --document for library get.");
23777
+ const result2 = await client.request(`/v1/library/${encodeURIComponent(argument)}${document2 ? `/documents?${new URLSearchParams({ path: document2, ...hash6 ? { sha256: hash6 } : {} })}` : ""}`);
23778
+ if (document2 && (typeof result2.content !== "string" || await sha256(result2.content) !== result2.sha256 || hash6 && result2.sha256 !== hash6)) throw new CliError("HASH_MISMATCH", "Reference document verification failed.", "Do not use this evidence; retry the pinned version.");
22968
23779
  output(result2);
22969
23780
  return;
22970
23781
  }
22971
23782
  if (subcommand === "media" && argument && str(args, "out")) {
22972
- const hash4 = str(args, "sha256");
22973
- if (!hash4 || !/^[a-f0-9]{64}$/.test(hash4)) throw new Error("--sha256 must be a media SHA-256 from library get.");
22974
- const destination = resolve7(str(args, "dir") ?? process.cwd(), str(args, "out"));
22975
- const root2 = await ensureRoot(dirname8(destination), true);
22976
- await checkedPath(root2, basename2(destination), false);
22977
- const response = await client.media(`/v1/library/${encodeURIComponent(argument)}/media/${hash4}`);
23783
+ const hash6 = str(args, "sha256");
23784
+ if (!hash6 || !/^[a-f0-9]{64}$/.test(hash6)) throw new Error("--sha256 must be a media SHA-256 from library get.");
23785
+ const destination = resolve8(str(args, "dir") ?? process.cwd(), str(args, "out"));
23786
+ const root2 = await ensureRoot(dirname11(destination), true);
23787
+ await checkedPath(root2, basename3(destination), false);
23788
+ const response = await client.media(`/v1/library/${encodeURIComponent(argument)}/media/${hash6}`);
22978
23789
  const reader = response.body?.getReader();
22979
23790
  if (!reader) throw new Error("Media body missing.");
22980
23791
  const chunks = [];
@@ -22990,9 +23801,9 @@ async function main() {
22990
23801
  chunks.push(value);
22991
23802
  }
22992
23803
  const bytes = Buffer.concat(chunks);
22993
- if (await sha256(bytes) !== hash4) throw new CliError("HASH_MISMATCH", "Reference media verification failed.", "Do not use this file as evidence.");
22994
- await atomicWrite(root2, basename2(destination), bytes, { document: false, overwrite: false });
22995
- output({ videoId: argument, path: destination, sha256: hash4, bytes: size, mimeType: response.headers.get("Content-Type"), reviewed: false });
23804
+ if (await sha256(bytes) !== hash6) throw new CliError("HASH_MISMATCH", "Reference media verification failed.", "Do not use this file as evidence.");
23805
+ await atomicWrite(root2, basename3(destination), bytes, { document: false, overwrite: false });
23806
+ output({ videoId: argument, path: destination, sha256: hash6, bytes: size, mimeType: response.headers.get("Content-Type"), reviewed: false });
22996
23807
  return;
22997
23808
  }
22998
23809
  throw new Error("Use library search, library get, or library media <video-id> --sha256 hash --out file.");
@@ -23031,7 +23842,7 @@ async function main() {
23031
23842
  const adapter = str(args, "adapter") ?? "neutral";
23032
23843
  if (!["neutral", "hyperframes", "remotion"].includes(adapter)) throw new Error("Adapter must be neutral, hyperframes or remotion");
23033
23844
  const generated = exportProject(resolved, adapter);
23034
- const destination = resolve7(root, str(args, "out") ?? `videos/${project}/plans/${adapter}`);
23845
+ const destination = resolve8(root, str(args, "out") ?? `videos/${project}/plans/${adapter}`);
23035
23846
  const outRoot = await ensureRoot(destination, true);
23036
23847
  for (const [path, content] of Object.entries(generated)) await atomicWrite(outRoot, path, content, { document: false });
23037
23848
  output({ adapter, kind: "editorial_handoff", rendered: false, directory: outRoot, files: Object.keys(generated), warnings: resolved.warnings });