editorbrain 0.9.0 → 0.11.0

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,205 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ // packages/core/src/memory.ts
4
+ function memoryPath(scope, id) {
5
+ if (!["brand", "style", "project"].includes(scope) || !/^[a-z0-9][a-z0-9_-]{0,79}$/.test(id)) throw new Error("Invalid memory scope");
6
+ return scope === "project" ? `videos/${id}/preferences.json` : `.editorbrain/${scope === "brand" ? "brands" : "styles"}/${id}/preferences.json`;
7
+ }
8
+ function memoryScope(path) {
9
+ const m = path.match(/^\.editorbrain\/(brands|styles)\/([a-z0-9_-]+)\/preferences\.json$/), p = path.match(/^videos\/([a-z0-9_-]+)\/preferences\.json$/);
10
+ return m ? { scope: m[1] === "brands" ? "brand" : "style", scopeId: m[2] } : p ? { scope: "project", scopeId: p[1] } : null;
11
+ }
12
+ function memoryIssues(path, doc) {
13
+ const scope = memoryScope(path);
14
+ if (!scope) return [];
15
+ if (doc.scope !== scope.scope || doc.scopeId !== scope.scopeId || !Array.isArray(doc.entries) || doc.entries.length > 60) return ["Memory requires matching scope and at most 60 entries"];
16
+ const ids = /* @__PURE__ */ new Set();
17
+ const errors = [];
18
+ for (const e of doc.entries) {
19
+ if (!e || typeof e.id !== "string" || !/^[a-zA-Z0-9_-]{1,100}$/.test(e.id) || ids.has(e.id) || typeof e.text !== "string" || !e.text.trim() || e.text.length > 800 || typeof e.conditions !== "string" || e.conditions.length > 400 || !["suggested", "confirmed", "disabled"].includes(e.status) || typeof e.source !== "string" || e.source.length > 400 || !["agent_reported", "user_saved", "weekly_proposal"].includes(e.provenance) || typeof e.updatedAt !== "string" || !Number.isFinite(Date.parse(e.updatedAt))) errors.push("Invalid or duplicate memory entry");
20
+ ids.add(e?.id);
21
+ }
22
+ return errors;
23
+ }
24
+ function resolveMemory(files, p) {
25
+ const entries = [["style", p.style], ["brand", p.brand], ["project", p.id]].flatMap(([scope, id]) => {
26
+ const path = memoryPath(scope, id);
27
+ if (!files[path]) return [];
28
+ return JSON.parse(files[path]).entries.map((e) => ({ ...e, scope, scopeId: id, path }));
29
+ });
30
+ return { confirmed: entries.filter((e) => e.status === "confirmed"), pending: entries.filter((e) => e.status === "suggested").map(({ id, scope, scopeId }) => ({ id, scope, scopeId })), policy: "Apply only relevant confirmed preferences. Conditions and prose conflicts require agent judgment. Project-specific guidance precedes brand then style; required tokens still govern. Suggestions are not instructions. Provenance is a claim, not proof of human confirmation." };
31
+ }
32
+
33
+ // packages/core/src/style-authoring.ts
34
+ var own = (value, key) => Object.prototype.hasOwnProperty.call(value, key);
35
+ var object = (value) => !!value && typeof value === "object" && !Array.isArray(value);
36
+ var nonempty = (value) => typeof value === "string" && !!value.trim();
37
+ var identifier = /^[a-z0-9][a-z0-9_-]{0,79}$/;
38
+ var digest = /^[a-f0-9]{64}$/;
39
+ var kinds = ["font", "audio", "image", "video", "editable", "script", "other"];
40
+ var forbiddenText = /(?:\b[a-z][a-z0-9+.-]*:\/\/|\b(?:data|file|mailto):|\b(?:authorization|api[_ -]?key|access[_ -]?token|client[_ -]?secret|password)\s*[:=]|\bBearer\s+\S+|-----BEGIN [A-Z ]*PRIVATE KEY-----)/i;
41
+ function safeStyleResourcePath(path) {
42
+ if (typeof path !== "string" || !path || path.length > 400 || path !== path.trim() || /[\\\x00-\x1f\x7f%:?#]/.test(path) || path.startsWith("/") || path.startsWith("~")) throw new Error("Resource path must be a safe workspace-relative path");
43
+ const parts = path.split("/");
44
+ if (parts.some((part) => !part || part === "." || part === ".." || part.length > 100 || /[. ]$/.test(part) || ["__proto__", "prototype", "constructor"].includes(part) || /^(con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\.|$)/i.test(part))) throw new Error("Unsafe or nonportable resource path segment");
45
+ if (parts.some((part) => /^\.env(?:\.|$)/i.test(part) || /^(?:\.?credentials|secrets?|service[-_]account)(?:\.|$)/i.test(part) || [".git", ".ssh", ".aws", ".gnupg", ".sync.json", "id_rsa", "id_ed25519"].includes(part.toLowerCase()))) throw new Error("Credential and private configuration paths cannot be style resources");
46
+ return path;
47
+ }
48
+ function validateStyleResources(value, expectedId) {
49
+ const path = `.editorbrain/styles/${expectedId}/resources.json`;
50
+ const issues = [];
51
+ const add = (message) => issues.push({ path, message });
52
+ const fields = (entry, allowed, label) => {
53
+ if (Object.keys(entry).some((key) => !allowed.includes(key))) add(`${label} has unsupported fields; do not include URLs, credentials or hidden resource sources`);
54
+ };
55
+ const text = (value2, label) => {
56
+ if (!nonempty(value2)) add(`${label} must be nonempty text`);
57
+ else if (forbiddenText.test(value2) || /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/.test(value2)) add(`${label} cannot contain URLs, credentials or control characters`);
58
+ };
59
+ if (!object(value)) return { valid: false, issues: [{ path, message: "Style resources must be a JSON object" }] };
60
+ fields(value, ["schemaVersion", "id", "resources"], "Style resources");
61
+ if (value.schemaVersion !== 1) add("schemaVersion must be 1");
62
+ if (!identifier.test(expectedId) || value.id !== expectedId) add(`Style resource id must match path: ${expectedId}`);
63
+ if (!Array.isArray(value.resources)) add("Style resources requires a resources array");
64
+ else {
65
+ const ids = /* @__PURE__ */ new Set();
66
+ for (const [index, resource] of value.resources.entries()) {
67
+ const label = `Resource ${index + 1}`;
68
+ if (!object(resource)) {
69
+ add(`${label} must be an object`);
70
+ continue;
71
+ }
72
+ fields(resource, ["id", "kind", "purpose", "required", "source"], label);
73
+ if (typeof resource.id !== "string" || !identifier.test(resource.id) || ["constructor", "prototype"].includes(resource.id)) add(`${label} requires a valid identifier`);
74
+ else if (ids.has(resource.id)) add(`Duplicate resource id: ${resource.id}`);
75
+ else ids.add(resource.id);
76
+ if (!kinds.includes(resource.kind)) add(`${label} has an invalid kind`);
77
+ text(resource.purpose, `${label} purpose`);
78
+ if (typeof resource.required !== "boolean") add(`${label} required must be boolean`);
79
+ const source = resource.source;
80
+ if (!object(source)) {
81
+ add(`${label} source must be local, asset or requirement`);
82
+ continue;
83
+ }
84
+ if (source.type === "local") {
85
+ fields(source, ["type", "path", "sha256"], `${label} local source`);
86
+ try {
87
+ safeStyleResourcePath(source.path);
88
+ } catch (error) {
89
+ add(`${label}: ${error.message}`);
90
+ }
91
+ if (source.sha256 !== void 0 && (typeof source.sha256 !== "string" || !digest.test(source.sha256))) add(`${label} sha256 must be a 64-character lowercase hexadecimal digest`);
92
+ } else if (source.type === "asset") {
93
+ fields(source, ["type", "assetId"], `${label} asset source`);
94
+ if (typeof source.assetId !== "string" || !digest.test(source.assetId)) add(`${label} assetId must be a 64-character lowercase hexadecimal digest`);
95
+ } else if (source.type === "requirement") {
96
+ fields(source, ["type", "description"], `${label} requirement source`);
97
+ text(source.description, `${label} requirement description`);
98
+ } else add(`${label} source must be local, asset or requirement`);
99
+ }
100
+ }
101
+ return { valid: issues.length === 0, issues };
102
+ }
103
+ function createStyle(options) {
104
+ const id = assertIdentifier(options.id);
105
+ const base = `.editorbrain/styles/${id}`;
106
+ safeDocumentPath(`${base}/style.json`);
107
+ const name = options.name ?? id;
108
+ if (!nonempty(name)) throw new Error("Style name must be nonempty text");
109
+ const style = { schemaVersion: 1, id, name, tokens: {}, references: [], components: [{ id: "editorial-mechanism", status: "concept" }] };
110
+ const resources = { schemaVersion: 1, id, resources: [] };
111
+ return {
112
+ [`${base}/style.json`]: stableJson(style),
113
+ [`${base}/style.md`]: `# ${name}
114
+
115
+ STYLE_DRAFT
116
+
117
+ Este documento es un borrador. Sustituye las preguntas por decisiones y evidencia; los componentes comienzan como concept.
118
+
119
+ ## Prop\xF3sito y encaje
120
+
121
+ Describe qu\xE9 funci\xF3n cumple el estilo, para qu\xE9 material y audiencia encaja, qu\xE9 necesita y cu\xE1ndo no conviene usarlo.
122
+
123
+ ## Mecanismos que lo definen
124
+
125
+ Explica c\xF3mo interact\xFAan narrativa, composici\xF3n, tipograf\xEDa, ritmo y sonido. Define qu\xE9 debe conservarse para reconocer el estilo y qu\xE9 se adapta a la marca, el guion y el footage real. No heredes una paleta, fuente o identidad personal por defecto.
126
+
127
+ ## T\xE9cnicas adaptables
128
+
129
+ Para cada t\xE9cnica registra su funci\xF3n, requisitos, ancla de timing (voz, acci\xF3n, lectura o beat), par\xE1metros adaptables, cu\xE1ndo omitirla y c\xF3mo verificarla en el render. Los timestamps del piloto son evidencia localizada, no tiempos fijos para otro video. Detalla movimiento, estados y cobertura cuando sean relevantes.
130
+
131
+ ## Pilotos y evidencia
132
+
133
+ Registra pilotos propios o referencias autorizadas en examples.json y referencias expl\xEDcitas en style.json. Separa observaci\xF3n, propuesta e inferencia; distingue revisi\xF3n de agente de revisi\xF3n humana. Documenta qu\xE9 mecanismo se prob\xF3, el material de destino, el resultado y las limitaciones. Una declaraci\xF3n transfer_tested no demuestra la prueba.
134
+
135
+ ## Recursos y l\xEDmites
136
+
137
+ Declara recursos en resources.json con su prop\xF3sito, si son necesarios y una ruta relativa al workspace, un assetId permanente o un requisito pendiente. Describe permisos y sustitutos. No guardes URLs temporales ni credenciales. Un scaffold o chequeo estructural no valida calidad editorial ni disponibilidad remota.
138
+ `,
139
+ [`${base}/examples.json`]: stableJson({ schemaVersion: 1, id, examples: [] }),
140
+ [`${base}/effects.md`]: "# Efectos y movimiento\n\nSTYLE_DRAFT\n\nPara cada t\xE9cnica define funci\xF3n, requisitos y evidencia del piloto; ancla entradas, estados, duraci\xF3n y salida a voz, acci\xF3n o tiempo de lectura del material real. Especifica qu\xE9 conservar, qu\xE9 adaptar, cu\xE1ndo omitir y c\xF3mo verificar movimiento, legibilidad y cobertura en el render. Separa lo observado de lo propuesto; no copies timestamps del piloto ni impongas efectos en cada beat. No deduzcas SFX a partir de im\xE1genes.\n",
141
+ [`${base}/music.md`]: "# M\xFAsica y sonido\n\nSTYLE_DRAFT\n\nDescribe funci\xF3n, energ\xEDa, instrumentaci\xF3n y uso del silencio seg\xFAn el relato. Ancla cambios a voz, acci\xF3n y beats reales; define cu\xE1ndo omitir m\xFAsica o SFX y c\xF3mo verificar inteligibilidad y mezcla escuchando el render. Separa sonidos corroborados de candidatos. Declara los recursos necesarios en resources.json y documenta su procedencia, licencia y sustitutos: una referencia no concede derechos de reutilizaci\xF3n.\n",
142
+ [`${base}/resources.json`]: stableJson(resources)
143
+ };
144
+ }
145
+ function checkStyle(files, id) {
146
+ const issues = [];
147
+ const warnings = [];
148
+ const manualChecks = [
149
+ "Review purpose, fit, defining mechanisms, what stays and what adapts against the actual destination material.",
150
+ "Inspect pilot evidence and its coverage; distinguish observations, proposals, agent review and human verification.",
151
+ "Preview the implemented edit: verify timing anchors, movement, legibility, audio and when each technique should be omitted.",
152
+ "Verify resource availability, local bytes, remote workspace authorization, provenance and reuse rights. Structural validation does not certify these."
153
+ ];
154
+ const finish = (documents2) => ({ valid: issues.length === 0, issues, warnings, documents: documents2, manualChecks });
155
+ const base = `.editorbrain/styles/${id}/`;
156
+ try {
157
+ assertIdentifier(id);
158
+ safeDocumentPath(`${base}style.json`);
159
+ } catch (error) {
160
+ issues.push({ path: base, message: error.message });
161
+ return finish([]);
162
+ }
163
+ let selected;
164
+ try {
165
+ selected = canonicalFiles(Object.fromEntries(Object.entries(files).filter(([path]) => canonicalDocumentPath(path).startsWith(base))));
166
+ } catch (error) {
167
+ issues.push({ path: base, message: error.message });
168
+ return finish([]);
169
+ }
170
+ const documents = Object.keys(selected).sort();
171
+ for (const name of ["style.json", "style.md", "examples.json"]) {
172
+ if (!own(selected, base + name)) issues.push({ path: base + name, message: "Missing required style document" });
173
+ }
174
+ for (const name of ["effects.md", "music.md"]) if (!own(selected, base + name)) warnings.push(`${base + name}: Optional guide is absent; document the relevant treatment before using this style.`);
175
+ for (const path of documents) {
176
+ const content = selected[path];
177
+ const result = validateDocument(path, content);
178
+ issues.push(...result.issues);
179
+ if (typeof content !== "string") continue;
180
+ if (!content.trim()) warnings.push(`${path}: Document is empty.`);
181
+ if (/^\s*(?:STYLE_DRAFT|DRAFT|TODO(?::[^\n]*)?)\s*$/m.test(content)) warnings.push(`${path}: Draft markers remain; complete the authored guidance before claiming readiness.`);
182
+ if (!result.valid || !path.endsWith(".json")) continue;
183
+ const doc = JSON.parse(content);
184
+ if (path === base + "examples.json") {
185
+ if (!Array.isArray(doc.examples)) issues.push({ path, message: "Style examples requires an examples array" });
186
+ else if (!doc.examples.length) warnings.push(`${path}: No pilot examples are recorded; evidence and transfer have not been assessed.`);
187
+ else warnings.push(`${path}: Example entries are preserved as descriptive data; this check cannot assess their evidence, review claims or transfer quality.`);
188
+ }
189
+ if (path === base + "style.json" && Array.isArray(doc.components)) {
190
+ for (const component of doc.components) {
191
+ if (component.status === "concept") warnings.push(`${path}: Component ${component.id} remains concept; implementation is pending.`);
192
+ if (component.status === "transfer_tested") warnings.push(`${path}: Component ${component.id} declares transfer_tested; the declaration is not proof of a tested transfer.`);
193
+ }
194
+ }
195
+ if (path === base + "resources.json") {
196
+ const resources = doc;
197
+ for (const resource of resources.resources) if (resource.source.type === "requirement") warnings.push(`${path}: Resource ${resource.id} is a requirement; availability has not been verified.`);
198
+ }
199
+ }
200
+ return finish(documents);
201
+ }
202
+
3
203
  // packages/core/src/index.ts
4
204
  var SCHEMA_VERSION = 1;
5
205
  var MAX_DOCUMENT_BYTES = 128 * 1024;
@@ -11,15 +211,15 @@ var ValidationError = class extends Error {
11
211
  }
12
212
  issues;
13
213
  };
14
- var identifier = /^[a-z0-9][a-z0-9_-]{0,79}$/;
214
+ var identifier2 = /^[a-z0-9][a-z0-9_-]{0,79}$/;
15
215
  var tokenName = /^[a-z][a-z0-9_.-]{0,79}$/;
16
- var own = (value, key) => Object.prototype.hasOwnProperty.call(value, key);
17
- var object = (value) => !!value && typeof value === "object" && !Array.isArray(value);
18
- var nonempty = (value) => typeof value === "string" && value.trim().length > 0;
216
+ var own2 = (value, key) => Object.prototype.hasOwnProperty.call(value, key);
217
+ var object2 = (value) => !!value && typeof value === "object" && !Array.isArray(value);
218
+ var nonempty2 = (value) => typeof value === "string" && value.trim().length > 0;
19
219
  var finite = (value) => typeof value === "number" && Number.isFinite(value);
20
220
  var tokenValue = (value) => typeof value === "string" || typeof value === "boolean" || finite(value) || Array.isArray(value) && value.every((v) => typeof v === "string");
21
221
  function assertIdentifier(value) {
22
- if (!identifier.test(value)) throw new Error(`Invalid identifier: ${value}. Use lowercase letters, numbers, _ or - (max 80).`);
222
+ if (!identifier2.test(value)) throw new Error(`Invalid identifier: ${value}. Use lowercase letters, numbers, _ or - (max 80).`);
23
223
  return value;
24
224
  }
25
225
  function safeDocumentPath(path) {
@@ -31,13 +231,13 @@ function safeDocumentPath(path) {
31
231
  return path;
32
232
  }
33
233
  function checkRules(value, add) {
34
- if (!object(value)) {
234
+ if (!object2(value)) {
35
235
  add("tokens must be an object");
36
236
  return;
37
237
  }
38
238
  for (const [key, rule] of Object.entries(value)) {
39
239
  if (!tokenName.test(key) || ["__proto__", "constructor", "prototype"].includes(key)) add(`Invalid semantic token ${key}`);
40
- if (!object(rule) || !tokenValue(rule.value) || !["required", "preferred", "example"].includes(String(rule.strength))) add(`Token ${key} requires value and strength required/preferred/example`);
240
+ if (!object2(rule) || !tokenValue(rule.value) || !["required", "preferred", "example"].includes(String(rule.strength))) add(`Token ${key} requires value and strength required/preferred/example`);
41
241
  }
42
242
  }
43
243
  function checkEvidence(value, add) {
@@ -46,7 +246,7 @@ function checkEvidence(value, add) {
46
246
  return;
47
247
  }
48
248
  for (const ref of value) {
49
- if (!object(ref) || !nonempty(ref.videoId) || !["reference_only", "licensed_asset", "owned_asset"].includes(String(ref.relationship))) add("Evidence must include videoId and explicit relationship");
249
+ if (!object2(ref) || !nonempty2(ref.videoId) || !["reference_only", "licensed_asset", "owned_asset"].includes(String(ref.relationship))) add("Evidence must include videoId and explicit relationship");
50
250
  else {
51
251
  if (ref.timestampSeconds !== void 0 && (!finite(ref.timestampSeconds) || ref.timestampSeconds < 0)) add("Evidence timestamp must be nonnegative seconds");
52
252
  if (ref.reviewStatus !== void 0 && !["unreviewed", "agent_reviewed", "human_verified"].includes(String(ref.reviewStatus))) add("Invalid evidence reviewStatus");
@@ -75,11 +275,12 @@ function validateDocument(path, content) {
75
275
  add("Invalid JSON");
76
276
  return { valid: false, issues };
77
277
  }
78
- if (!object(doc)) {
278
+ if (!object2(doc)) {
79
279
  add("JSON document must be an object");
80
280
  return { valid: false, issues };
81
281
  }
82
282
  if (doc.schemaVersion !== SCHEMA_VERSION) add("schemaVersion must be 1");
283
+ for (const message of memoryIssues(canonicalDocumentPath(path), doc)) add(message);
83
284
  const parts = canonicalDocumentPath(path).split("/");
84
285
  const isBrand = parts[0] === ".editorbrain" && parts[1] === "brands" && parts.length === 4;
85
286
  const isStyle = parts[0] === ".editorbrain" && parts[1] === "styles" && parts.length === 4;
@@ -88,38 +289,39 @@ function validateDocument(path, content) {
88
289
  const isMedia = parts[0] === "videos" && parts.length === 3 && parts[2] === "media.json";
89
290
  if (isBrand || isStyle || isProject || isOutput) {
90
291
  const expected = isOutput ? parts[2].slice(0, -5) : parts[isProject ? 1 : 2];
91
- if (!identifier.test(expected)) add("Invalid resource identifier in path");
292
+ if (!identifier2.test(expected)) add("Invalid resource identifier in path");
92
293
  if (doc.id !== void 0 && doc.id !== expected) add(`Document id must match path: ${expected}`);
93
294
  }
295
+ if (isStyle && parts[3] === "resources.json") for (const issue of validateStyleResources(doc, parts[2]).issues) add(issue.message);
94
296
  if (isBrand && parts[3] === "tokens.json" || isStyle && parts[3] === "style.json") {
95
- if (!nonempty(doc.id)) add("id is required");
297
+ if (!nonempty2(doc.id)) add("id is required");
96
298
  if (isBrand || doc.tokens !== void 0) checkRules(doc.tokens, add);
97
299
  if (isStyle) {
98
- if (!nonempty(doc.name)) add("Style name is required");
300
+ if (!nonempty2(doc.name)) add("Style name is required");
99
301
  if (doc.references !== void 0) checkEvidence(doc.references, add);
100
- if (doc.components !== void 0 && (!Array.isArray(doc.components) || doc.components.some((c) => !object(c) || !nonempty(c.id) || !["concept", "implemented", "transfer_tested"].includes(String(c.status))))) add("Components need id and explicit implementation status");
302
+ if (doc.components !== void 0 && (!Array.isArray(doc.components) || doc.components.some((c) => !object2(c) || !nonempty2(c.id) || !["concept", "implemented", "transfer_tested"].includes(String(c.status))))) add("Components need id and explicit implementation status");
101
303
  }
102
304
  }
103
305
  if (isProject) {
104
- for (const key of ["id", "brand", "style", "outputProfile"]) if (typeof doc[key] !== "string" || !identifier.test(doc[key])) add(`${key} must be a valid identifier`);
105
- if (!object(doc.intent) || !nonempty(doc.intent.topic) || !nonempty(doc.intent.goal)) add("intent requires topic and goal");
306
+ for (const key of ["id", "brand", "style", "outputProfile"]) if (typeof doc[key] !== "string" || !identifier2.test(doc[key])) add(`${key} must be a valid identifier`);
307
+ if (!object2(doc.intent) || !nonempty2(doc.intent.topic) || !nonempty2(doc.intent.goal)) add("intent requires topic and goal");
106
308
  if (doc.reviewMode !== void 0 && !["direct", "directions", "sample"].includes(doc.reviewMode)) add("reviewMode must be direct, directions or sample");
107
309
  if (doc.script !== void 0 && typeof doc.script !== "string") add("script must be text");
108
310
  if (doc.overrides !== void 0) {
109
- if (!object(doc.overrides)) add("overrides must be an object");
311
+ if (!object2(doc.overrides)) add("overrides must be an object");
110
312
  else for (const [key, rule] of Object.entries(doc.overrides)) {
111
313
  if (!tokenName.test(key) || ["__proto__", "constructor", "prototype"].includes(key)) add(`Invalid override token ${key}`);
112
- if (!object(rule) || !tokenValue(rule.value) || !nonempty(rule.reason) || rule.overrideRequired !== void 0 && typeof rule.overrideRequired !== "boolean") add(`Override ${key} requires value and a reason; overrideRequired must be boolean`);
314
+ if (!object2(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`);
113
315
  }
114
316
  }
115
317
  }
116
318
  if (isMedia) {
117
- if (!identifier.test(parts[1])) add("Invalid project identifier in media path");
319
+ if (!identifier2.test(parts[1])) add("Invalid project identifier in media path");
118
320
  if (!Array.isArray(doc.assets)) add("Production media requires an assets array");
119
321
  else {
120
322
  const ids = /* @__PURE__ */ new Set();
121
323
  for (const asset of doc.assets) {
122
- if (!object(asset) || typeof asset.id !== "string" || !/^[a-f0-9]{64}$/.test(asset.id) || !nonempty(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) {
324
+ if (!object2(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) {
123
325
  add("Production assets require a SHA-256 id, name, media mimeType and positive integer bytes");
124
326
  continue;
125
327
  }
@@ -129,11 +331,11 @@ function validateDocument(path, content) {
129
331
  }
130
332
  }
131
333
  if (isOutput) {
132
- if (!nonempty(doc.id)) add("id is required");
334
+ if (!nonempty2(doc.id)) add("id is required");
133
335
  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`);
134
336
  if (!finite(doc.fps) || doc.fps <= 0 || doc.fps > 240) add("fps must be between 0 and 240");
135
337
  if (doc.safeAreas !== void 0) {
136
- if (!object(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");
338
+ if (!object2(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");
137
339
  }
138
340
  }
139
341
  return { valid: issues.length === 0, issues };
@@ -154,7 +356,7 @@ function projectSources(project) {
154
356
  ];
155
357
  }
156
358
  function optionalProjectSources(project) {
157
- return [`.editorbrain/brands/${project.brand}/design.md`, `.editorbrain/styles/${project.style}/effects.md`, `.editorbrain/styles/${project.style}/music.md`, `videos/${project.id}/media.json`];
359
+ return [`.editorbrain/brands/${project.brand}/design.md`, `.editorbrain/styles/${project.style}/effects.md`, `.editorbrain/styles/${project.style}/music.md`, `videos/${project.id}/media.json`, memoryPath("brand", project.brand), memoryPath("style", project.style), memoryPath("project", project.id), `.editorbrain/styles/${project.style}/resources.json`];
158
360
  }
159
361
  function validateWorkspace(files) {
160
362
  files = canonicalFiles(files);
@@ -163,31 +365,31 @@ function validateWorkspace(files) {
163
365
  for (const [path, content] of Object.entries(files)) {
164
366
  if (!/^videos\/[^/]+\/project\.json$/.test(path) || !validateDocument(path, content).valid) continue;
165
367
  const project = JSON.parse(content);
166
- for (const required of projectSources(project)) if (!own(files, required)) issues.push({ path, message: `Missing required source ${required}` });
368
+ for (const required of projectSources(project)) if (!own2(files, required)) issues.push({ path, message: `Missing required source ${required}` });
167
369
  }
168
370
  return { valid: issues.length === 0, issues };
169
371
  }
170
372
  function stableJson(value) {
171
- const sort = (item) => Array.isArray(item) ? item.map(sort) : object(item) ? Object.fromEntries(Object.keys(item).sort().map((key) => [key, sort(item[key])])) : item;
373
+ const sort = (item) => Array.isArray(item) ? item.map(sort) : object2(item) ? Object.fromEntries(Object.keys(item).sort().map((key) => [key, sort(item[key])])) : item;
172
374
  return JSON.stringify(sort(value), null, 2) + "\n";
173
375
  }
174
376
  async function sha256(content) {
175
377
  const bytes = typeof content === "string" ? new TextEncoder().encode(content) : content;
176
- const digest = await crypto.subtle.digest("SHA-256", new Uint8Array(bytes));
177
- return Array.from(new Uint8Array(digest), (b) => b.toString(16).padStart(2, "0")).join("");
378
+ const digest2 = await crypto.subtle.digest("SHA-256", new Uint8Array(bytes));
379
+ return Array.from(new Uint8Array(digest2), (b) => b.toString(16).padStart(2, "0")).join("");
178
380
  }
179
381
  async function resolveProject(files, projectId) {
180
382
  files = canonicalFiles(files);
181
383
  assertIdentifier(projectId);
182
384
  const path = `videos/${projectId}/project.json`;
183
- if (!own(files, path)) throw new ValidationError([{ path, message: "Project does not exist" }]);
385
+ if (!own2(files, path)) throw new ValidationError([{ path, message: "Project does not exist" }]);
184
386
  const basic = validateDocument(path, files[path]);
185
387
  if (!basic.valid) throw new ValidationError(basic.issues);
186
388
  const project = JSON.parse(files[path]);
187
389
  const requiredPaths = projectSources(project);
188
390
  const optionalPaths = optionalProjectSources(project);
189
- const sourcePaths = [...requiredPaths, ...optionalPaths.filter((p) => own(files, p))];
190
- const relevant = Object.fromEntries(sourcePaths.filter((p) => own(files, p)).map((p) => [p, files[p]]));
391
+ const sourcePaths = [...requiredPaths, ...optionalPaths.filter((p) => own2(files, p))];
392
+ const relevant = Object.fromEntries(sourcePaths.filter((p) => own2(files, p)).map((p) => [p, files[p]]));
191
393
  const validation = validateWorkspace(relevant);
192
394
  if (!validation.valid) throw new ValidationError(validation.issues);
193
395
  const [brandPath, tokensPath, languagePath, assetsPath, styleMdPath, stylePath, examplesPath, outputPath, , briefPath, directionPath] = requiredPaths;
@@ -228,11 +430,13 @@ async function resolveProject(files, projectId) {
228
430
  output: output2,
229
431
  tokens,
230
432
  tokenSources,
231
- editorial: { brand: files[brandPath], style: files[styleMdPath], brief: files[briefPath], direction: files[directionPath], ...Object.fromEntries(["design", "effects", "music"].flatMap((key, index) => own(files, optionalPaths[index]) ? [[key, files[optionalPaths[index]]]] : [])) },
433
+ editorial: { brand: files[brandPath], style: files[styleMdPath], brief: files[briefPath], direction: files[directionPath], ...Object.fromEntries(["design", "effects", "music"].flatMap((key, index) => own2(files, optionalPaths[index]) ? [[key, files[optionalPaths[index]]]] : [])) },
232
434
  language: JSON.parse(files[languagePath]),
233
435
  brandAssets: JSON.parse(files[assetsPath]),
234
436
  examples: JSON.parse(files[examplesPath]),
235
- ...own(files, optionalPaths[3]) ? { media: JSON.parse(files[optionalPaths[3]]) } : {},
437
+ ...own2(files, optionalPaths[3]) ? { media: JSON.parse(files[optionalPaths[3]]) } : {},
438
+ ...own2(files, optionalPaths[7]) ? { styleResources: JSON.parse(files[optionalPaths[7]]) } : {},
439
+ memory: resolveMemory(files, project),
236
440
  evidence: style.references ?? [],
237
441
  warnings,
238
442
  lock: { schemaVersion: 1, projectId, sourceHashes, overrides: project.overrides ?? {}, adapters: Object.fromEntries(["neutral", "hyperframes", "remotion"].map((key) => [key, { version: "0.2.1", kind: "editorial_handoff", rendered: false }])) }
@@ -250,6 +454,10 @@ ${content}` : "";
250
454
 
251
455
  \`\`\`json
252
456
  ${stableJson(resolved.media)}\`\`\``) : "";
457
+ const resourcesSection = resolved.styleResources ? section("Style resources", `Resolve local paths from the original workspace root; asset IDs require authorized workspace access. Availability and reuse rights must be verified before use.
458
+
459
+ \`\`\`json
460
+ ${stableJson(resolved.styleResources)}\`\`\``) : "";
253
461
  if (adapter === "neutral") return { ...common, "brief.md": `# ${resolved.project.id}
254
462
 
255
463
  ${disclaimer}
@@ -266,7 +474,7 @@ ${resolved.editorial.brand}
266
474
 
267
475
  ## Style
268
476
 
269
- ${resolved.editorial.style}${section("Design", resolved.editorial.design)}${section("Effects", resolved.editorial.effects)}${section("Music and sound", resolved.editorial.music)}${mediaSection}
477
+ ${resolved.editorial.style}${section("Design", resolved.editorial.design)}${section("Effects", resolved.editorial.effects)}${section("Music and sound", resolved.editorial.music)}${mediaSection}${resourcesSection}
270
478
 
271
479
  Exact tokens, output settings and evidence are in resolved.json.
272
480
  ` };
@@ -294,13 +502,13 @@ ${resolved.editorial.direction}
294
502
 
295
503
  ## Style intent
296
504
 
297
- ${resolved.editorial.style}${section("Effects", resolved.editorial.effects)}${section("Music and sound", resolved.editorial.music)}${mediaSection}
505
+ ${resolved.editorial.style}${section("Effects", resolved.editorial.effects)}${section("Music and sound", resolved.editorial.music)}${mediaSection}${resourcesSection}
298
506
 
299
507
  ## Delivery
300
508
 
301
509
  ${resolved.output.width} \xD7 ${resolved.output.height}, ${resolved.output.fps} fps. Adapt composition to the actual material and safe areas in resolved.json. Implement and preview the composition in Hyperframes before claiming completion.
302
510
  ` };
303
- if (adapter === "remotion") return { ...common, "remotion-plan.json": stableJson({ schemaVersion: 1, kind: "editorial_handoff", renderer: "remotion", executable: false, note: disclaimer, composition: { width: resolved.output.width, height: resolved.output.height, fps: resolved.output.fps, durationInFrames: null }, tokens: resolved.tokens, project: resolved.project, direction: resolved.editorial.direction, editorial: resolved.editorial, ...resolved.media ? { media: resolved.media } : {}, evidence: resolved.evidence, implementationTasks: ["Determine duration from the script/audio and actual cuts.", "Implement React composition and assets.", "Preview and verify typography, safe areas, motion and audio."] }) };
511
+ if (adapter === "remotion") return { ...common, "remotion-plan.json": stableJson({ schemaVersion: 1, kind: "editorial_handoff", renderer: "remotion", executable: false, note: disclaimer, composition: { width: resolved.output.width, height: resolved.output.height, fps: resolved.output.fps, durationInFrames: null }, tokens: resolved.tokens, project: resolved.project, direction: resolved.editorial.direction, editorial: resolved.editorial, ...resolved.media ? { media: resolved.media } : {}, ...resolved.styleResources ? { styleResources: resolved.styleResources } : {}, evidence: resolved.evidence, implementationTasks: ["Determine duration from the script/audio and actual cuts.", "Implement React composition and assets.", "Preview and verify typography, safe areas, motion and audio."] }) };
304
512
  throw new Error(`Unsupported adapter: ${adapter}`);
305
513
  }
306
514
  function createWorkspace(options = {}) {
@@ -640,11 +848,11 @@ async function pushWorkspace(client, root, files, options = {}) {
640
848
  for (const [path, content] of Object.entries(files)) {
641
849
  const validation = validateDocument(path, content);
642
850
  if (!validation.valid) throw new Error(validation.issues.map((x) => `${x.path}: ${x.message}`).join("\n"));
643
- const digest = await sha256(content);
851
+ const digest2 = await sha256(content);
644
852
  const existing = remote.get(path);
645
853
  const base = state.documents[path];
646
- if (existing?.sha256 === digest) {
647
- state.documents[path] = { revision: existing.revision, sha256: digest };
854
+ if (existing?.sha256 === digest2) {
855
+ state.documents[path] = { revision: existing.revision, sha256: digest2 };
648
856
  unchanged++;
649
857
  continue;
650
858
  }
@@ -665,8 +873,8 @@ ${conflicts.join("\n")}`, "Run push --dry-run and reconcile the reported paths."
665
873
  for (const item of pending) {
666
874
  const result = await client.put(item.path, item.content, item.expectedRevision);
667
875
  checkRemote(result);
668
- const digest = await sha256(item.content);
669
- if (result.path !== item.path || result.sha256 !== digest) throw new Error("Server returned inconsistent write metadata");
876
+ const digest2 = await sha256(item.content);
877
+ if (result.path !== item.path || result.sha256 !== digest2) throw new Error("Server returned inconsistent write metadata");
670
878
  state.documents[item.path] = { revision: result.revision, sha256: result.sha256 };
671
879
  uploaded.push(item.path);
672
880
  await saveSyncState(root, state);
@@ -684,14 +892,14 @@ async function pullWorkspace(client, root, local, options = {}) {
684
892
  for (const summary of summaries) {
685
893
  checkRemote(summary);
686
894
  const content = local[summary.path];
687
- const digest = content === void 0 ? void 0 : await sha256(content);
895
+ const digest2 = content === void 0 ? void 0 : await sha256(content);
688
896
  const base = state.documents[summary.path];
689
- if (digest === summary.sha256) {
897
+ if (digest2 === summary.sha256) {
690
898
  state.documents[summary.path] = { revision: summary.revision, sha256: summary.sha256 };
691
899
  unchanged++;
692
900
  continue;
693
901
  }
694
- if (content !== void 0 && (!base || digest !== base.sha256)) {
902
+ if (content !== void 0 && (!base || digest2 !== base.sha256)) {
695
903
  conflicts.push(summary.path);
696
904
  continue;
697
905
  }
@@ -777,7 +985,7 @@ import { createHash } from "node:crypto";
777
985
  import { constants as constants2 } from "node:fs";
778
986
  import { lstat as lstat2, open as open2, readdir as readdir2, rename as rename2 } from "node:fs/promises";
779
987
  import { fileURLToPath } from "node:url";
780
- var previousSkillHashes = { "SKILL.md": ["9df2c89deeec30ddf556c359027efe1d90e6b4e9c08717cd8827e210b38a187f", "67b157cb45cae3621e6f2c43235c1d69907ad6abd45ab6b52a3c65f236732db9", "1e2fff65a1b303b18b2d65156bef6eb2439a19d77ba2a25b37bcb1819ff009e3"], "AGENTS.md": ["4f3ef860ade3a31aa7c6a6aab5cf6ec2a700850bf19ea2320324c12c2e0fbf5b", "1b07de01698a0040b47ea0eddb1b017f70a69e81acd0bf9710647ca5e02f5125", "b5ee3439ff974c668755816149fab76899d7ad9266f63978b30fbc9b7d2cd640"], "references/cli-mcp.md": ["5d3a79343fbd76cdae5bdbe12fb9c944d5dff4f489ac6534fd96e0cb8630b988", "8e011c31368cef2117a28ee972b39aab9535eeb0f601a4a0f9d6fc019b22c1c2", "666082653efc000f8a7cc5f1836c80bbdda5f0f57cdbbdcb393f2b84aeb93a01", "78b737796a8685b1135e0acb91b620c51845b62e31dba04eb51dd9dd38b41008"], "references/production-and-review.md": ["055c6df9e0f167be148b0a5c0bba5b43e108eb1f58f478d88b3536c6589ac7b3"], "references/workspace-files.md": ["e72af2871f42cd776be1bfff8009ed5562780e7f8a65b12c27887f3880631f81", "00780c64bd0244c6afb18d786b7512b43e315c376e8f88c67d6ba53d91275ccf", "97053b092e0ff36e3cfb764095fcb4a4fca94e722567e774b53bc118279f40e5"] };
988
+ var previousSkillHashes = { "SKILL.md": ["9df2c89deeec30ddf556c359027efe1d90e6b4e9c08717cd8827e210b38a187f", "67b157cb45cae3621e6f2c43235c1d69907ad6abd45ab6b52a3c65f236732db9", "1e2fff65a1b303b18b2d65156bef6eb2439a19d77ba2a25b37bcb1819ff009e3", "dacce1c4127f4281df6f7e1681e243444bb6f8a8e74b829e52ee0108c6672348", "09740ab4b275eada65b5040ae6000104ff39094f534a2784e67feb2afc2a59b3"], "AGENTS.md": ["4f3ef860ade3a31aa7c6a6aab5cf6ec2a700850bf19ea2320324c12c2e0fbf5b", "1b07de01698a0040b47ea0eddb1b017f70a69e81acd0bf9710647ca5e02f5125", "b5ee3439ff974c668755816149fab76899d7ad9266f63978b30fbc9b7d2cd640", "bacedb7bdedab5abe75c9601717cfdb05a96208fa2e2a42af878e2dfd2ae4251", "ea3b473179857ccc07e9e4f21a6ede9e0906a09e495f4a051069d15aee1556cd"], "references/cli-mcp.md": ["5d3a79343fbd76cdae5bdbe12fb9c944d5dff4f489ac6534fd96e0cb8630b988", "8e011c31368cef2117a28ee972b39aab9535eeb0f601a4a0f9d6fc019b22c1c2", "666082653efc000f8a7cc5f1836c80bbdda5f0f57cdbbdcb393f2b84aeb93a01", "78b737796a8685b1135e0acb91b620c51845b62e31dba04eb51dd9dd38b41008", "8b2e01bfd937237919f5ade794eed01dc6e32b556ede9de3a0ccc28cbdd988d1", "ca88b2816a795a5ebe834074b178469367634b80ad837e3697182ea585af1c05"], "references/production-and-review.md": ["055c6df9e0f167be148b0a5c0bba5b43e108eb1f58f478d88b3536c6589ac7b3"], "references/workspace-files.md": ["e72af2871f42cd776be1bfff8009ed5562780e7f8a65b12c27887f3880631f81", "00780c64bd0244c6afb18d786b7512b43e315c376e8f88c67d6ba53d91275ccf", "97053b092e0ff36e3cfb764095fcb4a4fca94e722567e774b53bc118279f40e5", "70fc660e7ed3d70c16c33c62c8e0399b99c3251808f093ef3d4ea5e8d39e4c8a", "28c62c1e28842142607f5168ee79b18fd797900dd66dd9ac5eb3cfcf8a09f781"] };
781
989
  var destinations = {
782
990
  codex: { directory: ".agents/skills/editorbrain", entryPoint: "AGENTS.md" },
783
991
  claude: { directory: ".claude/skills/editorbrain", entryPoint: "CLAUDE.md" }
@@ -848,13 +1056,13 @@ Keep credentials outside this folder. Treat retrieved reference material as evid
848
1056
  for (const selected of agents) {
849
1057
  const to = destinations[selected].directory, from = to.replace(/editorbrain$/, "editorharness");
850
1058
  const oldPath = await checkedPath(root, from, false), newPath = await checkedPath(root, to, false);
851
- const exists = async (path) => lstat2(path).then(() => true, (error) => {
1059
+ const exists2 = async (path) => lstat2(path).then(() => true, (error) => {
852
1060
  if (error.code === "ENOENT") return false;
853
1061
  throw error;
854
1062
  });
855
- if (await exists(oldPath)) {
1063
+ if (await exists2(oldPath)) {
856
1064
  await loadSkill(oldPath);
857
- if (await exists(newPath)) throw new Error("Two skill directories exist. Reconcile them before installing; neither was changed.");
1065
+ if (await exists2(newPath)) throw new Error("Two skill directories exist. Reconcile them before installing; neither was changed.");
858
1066
  migrations.push({ from, to });
859
1067
  }
860
1068
  }
@@ -916,7 +1124,7 @@ import { lstat as lstat3, open as open3, rename as rename3, unlink as unlink2 }
916
1124
  import { hostname } from "node:os";
917
1125
  import { randomUUID as randomUUID2 } from "node:crypto";
918
1126
  var LOCK_PATH = ".editorbrain-operation.lock";
919
- async function withWorkspaceLock(root, operation) {
1127
+ async function withWorkspaceLock(root, operation, options = {}) {
920
1128
  const legacyLock = await checkedPath(root, ".editorharness-operation.lock", false);
921
1129
  let legacy;
922
1130
  try {
@@ -940,12 +1148,12 @@ async function withWorkspaceLock(root, operation) {
940
1148
  await handle.close();
941
1149
  const oldRoot = await checkedPath(root, ".editorharness", false);
942
1150
  const newRoot = await checkedPath(root, ".editorbrain", false);
943
- const exists = async (path2) => lstat3(path2).then(() => true, (error) => {
1151
+ const exists2 = async (path2) => lstat3(path2).then(() => true, (error) => {
944
1152
  if (error.code === "ENOENT") return false;
945
1153
  throw error;
946
1154
  });
947
- if (await exists(oldRoot)) {
948
- if (await exists(newRoot)) throw new CliError("WORKSPACE_PATH_CONFLICT", "This folder contains two versions of the workspace directory.", "Reconcile the directories before continuing; neither was changed.");
1155
+ if (options.migrateLegacy !== false && await exists2(oldRoot)) {
1156
+ if (await exists2(newRoot)) throw new CliError("WORKSPACE_PATH_CONFLICT", "This folder contains two versions of the workspace directory.", "Reconcile the directories before continuing; neither was changed.");
949
1157
  await rename3(oldRoot, newRoot);
950
1158
  }
951
1159
  return await operation();
@@ -1067,7 +1275,7 @@ async function configureMcp(agent, directory, url, noBrowser, runner = runHost)
1067
1275
  const version = await runner(agent, ["--version"], directory);
1068
1276
  if (version.code !== 0) throw new CliError("AGENT_UNAVAILABLE", `${agent} CLI is unavailable.`, "Install the official agent CLI, or use setup --skip-mcp.");
1069
1277
  const inspect = await runner(agent, agent === "codex" ? ["mcp", "list", "--json"] : ["mcp", "get", "editorbrain"], directory);
1070
- let exists = false;
1278
+ let exists2 = false;
1071
1279
  if (agent === "codex") {
1072
1280
  if (inspect.code !== 0) throw new CliError("MCP_INSPECTION_FAILED", "Could not inspect Codex MCP configuration.", "Inspect it manually; setup did not overwrite it.");
1073
1281
  let list;
@@ -1077,12 +1285,12 @@ async function configureMcp(agent, directory, url, noBrowser, runner = runHost)
1077
1285
  throw new CliError("MCP_INSPECTION_FAILED", "Codex returned an unsupported configuration format.", "Inspect MCP configuration manually.");
1078
1286
  }
1079
1287
  if (!Array.isArray(list)) throw new CliError("MCP_INSPECTION_FAILED", "Unsupported MCP configuration response.", "Inspect MCP configuration manually.");
1080
- exists = list.some((item) => item?.name === "editorbrain");
1288
+ exists2 = list.some((item) => item?.name === "editorbrain");
1081
1289
  } else {
1082
- exists = inspect.code === 0;
1083
- if (!exists && !/^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.");
1290
+ exists2 = inspect.code === 0;
1291
+ if (!exists2 && !/^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.");
1084
1292
  }
1085
- if (exists) return { agent, status: "preserved", next: "An existing editorbrain server was preserved. Check its endpoint and authentication, then " + next, hostVerified: false };
1293
+ if (exists2) return { agent, status: "preserved", next: "An existing editorbrain server was preserved. Check its endpoint and authentication, then " + next, hostVerified: false };
1086
1294
  const added = await runner(agent, args, directory, true);
1087
1295
  return { agent, status: added.code === 0 ? "registered" : "action_required", next, hostVerified: false };
1088
1296
  }
@@ -1137,10 +1345,10 @@ async function setup(options, runner = runHost) {
1137
1345
  }
1138
1346
 
1139
1347
  // packages/cli/src/index.ts
1140
- import { chmod, lstat as lstat4, mkdir as mkdir2, open as open4 } from "node:fs/promises";
1141
- import { constants as constants4 } from "node:fs";
1348
+ import { chmod, lstat as lstat5, mkdir as mkdir3, open as open5 } from "node:fs/promises";
1349
+ import { constants as constants5 } from "node:fs";
1142
1350
  import { homedir } from "node:os";
1143
- import { basename, dirname as dirname2, extname, join as join2, resolve as resolve4 } from "node:path";
1351
+ import { basename, dirname as dirname3, extname, join as join2, resolve as resolve4 } from "node:path";
1144
1352
 
1145
1353
  // packages/cli/src/device-auth.ts
1146
1354
  import { spawn as spawn2 } from "node:child_process";
@@ -1342,6 +1550,225 @@ async function prepareDirectionContext(root, client, projectId) {
1342
1550
  };
1343
1551
  }
1344
1552
 
1553
+ // packages/cli/src/style-authoring.ts
1554
+ import { createHash as createHash2 } from "node:crypto";
1555
+ import { constants as constants4 } from "node:fs";
1556
+ import { lstat as lstat4, mkdir as mkdir2, open as open4, readdir as readdir3, rmdir, unlink as unlink3 } from "node:fs/promises";
1557
+ import { dirname as dirname2 } from "node:path";
1558
+ function styleId(id) {
1559
+ try {
1560
+ return assertIdentifier(id);
1561
+ } catch {
1562
+ throw new CliError("INVALID_ARGUMENT", "A style ID must use lowercase letters, numbers, _ or - (1\u201380 characters).", "Use styles create <style-id> or styles check <style-id>.");
1563
+ }
1564
+ }
1565
+ async function exists(path) {
1566
+ try {
1567
+ await lstat4(path);
1568
+ return true;
1569
+ } catch (error) {
1570
+ if (error.code === "ENOENT") return false;
1571
+ throw error;
1572
+ }
1573
+ }
1574
+ async function preflightCreate(root, id, files) {
1575
+ for (const prefix of [".editorbrain", ".editorharness"]) {
1576
+ const path = `${prefix}/styles/${id}`;
1577
+ if (await exists(await checkedPath(root, path, false))) {
1578
+ throw new CliError("STYLE_EXISTS", `Style ${id} already exists in this workspace.`, "Choose a new style ID or edit the existing style documents; creation never replaces a style.");
1579
+ }
1580
+ }
1581
+ if (await exists(await checkedPath(root, ".editorharness", false))) {
1582
+ throw new CliError("WORKSPACE_PATH_CONFLICT", "This workspace still has a legacy portable directory.", "Migrate it using the existing workspace pull flow, or create the style in a separate folder. No style or project was changed.");
1583
+ }
1584
+ for (const path of Object.keys(files)) {
1585
+ if (!path.startsWith(`.editorbrain/styles/${id}/`)) throw new Error("Style template escaped its destination");
1586
+ const target = await checkedPath(root, path);
1587
+ if (await exists(target)) throw new CliError("STYLE_EXISTS", `Refusing to replace existing file: ${path}`, "Choose a new style ID.");
1588
+ }
1589
+ }
1590
+ async function makeDirectories(root, path, created) {
1591
+ const segments = path.split("/");
1592
+ for (let i = 1; i <= segments.length; i++) {
1593
+ const relative2 = segments.slice(0, i).join("/");
1594
+ const target = await checkedPath(root, relative2, false);
1595
+ try {
1596
+ await mkdir2(target);
1597
+ const info = await lstat4(target);
1598
+ created.push({ path: relative2, dev: info.dev, ino: info.ino });
1599
+ } catch (error) {
1600
+ if (error.code !== "EEXIST") throw error;
1601
+ if (!(await lstat4(await checkedPath(root, relative2, false))).isDirectory()) throw new Error(`Not a directory: ${relative2}`);
1602
+ }
1603
+ }
1604
+ }
1605
+ async function cleanCreated(root, paths, directory) {
1606
+ for (const item of [...paths].reverse()) {
1607
+ try {
1608
+ const target = await checkedPath(root, item.path, false);
1609
+ const info = await lstat4(target);
1610
+ if (info.dev !== item.dev || info.ino !== item.ino || info.isSymbolicLink()) continue;
1611
+ if (directory) await rmdir(target);
1612
+ else await unlink3(target);
1613
+ } catch {
1614
+ }
1615
+ }
1616
+ }
1617
+ async function createLocalStyle(options) {
1618
+ const id = styleId(options.id);
1619
+ if (options.name !== void 0 && (!options.name.trim() || options.name.length > 200 || /[\x00-\x1f\x7f]/.test(options.name))) {
1620
+ throw new CliError("INVALID_ARGUMENT", "--name must contain 1\u2013200 characters without control characters.", "Use a short, descriptive style name.");
1621
+ }
1622
+ const files = createStyle({ id, name: options.name });
1623
+ const root = await ensureRoot(options.directory, true);
1624
+ await preflightCreate(root, id, files);
1625
+ return withWorkspaceLock(root, async () => {
1626
+ await preflightCreate(root, id, files);
1627
+ const createdFiles = [];
1628
+ const createdDirectories = [];
1629
+ try {
1630
+ for (const [path, content] of Object.entries(files)) {
1631
+ await makeDirectories(root, dirname2(path), createdDirectories);
1632
+ const target = await checkedPath(root, path);
1633
+ const handle = await open4(target, constants4.O_WRONLY | constants4.O_CREAT | constants4.O_EXCL | constants4.O_NOFOLLOW, 420);
1634
+ try {
1635
+ const info = await handle.stat();
1636
+ createdFiles.push({ path, dev: info.dev, ino: info.ino });
1637
+ await handle.writeFile(content, "utf8");
1638
+ } finally {
1639
+ await handle.close();
1640
+ }
1641
+ }
1642
+ } catch (error) {
1643
+ await cleanCreated(root, createdFiles, false);
1644
+ await cleanCreated(root, createdDirectories, true);
1645
+ throw error;
1646
+ }
1647
+ return {
1648
+ root,
1649
+ id,
1650
+ status: "draft",
1651
+ created: Object.keys(files),
1652
+ mediaUploaded: false,
1653
+ next: `Read references/style-creator.md in the EditorBrain skill. Edit .editorbrain/styles/${id}/style.md, effects.md, music.md, resources.json and examples.json, and document evidence actually reviewed; then run editorbrain styles check ${id} --dir <workspace>. Assess adaptation using the actual material in the next requested edit; structural checks do not assess editorial quality.`
1654
+ };
1655
+ }, { migrateLegacy: false });
1656
+ }
1657
+ async function readStyle(root, id) {
1658
+ const files = /* @__PURE__ */ Object.create(null);
1659
+ const issues = [];
1660
+ const directories = [];
1661
+ for (const prefix of [".editorbrain", ".editorharness"]) {
1662
+ const path = `${prefix}/styles/${id}`;
1663
+ try {
1664
+ const target = await checkedPath(root, path, false);
1665
+ if (!await exists(target)) continue;
1666
+ if (!(await lstat4(target)).isDirectory()) {
1667
+ issues.push({ path, message: "Style path is not a directory." });
1668
+ continue;
1669
+ }
1670
+ directories.push(path);
1671
+ const entries = await readdir3(target, { withFileTypes: true });
1672
+ for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
1673
+ if (!/\.(json|md)$/.test(entry.name)) continue;
1674
+ const document = `${path}/${entry.name}`;
1675
+ if (!entry.isFile()) {
1676
+ issues.push({ path: document, message: "Style document must be a regular file, not a symlink or directory." });
1677
+ continue;
1678
+ }
1679
+ try {
1680
+ files[document] = await readText(root, document);
1681
+ } catch (error) {
1682
+ issues.push({ path: document, message: `Cannot safely read style document: ${error.message}` });
1683
+ }
1684
+ }
1685
+ } catch (error) {
1686
+ issues.push({ path, message: `Cannot safely read style directory: ${error.message}` });
1687
+ }
1688
+ }
1689
+ if (directories.length > 1) issues.push({ path: `.editorbrain/styles/${id}`, message: "Style exists in both current and legacy directories. Reconcile the duplicate before checking it." });
1690
+ return { files, issues };
1691
+ }
1692
+ async function checkLocalResource(root, path, expected) {
1693
+ safeStyleResourcePath(path);
1694
+ const target = await checkedPath(root, path, false);
1695
+ const handle = await open4(target, constants4.O_RDONLY | constants4.O_NOFOLLOW | constants4.O_NONBLOCK);
1696
+ try {
1697
+ const before = await handle.stat();
1698
+ if (!before.isFile()) throw new Error("Resource is not a regular file.");
1699
+ if (!expected) return {};
1700
+ const hash = createHash2("sha256");
1701
+ for await (const chunk of handle.createReadStream({ autoClose: false })) hash.update(chunk);
1702
+ const after = await handle.stat();
1703
+ const current = await lstat4(await checkedPath(root, path, false));
1704
+ if (before.size !== after.size || before.mtimeMs !== after.mtimeMs || before.ctimeMs !== after.ctimeMs || current.dev !== before.dev || current.ino !== before.ino) {
1705
+ throw new Error("Resource changed while its hash was being checked; check it again.");
1706
+ }
1707
+ return { actualSha256: hash.digest("hex") };
1708
+ } finally {
1709
+ await handle.close();
1710
+ }
1711
+ }
1712
+ async function checkLocalStyle(options) {
1713
+ const id = styleId(options.id);
1714
+ const root = await ensureRoot(options.directory);
1715
+ const local = await readStyle(root, id);
1716
+ const structural = checkStyle(local.files, id);
1717
+ const localIssues = [...local.issues];
1718
+ const warnings = [...structural.warnings];
1719
+ const resources = [];
1720
+ const resourcePath = Object.keys(local.files).find((path) => path.endsWith("/resources.json"));
1721
+ let document;
1722
+ if (resourcePath) {
1723
+ try {
1724
+ const parsed = JSON.parse(local.files[resourcePath]);
1725
+ if (validateStyleResources(parsed, id).valid) document = parsed;
1726
+ } catch {
1727
+ }
1728
+ }
1729
+ for (const resource of document?.resources ?? []) {
1730
+ const result = { id: resource.id, source: resource.source.type, required: resource.required, status: "unverified" };
1731
+ resources.push(result);
1732
+ if (resource.source.type !== "local") {
1733
+ warnings.push(`Resource ${resource.id}: ${resource.source.type === "asset" ? "remote asset availability and access" : "installed or external requirement"} not verified by this local check.`);
1734
+ continue;
1735
+ }
1736
+ result.path = resource.source.path;
1737
+ result.expectedSha256 = resource.source.sha256;
1738
+ let message;
1739
+ try {
1740
+ const checked = await checkLocalResource(root, resource.source.path, resource.source.sha256);
1741
+ result.actualSha256 = checked.actualSha256;
1742
+ result.status = resource.source.sha256 ? "verified" : "present";
1743
+ if (resource.source.sha256 && checked.actualSha256 !== resource.source.sha256) {
1744
+ result.status = "hash_mismatch";
1745
+ message = "Resource SHA-256 does not match resources.json.";
1746
+ } else if (!resource.source.sha256) warnings.push(`Resource ${resource.id}: local file is present, but its content hash is not pinned.`);
1747
+ } catch (error) {
1748
+ result.status = error.code === "ENOENT" ? "missing" : "invalid";
1749
+ message = result.status === "missing" ? "Local resource is missing." : `Cannot safely verify resource: ${error.message}`;
1750
+ }
1751
+ if (message) {
1752
+ const detail = `Resource ${resource.id}: ${message}`;
1753
+ if (resource.required) localIssues.push({ path: resource.source.path, message: detail });
1754
+ else warnings.push(`Optional ${detail}`);
1755
+ }
1756
+ }
1757
+ const issues = [...structural.issues, ...localIssues];
1758
+ return {
1759
+ root,
1760
+ id,
1761
+ ...structural,
1762
+ valid: issues.length === 0,
1763
+ issues,
1764
+ warnings,
1765
+ localIssues,
1766
+ resources,
1767
+ mediaUploaded: false,
1768
+ quality: "not_assessed"
1769
+ };
1770
+ }
1771
+
1345
1772
  // packages/cli/src/index.ts
1346
1773
  var HELP = `EditorBrain \u2014 portable editorial context for AI video agents
1347
1774
 
@@ -1361,7 +1788,18 @@ editorbrain pull [--project id | --all] [--dir folder] [--dry-run]
1361
1788
  editorbrain push [--dir folder] [--dry-run]
1362
1789
  editorbrain projects list
1363
1790
  editorbrain documents list [--prefix path]
1364
- editorbrain documents get <path>
1791
+ editorbrain documents get <path> [--revision N]
1792
+ editorbrain documents history <path> [--before N]
1793
+ editorbrain documents restore <path> --revision N --expected-revision N
1794
+ editorbrain memory list --scope brand|style|project --id ID
1795
+ editorbrain memory add --scope brand|style|project --id ID --text TEXT --status suggested|confirmed --expected-revision N
1796
+ editorbrain memory confirm|disable ENTRY --scope brand|style|project --id ID --expected-revision N
1797
+ editorbrain feedback add --project ID --text TEXT
1798
+ editorbrain improvements list --brand ID
1799
+ editorbrain improvements accept RUN --index N --expected-revision N
1800
+ editorbrain review list --project ID
1801
+ editorbrain review create --project ID --asset SHA --title TEXT --kind sample|render --days 7 --share-authorized
1802
+ editorbrain review revoke ID
1365
1803
  editorbrain workflow check [--project id] [--previous-version hash]
1366
1804
  editorbrain search brief --file <editorial-brief.json> [--project id] [--mode styles|events|both] [--review direct|directions|sample]
1367
1805
  editorbrain search edit [--script file | --video file | --asset sha256 | --project id] [--requirements text] [--assets tag,tag] [--mode styles|events|both] [--review direct|directions|sample]
@@ -1371,6 +1809,8 @@ editorbrain library media <video-id> --sha256 media-hash --out file
1371
1809
  editorbrain narrative search [query] [--kind hook|cta|...] [--channel visual|spoken|...] [--limit 10]
1372
1810
  editorbrain narrative get <video-id> [--sha256 document-hash] [--event-offset 0] [--relation-offset 0] [--limit 50]
1373
1811
  editorbrain styles list
1812
+ editorbrain styles create <style-id> [--name name] [--dir folder]
1813
+ editorbrain styles check <style-id> [--dir folder]
1374
1814
  editorbrain styles recommend <project-id> [--assets tag,tag] [--capabilities tag,tag] [--style pack-id] [--duration seconds]
1375
1815
  editorbrain direction draft <project-id> [--assets tag,tag] [--capabilities tag,tag] [--style pack-id] [--duration seconds] [--out new-folder]
1376
1816
  editorbrain assets list [--limit 200] [--cursor opaque-cursor]
@@ -1385,14 +1825,15 @@ Secrets go to a user config with mode 0600, never the portable project.
1385
1825
  Resolve writes the source-hash lock. Export produces a handoff, not a rendered video.
1386
1826
  Push/pull preserve conflicting changes; media uploads use the separate assets command.
1387
1827
  Skill install works offline, preserves existing instructions, and defaults to both agents.
1828
+ Styles create/check work locally without login or media upload; checks do not certify render quality.
1388
1829
  Keys create stores the returned secret in the private user config; stdout has no key.
1389
1830
  Output is JSON except help; login progress/code goes to stderr. --url overrides the configured endpoint.
1390
1831
  `;
1391
1832
  function parseArgs(argv) {
1392
1833
  const positional = [];
1393
1834
  const flags = {};
1394
- const booleans = /* @__PURE__ */ new Set(["help", "version", "offline", "dry-run", "key-stdin", "invite-stdin", "no-browser", "skip-mcp", "all"]);
1395
- const known = /* @__PURE__ */ new Set(["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", "file", ...booleans]);
1835
+ const booleans = /* @__PURE__ */ new Set(["help", "version", "offline", "dry-run", "key-stdin", "invite-stdin", "no-browser", "skip-mcp", "all", "share-authorized"]);
1836
+ const known = /* @__PURE__ */ new Set(["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", "file", "revision", "expected-revision", "before", "scope", "id", "text", "conditions", "status", "index", "title", "days", ...booleans]);
1396
1837
  for (let i = 0; i < argv.length; i++) {
1397
1838
  const arg = argv[i];
1398
1839
  if (!arg.startsWith("--")) {
@@ -1421,13 +1862,13 @@ async function configRoot(create = false) {
1421
1862
  let path = process.env.EDITORBRAIN_CONFIG_DIR ?? process.env.EDITORHARNESS_CONFIG_DIR ?? join2(homedir(), ".config", "editorbrain");
1422
1863
  if (!create && !process.env.EDITORBRAIN_CONFIG_DIR && !process.env.EDITORHARNESS_CONFIG_DIR) {
1423
1864
  try {
1424
- await lstat4(path);
1865
+ await lstat5(path);
1425
1866
  } catch (error) {
1426
1867
  if (error.code !== "ENOENT") throw error;
1427
1868
  path = join2(homedir(), ".config", "editorharness");
1428
1869
  }
1429
1870
  }
1430
- if (create) await mkdir2(path, { recursive: true, mode: 448 });
1871
+ if (create) await mkdir3(path, { recursive: true, mode: 448 });
1431
1872
  return ensureRoot(path);
1432
1873
  }
1433
1874
  async function loadConfig() {
@@ -1472,7 +1913,7 @@ async function init(args) {
1472
1913
  for (const path of Object.keys(files)) {
1473
1914
  const target = await checkedPath(root, path);
1474
1915
  try {
1475
- await lstat4(target);
1916
+ await lstat5(target);
1476
1917
  throw new Error(`Refusing to replace existing file: ${path}`);
1477
1918
  } catch (error) {
1478
1919
  if (error.code !== "ENOENT") throw error;
@@ -1484,13 +1925,13 @@ async function init(args) {
1484
1925
  for (const path of directories) {
1485
1926
  const target = await checkedPath(root, path, false);
1486
1927
  try {
1487
- if (!(await lstat4(target)).isDirectory()) throw new Error(`Not a directory: ${path}`);
1928
+ if (!(await lstat5(target)).isDirectory()) throw new Error(`Not a directory: ${path}`);
1488
1929
  } catch (error) {
1489
1930
  if (error.code !== "ENOENT") throw error;
1490
1931
  }
1491
1932
  }
1492
1933
  for (const [path, content] of Object.entries(files)) await atomicWrite(root, path, content, { overwrite: false });
1493
- for (const path of directories) await mkdir2(await checkedPath(root, path, false), { recursive: true });
1934
+ for (const path of directories) await mkdir3(await checkedPath(root, path, false), { recursive: true });
1494
1935
  const guidance = await installGuidance();
1495
1936
  const entryPoints = guidance.readInstructions.filter((item) => item.entryPointStatus === "created").map((item) => item.entryPoint);
1496
1937
  output({ root, created: Object.keys(files), entryPoints, guidance, next: `Edit brand tokens and brief, then run editorbrain resolve ${project}` });
@@ -1596,6 +2037,20 @@ async function main() {
1596
2037
  output({ endpoint: client.url, ...await client.me() });
1597
2038
  return;
1598
2039
  }
2040
+ if (command === "styles" && (subcommand === "create" || subcommand === "check")) {
2041
+ const allowed = new Set(subcommand === "create" ? ["dir", "name"] : ["dir"]);
2042
+ if (args.positional.length !== 3 || Object.keys(args.flags).some((flag) => !allowed.has(flag))) {
2043
+ throw new CliError("INVALID_ARGUMENT", `Use styles ${subcommand} <style-id> [--dir folder]${subcommand === "create" ? " [--name name]" : ""}.`, "Style authoring runs locally and accepts only the documented options.");
2044
+ }
2045
+ const options = { directory: str(args, "dir") ?? process.cwd(), id: argument };
2046
+ if (subcommand === "create") output(await createLocalStyle({ ...options, name: str(args, "name") }));
2047
+ else {
2048
+ const result = await checkLocalStyle(options);
2049
+ output(result);
2050
+ if (!result.valid) process.exitCode = 1;
2051
+ }
2052
+ return;
2053
+ }
1599
2054
  if (command === "styles" || command === "direction") {
1600
2055
  const client = new Client(await config(args));
1601
2056
  if (command === "styles" && subcommand === "list") {
@@ -1621,7 +2076,7 @@ async function main() {
1621
2076
  const outRoot = await ensureRoot(resolve4(str(args, "dir") ?? process.cwd(), destination), true);
1622
2077
  for (const path of Object.keys(result.files)) {
1623
2078
  try {
1624
- await lstat4(await checkedPath(outRoot, path, false));
2079
+ await lstat5(await checkedPath(outRoot, path, false));
1625
2080
  throw new Error(`Refusing to replace existing proposal: ${path}`);
1626
2081
  } catch (error) {
1627
2082
  if (error.code !== "ENOENT") throw error;
@@ -1667,12 +2122,57 @@ async function main() {
1667
2122
  return;
1668
2123
  }
1669
2124
  if (subcommand === "get" && argument) {
1670
- const doc = await client.get(argument);
2125
+ const doc = await client.request(`/v1/documents/${argument.split("/").map(encodeURIComponent).join("/")}${str(args, "revision") ? "?revision=" + encodeURIComponent(str(args, "revision")) : ""}`);
1671
2126
  if (await sha256(doc.content) !== doc.sha256) throw new Error("Document integrity failed");
1672
2127
  output(doc);
1673
2128
  return;
1674
2129
  }
1675
- throw new Error("Use documents list --prefix path or documents get <path>.");
2130
+ if (subcommand === "history" && argument) {
2131
+ output(await client.request("/v1/history?" + new URLSearchParams({ path: argument, ...str(args, "before") ? { before: str(args, "before") } : {} })));
2132
+ return;
2133
+ }
2134
+ if (subcommand === "restore" && argument) {
2135
+ output(await client.request("/v1/history/restore", { method: "POST", body: { path: argument, revision: Number(str(args, "revision")), expectedRevision: Number(str(args, "expected-revision")) } }));
2136
+ return;
2137
+ }
2138
+ throw new Error("Use documents list/get/history/restore. Restore requires --revision and --expected-revision.");
2139
+ }
2140
+ if (command === "memory" || command === "feedback" || command === "review" || command === "improvements") {
2141
+ const client = new Client(await config(args));
2142
+ if (command === "memory" && subcommand === "list") {
2143
+ output(await client.request("/v1/memory?" + new URLSearchParams({ scope: str(args, "scope") ?? "brand", scopeId: str(args, "id") ?? "" })));
2144
+ return;
2145
+ }
2146
+ if (command === "memory" && ["add", "confirm", "disable"].includes(subcommand ?? "")) {
2147
+ output(await client.request("/v1/memory", { method: "POST", body: { scope: str(args, "scope") ?? "brand", scopeId: str(args, "id"), action: subcommand, entryId: argument, expectedRevision: Number(str(args, "expected-revision")), text: str(args, "text"), conditions: str(args, "conditions") ?? "", status: str(args, "status") ?? "suggested" } }));
2148
+ return;
2149
+ }
2150
+ if (command === "feedback" && subcommand === "add") {
2151
+ output(await client.request("/v1/feedback", { method: "POST", body: { projectId: str(args, "project"), text: str(args, "text") } }));
2152
+ return;
2153
+ }
2154
+ if (command === "improvements" && subcommand === "list") {
2155
+ output(await client.request("/v1/improvements?" + new URLSearchParams({ brandId: str(args, "brand") ?? "" })));
2156
+ return;
2157
+ }
2158
+ if (command === "improvements" && subcommand === "accept") {
2159
+ output(await client.request("/v1/improvements/accept", { method: "POST", body: { runId: argument, index: Number(str(args, "index")), expectedRevision: Number(str(args, "expected-revision")) } }));
2160
+ return;
2161
+ }
2162
+ if (command === "review" && subcommand === "list") {
2163
+ output(await client.request("/v1/reviews?" + new URLSearchParams({ projectId: str(args, "project") ?? "" })));
2164
+ return;
2165
+ }
2166
+ if (command === "review" && subcommand === "revoke" && argument) {
2167
+ output(await client.request("/v1/reviews/" + encodeURIComponent(argument), { method: "DELETE" }));
2168
+ return;
2169
+ }
2170
+ if (command === "review" && subcommand === "create") {
2171
+ if (!args.flags["share-authorized"]) throw new Error("Sharing requires explicit user authorization and --share-authorized. Anyone with the link can view.");
2172
+ output(await client.request("/v1/reviews", { method: "POST", body: { projectId: str(args, "project"), assetId: str(args, "asset"), title: str(args, "title"), kind: str(args, "kind") ?? "sample", days: Number(str(args, "days") ?? 7), shareAuthorized: true } }));
2173
+ return;
2174
+ }
2175
+ throw new Error("Use memory list/add/confirm/disable, feedback add, improvements list/accept, review list/create/revoke. See help for flags.");
1676
2176
  }
1677
2177
  if (command === "assets") {
1678
2178
  const client = new Client(await config(args));
@@ -1690,7 +2190,7 @@ async function main() {
1690
2190
  }
1691
2191
  if (subcommand === "upload" && argument) {
1692
2192
  const target = resolve4(str(args, "dir") ?? process.cwd(), argument);
1693
- const handle = await open4(target, constants4.O_RDONLY | constants4.O_NOFOLLOW);
2193
+ const handle = await open5(target, constants5.O_RDONLY | constants5.O_NOFOLLOW);
1694
2194
  let bytes;
1695
2195
  try {
1696
2196
  const info = await handle.stat();
@@ -1712,7 +2212,7 @@ async function main() {
1712
2212
  if (subcommand === "download" && argument && str(args, "out")) {
1713
2213
  if (!/^[a-f0-9]{64}$/.test(argument)) throw new Error("Asset ID must be its SHA-256");
1714
2214
  const destination = resolve4(str(args, "dir") ?? process.cwd(), str(args, "out"));
1715
- const root2 = await ensureRoot(dirname2(destination), true);
2215
+ const root2 = await ensureRoot(dirname3(destination), true);
1716
2216
  await checkedPath(root2, basename(destination), false);
1717
2217
  const response = await client.media(`/v1/assets/${argument}`);
1718
2218
  const bytes = new Uint8Array(await response.arrayBuffer());
@@ -1747,7 +2247,7 @@ async function main() {
1747
2247
  if (command === "search" && subcommand === "brief") {
1748
2248
  const path = str(args, "file");
1749
2249
  if (!path) throw new Error("Provide --file with the editorial brief JSON after inspecting the actual material.");
1750
- const handle = await open4(resolve4(path), constants4.O_RDONLY | constants4.O_NOFOLLOW);
2250
+ const handle = await open5(resolve4(path), constants5.O_RDONLY | constants5.O_NOFOLLOW);
1751
2251
  let brief;
1752
2252
  try {
1753
2253
  const stat = await handle.stat();
@@ -1769,7 +2269,7 @@ async function main() {
1769
2269
  const scriptPath = str(args, "script"), videoPath = str(args, "video");
1770
2270
  let script, assetId = str(args, "asset");
1771
2271
  const readInput = async (path, max) => {
1772
- const handle = await open4(resolve4(path), constants4.O_RDONLY | constants4.O_NOFOLLOW);
2272
+ const handle = await open5(resolve4(path), constants5.O_RDONLY | constants5.O_NOFOLLOW);
1773
2273
  try {
1774
2274
  const stat = await handle.stat();
1775
2275
  if (!stat.isFile() || stat.size < 1 || stat.size > max) throw new Error(`Input must be a regular file of at most ${max} bytes.`);
@@ -1818,7 +2318,7 @@ async function main() {
1818
2318
  const hash = str(args, "sha256");
1819
2319
  if (!hash || !/^[a-f0-9]{64}$/.test(hash)) throw new Error("--sha256 must be a media SHA-256 from library get.");
1820
2320
  const destination = resolve4(str(args, "dir") ?? process.cwd(), str(args, "out"));
1821
- const root2 = await ensureRoot(dirname2(destination), true);
2321
+ const root2 = await ensureRoot(dirname3(destination), true);
1822
2322
  await checkedPath(root2, basename(destination), false);
1823
2323
  const response = await client.media(`/v1/library/${encodeURIComponent(argument)}/media/${hash}`);
1824
2324
  const reader = response.body?.getReader();