editorbrain 0.11.0 → 0.12.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.
@@ -52,7 +52,7 @@ function validateStyleResources(value, expectedId) {
52
52
  const fields = (entry, allowed, label) => {
53
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
54
  };
55
- const text = (value2, label) => {
55
+ const text2 = (value2, label) => {
56
56
  if (!nonempty(value2)) add(`${label} must be nonempty text`);
57
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
58
  };
@@ -74,7 +74,7 @@ function validateStyleResources(value, expectedId) {
74
74
  else if (ids.has(resource.id)) add(`Duplicate resource id: ${resource.id}`);
75
75
  else ids.add(resource.id);
76
76
  if (!kinds.includes(resource.kind)) add(`${label} has an invalid kind`);
77
- text(resource.purpose, `${label} purpose`);
77
+ text2(resource.purpose, `${label} purpose`);
78
78
  if (typeof resource.required !== "boolean") add(`${label} required must be boolean`);
79
79
  const source = resource.source;
80
80
  if (!object(source)) {
@@ -94,7 +94,7 @@ function validateStyleResources(value, expectedId) {
94
94
  if (typeof source.assetId !== "string" || !digest.test(source.assetId)) add(`${label} assetId must be a 64-character lowercase hexadecimal digest`);
95
95
  } else if (source.type === "requirement") {
96
96
  fields(source, ["type", "description"], `${label} requirement source`);
97
- text(source.description, `${label} requirement description`);
97
+ text2(source.description, `${label} requirement description`);
98
98
  } else add(`${label} source must be local, asset or requirement`);
99
99
  }
100
100
  }
@@ -151,21 +151,21 @@ function checkStyle(files, id) {
151
151
  "Preview the implemented edit: verify timing anchors, movement, legibility, audio and when each technique should be omitted.",
152
152
  "Verify resource availability, local bytes, remote workspace authorization, provenance and reuse rights. Structural validation does not certify these."
153
153
  ];
154
- const finish = (documents2) => ({ valid: issues.length === 0, issues, warnings, documents: documents2, manualChecks });
154
+ const finish2 = (documents2) => ({ valid: issues.length === 0, issues, warnings, documents: documents2, manualChecks });
155
155
  const base = `.editorbrain/styles/${id}/`;
156
156
  try {
157
157
  assertIdentifier(id);
158
158
  safeDocumentPath(`${base}style.json`);
159
159
  } catch (error) {
160
160
  issues.push({ path: base, message: error.message });
161
- return finish([]);
161
+ return finish2([]);
162
162
  }
163
163
  let selected;
164
164
  try {
165
165
  selected = canonicalFiles(Object.fromEntries(Object.entries(files).filter(([path]) => canonicalDocumentPath(path).startsWith(base))));
166
166
  } catch (error) {
167
167
  issues.push({ path: base, message: error.message });
168
- return finish([]);
168
+ return finish2([]);
169
169
  }
170
170
  const documents = Object.keys(selected).sort();
171
171
  for (const name of ["style.json", "style.md", "examples.json"]) {
@@ -197,7 +197,138 @@ function checkStyle(files, id) {
197
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
198
  }
199
199
  }
200
- return finish(documents);
200
+ return finish2(documents);
201
+ }
202
+
203
+ // packages/core/src/production-plan.ts
204
+ var record = (v) => !!v && typeof v === "object" && !Array.isArray(v);
205
+ var positive = (v) => typeof v === "number" && Number.isFinite(v) && v > 0;
206
+ var nonnegative = (v) => v === 0 || positive(v);
207
+ var hash = (v) => typeof v === "string" && /^[a-f0-9]{64}$/.test(v);
208
+ var text = (v) => typeof v === "string" && v.trim().length > 0 && v.length <= 2e3;
209
+ var range = (v) => record(v) && nonnegative(v.startSeconds) && positive(v.endSeconds) && v.endSeconds > v.startSeconds;
210
+ var MAX_ENTRIES = 2e3;
211
+ function validateProductionPlan(value) {
212
+ const issues = [];
213
+ if (!record(value) || value.schemaVersion !== 1 || value.kind !== "editorbrain.production-plan") return { valid: false, issues: ["Expected schemaVersion 1 and kind editorbrain.production-plan."] };
214
+ if (Object.keys(value).some((key) => !["schemaVersion", "kind", "inspections", "uses"].includes(key))) issues.push("Unknown plan fields; keep editorial notes in direction.md and use the documented plan schema.");
215
+ if (!Array.isArray(value.inspections) || value.inspections.length > MAX_ENTRIES) issues.push("inspections must be an array of at most 2000 observations.");
216
+ else value.inspections.forEach((item, index) => {
217
+ const label = `inspections[${index}]`;
218
+ if (!record(item) || !hash(item.sourceSha256) || !["visual", "motion", "audio"].includes(String(item.modality)) || !["sampled_frames", "playback", "listening"].includes(String(item.method))) {
219
+ issues.push(`${label}: invalid source hash, modality or method.`);
220
+ return;
221
+ }
222
+ if (Object.keys(item).some((key) => !["sourceSha256", "modality", "method", "intervals", "sampledSeconds", "note"].includes(key))) issues.push(`${label}: unsupported field.`);
223
+ if (item.note !== void 0 && !text(item.note)) issues.push(`${label}: note must be nonempty text of at most 2000 characters.`);
224
+ if (item.method === "sampled_frames") {
225
+ 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.`);
226
+ } else {
227
+ if (item.sampledSeconds !== void 0 || !Array.isArray(item.intervals) || !item.intervals.length || item.intervals.length > MAX_ENTRIES || !item.intervals.every(range)) issues.push(`${label}: playback/listening requires valid intervals, not sampledSeconds.`);
228
+ if (item.method === "listening" && item.modality !== "audio" || item.method === "playback" && item.modality === "audio") issues.push(`${label}: listening covers audio; playback covers visual or motion. Record separately what was actually inspected.`);
229
+ }
230
+ });
231
+ if (!Array.isArray(value.uses) || value.uses.length > MAX_ENTRIES) issues.push("uses must be an array of at most 2000 source uses.");
232
+ else {
233
+ const ids = /* @__PURE__ */ new Set();
234
+ value.uses.forEach((item, index) => {
235
+ const label = `uses[${index}]`;
236
+ if (!record(item) || typeof item.id !== "string" || !/^[a-zA-Z0-9_-]{1,80}$/.test(item.id) || !hash(item.sourceSha256) || !range(item.source) || !range(item.timeline)) {
237
+ issues.push(`${label}: requires ID, source hash, source and timeline intervals.`);
238
+ return;
239
+ }
240
+ if (ids.has(item.id)) issues.push(`${label}: duplicate use ID ${item.id}.`);
241
+ ids.add(item.id);
242
+ if (Object.keys(item).some((key) => !["id", "sourceSha256", "source", "timeline", "purpose", "reuseReason"].includes(key))) issues.push(`${label}: unsupported field.`);
243
+ for (const key of ["purpose", "reuseReason"]) if (item[key] !== void 0 && !text(item[key])) issues.push(`${label}: ${key} must be nonempty text of at most 2000 characters.`);
244
+ });
245
+ }
246
+ return { valid: !issues.length, issues };
247
+ }
248
+ function mergeProductionIntervals(intervals) {
249
+ const merged = [];
250
+ for (const interval of [...intervals].sort((a, b) => a.startSeconds - b.startSeconds)) {
251
+ const last = merged.at(-1);
252
+ if (last && interval.startSeconds <= last.endSeconds) last.endSeconds = Math.max(last.endSeconds, interval.endSeconds);
253
+ else merged.push({ ...interval });
254
+ }
255
+ return merged;
256
+ }
257
+ function checkProductionPlan(inventory, value) {
258
+ const checked = validateProductionPlan(value);
259
+ const findings = checked.issues.map((message) => ({ code: "INVALID_PLAN", severity: "error", message }));
260
+ const sources = /* @__PURE__ */ new Map();
261
+ let inventoryComplete = true;
262
+ if (!record(inventory) || inventory.schemaVersion !== 1 || inventory.kind !== "editorbrain.media-inventory" || inventory.summary === true || typeof inventory.complete !== "boolean" || !Array.isArray(inventory.assets) || inventory.assets.length > 1e4) {
263
+ inventoryComplete = false;
264
+ findings.push({ code: "INVALID_INVENTORY", severity: "error", message: "Use the full schemaVersion 1 media inventory saved with --out, not compact stdout. It must declare complete." });
265
+ } else {
266
+ if (inventory.complete === false || Array.isArray(inventory.failures) && inventory.failures.length) {
267
+ inventoryComplete = false;
268
+ findings.push({ code: "INVENTORY_INCOMPLETE", severity: "warning", message: "Inventory traversal or probing was incomplete. This plan check covers only recorded assets." });
269
+ }
270
+ for (const asset of inventory.assets) {
271
+ if (!record(asset) || !hash(asset.sha256) || !(asset.durationSeconds === null || nonnegative(asset.durationSeconds)) || asset.streams !== void 0 && (!Array.isArray(asset.streams) || asset.streams.some((s) => !record(s) || typeof s.type !== "string"))) {
272
+ findings.push({ code: "INVALID_INVENTORY", severity: "error", message: "Inventory assets need a source hash, nonnegative duration or null, and valid optional streams." });
273
+ continue;
274
+ }
275
+ const existing = sources.get(asset.sha256);
276
+ if (existing && existing.durationSeconds !== asset.durationSeconds) findings.push({ code: "INVENTORY_CONFLICT", severity: "error", message: "The same content hash has conflicting durations.", sourceSha256: asset.sha256 });
277
+ sources.set(asset.sha256, asset);
278
+ }
279
+ }
280
+ if (findings.some((f) => f.severity === "error")) return { schemaVersion: 1, kind: "editorbrain.production-plan-check", valid: false, inventoryComplete, findings, coverage: [], sourceFilesReverified: false, quality: "not_assessed", observations: "agent_declared" };
281
+ const plan = value;
282
+ const sourceFor = (sourceSha256) => {
283
+ const source = sources.get(sourceSha256);
284
+ if (!source) findings.push({ code: "UNKNOWN_SOURCE", severity: "error", message: "A source hash is absent from the selected inventory. Refresh the inventory after changing media.", sourceSha256 });
285
+ return source;
286
+ };
287
+ const inBounds = (end, source) => source.durationSeconds === null || end <= source.durationSeconds + 1e-3;
288
+ for (const inspection of plan.inspections) {
289
+ const source = sourceFor(inspection.sourceSha256);
290
+ if (!source) continue;
291
+ if (source.streams && !source.streams.some((s) => s.type === (inspection.modality === "audio" ? "audio" : "video") && (inspection.modality !== "motion" || !s.attachedPicture))) findings.push({ code: "MODALITY_ABSENT", severity: "error", message: `Declared ${inspection.modality} inspection has no matching stream.`, sourceSha256: source.sha256 });
292
+ if ([...inspection.sampledSeconds ?? [], ...(inspection.intervals ?? []).map((r) => r.endSeconds)].some((end) => !inBounds(end, source))) findings.push({ code: "INSPECTION_OUT_OF_BOUNDS", severity: "error", message: "Inspection extends past the source duration.", sourceSha256: source.sha256 });
293
+ }
294
+ const validUses = [];
295
+ for (const use of plan.uses) {
296
+ const source = sourceFor(use.sourceSha256);
297
+ if (!source) continue;
298
+ if (!inBounds(use.source.endSeconds, source)) findings.push({ code: "USE_OUT_OF_BOUNDS", severity: "error", message: "Source interval extends past the source duration.", sourceSha256: source.sha256, useIds: [use.id], interval: use.source });
299
+ else validUses.push(use);
300
+ }
301
+ for (const use of validUses) {
302
+ const source = sources.get(use.sourceSha256);
303
+ const audioOnly = source.streams?.some((s) => s.type === "audio") && !source.streams.some((s) => s.type === "video" && !s.attachedPicture);
304
+ const inspected = mergeProductionIntervals(plan.inspections.filter((i) => i.sourceSha256 === source.sha256 && (audioOnly ? i.modality === "audio" : i.modality === "visual" || i.modality === "motion")).flatMap((i) => i.intervals ?? []).filter((r) => inBounds(r.endSeconds, source)));
305
+ const covered = inspected.reduce((sum, r) => sum + Math.max(0, Math.min(use.source.endSeconds, r.endSeconds) - Math.max(use.source.startSeconds, r.startSeconds)), 0);
306
+ const missing = use.source.endSeconds - use.source.startSeconds - covered;
307
+ if (missing > 1e-3) findings.push({ code: "USED_RANGE_COVERAGE_GAP", severity: source.durationSeconds === null ? "info" : "warning", message: `${Math.round(missing * 1e3) / 1e3}s of this source use lack declared continuous ${audioOnly ? "audio" : "visual/motion"} inspection. Review relevant content when needed; snapshots do not establish interval coverage. This is not a requirement to inspect every unused source.`, sourceSha256: source.sha256, useIds: [use.id], interval: use.source });
308
+ }
309
+ let repeatedPairs = 0;
310
+ for (let i = 0; i < validUses.length; i++) for (let j = i + 1; j < validUses.length; j++) {
311
+ const a = validUses[i], b = validUses[j];
312
+ if (a.sourceSha256 !== b.sourceSha256) continue;
313
+ const startSeconds = Math.max(a.source.startSeconds, b.source.startSeconds), endSeconds = Math.min(a.source.endSeconds, b.source.endSeconds);
314
+ if (endSeconds - startSeconds <= 1e-3) continue;
315
+ repeatedPairs++;
316
+ if (repeatedPairs <= 200) findings.push({ code: b.reuseReason ? "SOURCE_REUSE_EXPLAINED" : "SOURCE_REUSE_REVIEW", severity: b.reuseReason ? "info" : "warning", message: b.reuseReason ? `Declared reuse: ${b.reuseReason}` : "These uses overlap the same source interval. Review whether the callback, comparison or repeated action serves the edit.", sourceSha256: a.sourceSha256, useIds: [a.id, b.id], interval: { startSeconds, endSeconds } });
317
+ }
318
+ if (repeatedPairs > 200) findings.push({ code: "REUSE_DETAILS_LIMIT", severity: "info", message: `${repeatedPairs} repeated pairs detected; only the first 200 are detailed.` });
319
+ const coverage = [...sources.values()].map((source) => {
320
+ const byModality = ["visual", "motion", "audio"].map((modality) => {
321
+ const observations = plan.inspections.filter((i) => i.sourceSha256 === source.sha256 && i.modality === modality);
322
+ const intervals = mergeProductionIntervals(observations.flatMap((i) => i.intervals ?? []).filter((r) => inBounds(r.endSeconds, source)));
323
+ const seconds = intervals.reduce((sum, i) => sum + i.endSeconds - i.startSeconds, 0);
324
+ return { modality, intervals, seconds, fraction: !source.durationSeconds ? null : seconds / source.durationSeconds, sampledSeconds: [...new Set(observations.flatMap((i) => i.sampledSeconds ?? []))].sort((a, b) => a - b) };
325
+ });
326
+ const sourceUses = validUses.filter((use) => use.sourceSha256 === source.sha256);
327
+ if (!plan.inspections.some((i) => i.sourceSha256 === source.sha256)) findings.push({ code: sourceUses.length ? "USED_SOURCE_UNINSPECTED" : "SOURCE_UNINSPECTED", severity: sourceUses.length ? "warning" : "info", message: sourceUses.length ? "Used source has no declared content inspection; metadata is not content review." : "Source has no declared content inspection. Using every source is not required.", sourceSha256: source.sha256 });
328
+ if (source.durationSeconds === null) findings.push({ code: "DURATION_UNKNOWN", severity: "info", message: "Duration unknown; interval bounds and coverage fraction cannot be verified for this source.", sourceSha256: source.sha256 });
329
+ return { sourceSha256: source.sha256, durationSeconds: source.durationSeconds, uses: sourceUses.length, byModality };
330
+ });
331
+ return { schemaVersion: 1, kind: "editorbrain.production-plan-check", valid: !findings.some((f) => f.severity === "error"), inventoryComplete, findings, coverage, repeatedPairs, sourceFilesReverified: false, quality: "not_assessed", observations: "agent_declared" };
201
332
  }
202
333
 
203
334
  // packages/core/src/index.ts
@@ -985,7 +1116,7 @@ import { createHash } from "node:crypto";
985
1116
  import { constants as constants2 } from "node:fs";
986
1117
  import { lstat as lstat2, open as open2, readdir as readdir2, rename as rename2 } from "node:fs/promises";
987
1118
  import { fileURLToPath } from "node:url";
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"] };
1119
+ var previousSkillHashes = { "SKILL.md": ["9df2c89deeec30ddf556c359027efe1d90e6b4e9c08717cd8827e210b38a187f", "67b157cb45cae3621e6f2c43235c1d69907ad6abd45ab6b52a3c65f236732db9", "1e2fff65a1b303b18b2d65156bef6eb2439a19d77ba2a25b37bcb1819ff009e3", "dacce1c4127f4281df6f7e1681e243444bb6f8a8e74b829e52ee0108c6672348", "09740ab4b275eada65b5040ae6000104ff39094f534a2784e67feb2afc2a59b3", "f18269ad5504fff8a4fc522d463483261f5041e56cb7f1081f9614bc67e0d96f"], "AGENTS.md": ["4f3ef860ade3a31aa7c6a6aab5cf6ec2a700850bf19ea2320324c12c2e0fbf5b", "1b07de01698a0040b47ea0eddb1b017f70a69e81acd0bf9710647ca5e02f5125", "b5ee3439ff974c668755816149fab76899d7ad9266f63978b30fbc9b7d2cd640", "bacedb7bdedab5abe75c9601717cfdb05a96208fa2e2a42af878e2dfd2ae4251", "ea3b473179857ccc07e9e4f21a6ede9e0906a09e495f4a051069d15aee1556cd", "faa2bba99ae3023be7f588d02177a4abf583d3a8a65f617b8cff7a3ceed09dfc"], "references/cli-mcp.md": ["5d3a79343fbd76cdae5bdbe12fb9c944d5dff4f489ac6534fd96e0cb8630b988", "8e011c31368cef2117a28ee972b39aab9535eeb0f601a4a0f9d6fc019b22c1c2", "666082653efc000f8a7cc5f1836c80bbdda5f0f57cdbbdcb393f2b84aeb93a01", "78b737796a8685b1135e0acb91b620c51845b62e31dba04eb51dd9dd38b41008", "8b2e01bfd937237919f5ade794eed01dc6e32b556ede9de3a0ccc28cbdd988d1", "ca88b2816a795a5ebe834074b178469367634b80ad837e3697182ea585af1c05", "490daf154b428b536d0a6f590030c854b2fc8ca6adb995c6ecd5a162f85e471e"], "references/production-and-review.md": ["055c6df9e0f167be148b0a5c0bba5b43e108eb1f58f478d88b3536c6589ac7b3"], "references/workspace-files.md": ["e72af2871f42cd776be1bfff8009ed5562780e7f8a65b12c27887f3880631f81", "00780c64bd0244c6afb18d786b7512b43e315c376e8f88c67d6ba53d91275ccf", "97053b092e0ff36e3cfb764095fcb4a4fca94e722567e774b53bc118279f40e5", "70fc660e7ed3d70c16c33c62c8e0399b99c3251808f093ef3d4ea5e8d39e4c8a", "28c62c1e28842142607f5168ee79b18fd797900dd66dd9ac5eb3cfcf8a09f781", "b86eae85cf86dbf4409b10f4d8df1f1649fc408ce1a240f7cf26d5bb69aabfd1"], "references/first-edit.md": ["cb1e9d3272b2b22ae095aed02e33067c646da48e7b64cf71ff81197f6e67b673"], "references/memory-and-review.md": ["51a274e522270f1fc295d0c0b9a1c23672ce01abea097c2499c9a8db56c0c289"], "references/search-brief.md": ["137203383a7873593136428520f5f9cd6ce1ab6cfeab365c277175786eba3ab3"], "references/style-creator.md": ["1b1bc289a5bc308cbbf7f4d699de2933b8934733a2b69b3e98fb1b7420d986f2"] };
989
1120
  var destinations = {
990
1121
  codex: { directory: ".agents/skills/editorbrain", entryPoint: "AGENTS.md" },
991
1122
  claude: { directory: ".claude/skills/editorbrain", entryPoint: "CLAUDE.md" }
@@ -1034,6 +1165,9 @@ async function loadSkill(directory) {
1034
1165
  if (!files.has("SKILL.md")) throw new Error("Bundled EditorBrain skill is missing SKILL.md. Reinstall the CLI package.");
1035
1166
  return files;
1036
1167
  }
1168
+ async function bundledGuidanceFiles() {
1169
+ return [...(await loadSkill(bundledSkillDirectory())).keys()];
1170
+ }
1037
1171
  async function prepareAgentGuidance(root, agent = "both", sourceDirectory = bundledSkillDirectory()) {
1038
1172
  root = await ensureRoot(root);
1039
1173
  parseAgentTarget(agent);
@@ -1175,7 +1309,6 @@ async function cliVersion() {
1175
1309
  const metadata = JSON.parse(await readFile(new URL("../package.json", import.meta.url), "utf8"));
1176
1310
  return metadata.version;
1177
1311
  }
1178
- var guidanceFiles = ["SKILL.md", "AGENTS.md", "references/cli-mcp.md", "references/production-and-review.md", "references/workspace-files.md"];
1179
1312
  async function diagnose(options, fetcher = fetch) {
1180
1313
  const checks = [];
1181
1314
  let root;
@@ -1203,7 +1336,7 @@ async function diagnose(options, fetcher = fetch) {
1203
1336
  const directory = agent === "codex" ? ".agents/skills/editorbrain" : ".claude/skills/editorbrain";
1204
1337
  let different = false;
1205
1338
  try {
1206
- for (const file of guidanceFiles) {
1339
+ for (const file of await bundledGuidanceFiles()) {
1207
1340
  const current = await readText(root, `${directory}/${file}`, false);
1208
1341
  const bundled = await readFile(resolve2(bundledSkillDirectory(), file), "utf8");
1209
1342
  if (current !== bundled) different = true;
@@ -1345,10 +1478,10 @@ async function setup(options, runner = runHost) {
1345
1478
  }
1346
1479
 
1347
1480
  // packages/cli/src/index.ts
1348
- import { chmod, lstat as lstat5, mkdir as mkdir3, open as open5 } from "node:fs/promises";
1349
- import { constants as constants5 } from "node:fs";
1481
+ import { chmod, lstat as lstat7, mkdir as mkdir5, open as open7 } from "node:fs/promises";
1482
+ import { constants as constants7 } from "node:fs";
1350
1483
  import { homedir } from "node:os";
1351
- import { basename, dirname as dirname3, extname, join as join2, resolve as resolve4 } from "node:path";
1484
+ import { basename as basename2, dirname as dirname6, extname as extname3, join as join3, resolve as resolve6 } from "node:path";
1352
1485
 
1353
1486
  // packages/cli/src/device-auth.ts
1354
1487
  import { spawn as spawn2 } from "node:child_process";
@@ -1369,12 +1502,12 @@ function browserCommand(url, platform = process.platform) {
1369
1502
  }
1370
1503
  async function openSystemBrowser(url) {
1371
1504
  const { command, args } = browserCommand(url);
1372
- await new Promise((resolve5, reject) => {
1505
+ await new Promise((resolve7, reject) => {
1373
1506
  const child = spawn2(command, args, { shell: false, stdio: "ignore", detached: true, windowsHide: true });
1374
1507
  child.once("error", reject);
1375
1508
  child.once("spawn", () => {
1376
1509
  child.unref();
1377
- resolve5();
1510
+ resolve7();
1378
1511
  });
1379
1512
  });
1380
1513
  }
@@ -1387,7 +1520,7 @@ function expired() {
1387
1520
  function invalid() {
1388
1521
  return new DeviceLoginError("invalid_response", "The authorization server returned an invalid response. No credential was saved.");
1389
1522
  }
1390
- function record(value) {
1523
+ function record2(value) {
1391
1524
  return !!value && typeof value === "object" && !Array.isArray(value);
1392
1525
  }
1393
1526
  function abortCheck(signal) {
@@ -1435,11 +1568,11 @@ async function responseJson(response) {
1435
1568
  }
1436
1569
  }
1437
1570
  function sessionFrom(value) {
1438
- if (!record(value) || typeof value.deviceCode !== "string" || !/^[a-zA-Z0-9._~-]{16,1024}$/.test(value.deviceCode) || typeof value.userCode !== "string" || !/^[A-Z0-9-]{4,32}$/.test(value.userCode) || typeof value.verificationUri !== "string" || !Number.isInteger(value.expiresIn) || Number(value.expiresIn) < 1 || Number(value.expiresIn) > 3600 || !Number.isInteger(value.interval) || Number(value.interval) < 1 || Number(value.interval) > 60) throw invalid();
1571
+ if (!record2(value) || typeof value.deviceCode !== "string" || !/^[a-zA-Z0-9._~-]{16,1024}$/.test(value.deviceCode) || typeof value.userCode !== "string" || !/^[A-Z0-9-]{4,32}$/.test(value.userCode) || typeof value.verificationUri !== "string" || !Number.isInteger(value.expiresIn) || Number(value.expiresIn) < 1 || Number(value.expiresIn) > 3600 || !Number.isInteger(value.interval) || Number(value.interval) < 1 || Number(value.interval) > 60) throw invalid();
1439
1572
  return { deviceCode: value.deviceCode, userCode: value.userCode, verificationUri: verificationUrl(value.verificationUri, value.userCode, value.deviceCode), expiresIn: Number(value.expiresIn), interval: Number(value.interval) };
1440
1573
  }
1441
1574
  function authorizationFrom(value) {
1442
- if (!record(value) || typeof value.apiKey !== "string" || !/^eh_[a-f0-9]{64}$/.test(value.apiKey) || value.role !== "editor" || !record(value.workspace) || typeof value.workspace.id !== "string" || !/^[a-zA-Z0-9_-]{1,100}$/.test(value.workspace.id) || typeof value.workspace.name !== "string" || !value.workspace.name.trim() || value.workspace.name.length > 100) throw invalid();
1575
+ if (!record2(value) || typeof value.apiKey !== "string" || !/^eh_[a-f0-9]{64}$/.test(value.apiKey) || value.role !== "editor" || !record2(value.workspace) || typeof value.workspace.id !== "string" || !/^[a-zA-Z0-9_-]{1,100}$/.test(value.workspace.id) || typeof value.workspace.name !== "string" || !value.workspace.name.trim() || value.workspace.name.length > 100) throw invalid();
1443
1576
  return { apiKey: value.apiKey, workspace: { id: value.workspace.id, name: value.workspace.name }, role: "editor" };
1444
1577
  }
1445
1578
  async function loginWithDevice(options, overrides = {}) {
@@ -1463,7 +1596,7 @@ async function loginWithDevice(options, overrides = {}) {
1463
1596
  throw new DeviceLoginError("network_error", "The authorization server could not be reached. No credential was saved.");
1464
1597
  }
1465
1598
  };
1466
- const parse = async (response) => {
1599
+ const parse2 = async (response) => {
1467
1600
  try {
1468
1601
  return await responseJson(response);
1469
1602
  } catch (error) {
@@ -1475,7 +1608,7 @@ async function loginWithDevice(options, overrides = {}) {
1475
1608
  };
1476
1609
  const started = await post("/v1/auth/device", {});
1477
1610
  if (started.status !== 200 && started.status !== 201) throw new DeviceLoginError("server_error", `Could not start browser authorization (HTTP ${started.status}).`);
1478
- const session = sessionFrom(await parse(started));
1611
+ const session = sessionFrom(await parse2(started));
1479
1612
  deadline = Math.min(deadline, runtime.now() + session.expiresIn * 1e3);
1480
1613
  const { deviceCode, ...challenge } = session;
1481
1614
  options.onChallenge?.(challenge);
@@ -1502,7 +1635,7 @@ async function loginWithDevice(options, overrides = {}) {
1502
1635
  if (runtime.now() >= deadline) throw expired();
1503
1636
  const response = await post("/v1/auth/device/token", { deviceCode });
1504
1637
  if (response.status === 200) {
1505
- const result = authorizationFrom(await parse(response));
1638
+ const result = authorizationFrom(await parse2(response));
1506
1639
  abortCheck(options.signal);
1507
1640
  return result;
1508
1641
  }
@@ -1697,14 +1830,14 @@ async function checkLocalResource(root, path, expected) {
1697
1830
  const before = await handle.stat();
1698
1831
  if (!before.isFile()) throw new Error("Resource is not a regular file.");
1699
1832
  if (!expected) return {};
1700
- const hash = createHash2("sha256");
1701
- for await (const chunk of handle.createReadStream({ autoClose: false })) hash.update(chunk);
1833
+ const hash2 = createHash2("sha256");
1834
+ for await (const chunk of handle.createReadStream({ autoClose: false })) hash2.update(chunk);
1702
1835
  const after = await handle.stat();
1703
1836
  const current = await lstat4(await checkedPath(root, path, false));
1704
1837
  if (before.size !== after.size || before.mtimeMs !== after.mtimeMs || before.ctimeMs !== after.ctimeMs || current.dev !== before.dev || current.ino !== before.ino) {
1705
1838
  throw new Error("Resource changed while its hash was being checked; check it again.");
1706
1839
  }
1707
- return { actualSha256: hash.digest("hex") };
1840
+ return { actualSha256: hash2.digest("hex") };
1708
1841
  } finally {
1709
1842
  await handle.close();
1710
1843
  }
@@ -1769,6 +1902,683 @@ async function checkLocalStyle(options) {
1769
1902
  };
1770
1903
  }
1771
1904
 
1905
+ // packages/cli/src/media-inventory.ts
1906
+ import { createHash as createHash4 } from "node:crypto";
1907
+ import { link, lstat as lstat6, mkdir as mkdir3, mkdtemp, opendir, readFile as readFile2, rm } from "node:fs/promises";
1908
+ import { basename, dirname as dirname4, extname as extname2, join as join2 } from "node:path";
1909
+
1910
+ // packages/cli/src/media-runtime.ts
1911
+ import { spawn as spawn3 } from "node:child_process";
1912
+ import { createHash as createHash3 } from "node:crypto";
1913
+ import { constants as constants5 } from "node:fs";
1914
+ import { lstat as lstat5, open as open5, realpath as realpath2 } from "node:fs/promises";
1915
+ import { dirname as dirname3, extname, parse, resolve as resolve4 } from "node:path";
1916
+ var MediaToolError = class extends CliError {
1917
+ constructor(code, message) {
1918
+ 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.");
1919
+ this.name = "MediaToolError";
1920
+ }
1921
+ };
1922
+ var SUPPORTED_MEDIA_EXTENSIONS = /* @__PURE__ */ new Set([
1923
+ ".mp4",
1924
+ ".mov",
1925
+ ".m4v",
1926
+ ".mkv",
1927
+ ".webm",
1928
+ ".avi",
1929
+ ".mpg",
1930
+ ".mpeg",
1931
+ ".mts",
1932
+ ".m2ts",
1933
+ ".ts",
1934
+ ".ogv",
1935
+ ".wmv",
1936
+ ".wav",
1937
+ ".mp3",
1938
+ ".m4a",
1939
+ ".aac",
1940
+ ".flac",
1941
+ ".ogg",
1942
+ ".opus",
1943
+ ".aif",
1944
+ ".aiff",
1945
+ ".ac3",
1946
+ ".eac3",
1947
+ ".jpg",
1948
+ ".jpeg",
1949
+ ".png",
1950
+ ".webp",
1951
+ ".bmp",
1952
+ ".tif",
1953
+ ".tiff",
1954
+ ".gif",
1955
+ ".avif",
1956
+ ".heic",
1957
+ ".heif"
1958
+ ]);
1959
+ var PROTOCOLS = "file,pipe";
1960
+ 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";
1961
+ var DEFAULT_TIMEOUT = 3e4;
1962
+ var DEFAULT_OUTPUT_LIMIT = 2 * 1024 * 1024;
1963
+ function resolveLocalMediaPath(value) {
1964
+ 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("\\\\")) {
1965
+ throw new MediaToolError("MEDIA_LOCAL_PATH_REQUIRED", "Provide a local filesystem path, not a URL, protocol or network share.");
1966
+ }
1967
+ return resolve4(value);
1968
+ }
1969
+ async function assertNoMediaSymlinks(value) {
1970
+ const path = resolveLocalMediaPath(value);
1971
+ let cursor = path;
1972
+ while (cursor !== parse(cursor).root) {
1973
+ const info = await lstat5(cursor);
1974
+ if (info.isSymbolicLink()) {
1975
+ const systemAlias = process.platform === "darwin" && (cursor === "/tmp" || cursor === "/var") && await realpath2(cursor) === `/private${cursor}`;
1976
+ if (!systemAlias) throw new MediaToolError("MEDIA_SYMLINK", "Symlink media paths are not followed. Use the original local path.");
1977
+ }
1978
+ cursor = dirname3(cursor);
1979
+ }
1980
+ return path;
1981
+ }
1982
+ async function assertLocalMediaFile(value) {
1983
+ const path = await assertNoMediaSymlinks(value);
1984
+ const info = await lstat5(path);
1985
+ if (!info.isFile()) throw new MediaToolError("MEDIA_NOT_REGULAR", "The media input must be a regular local file.");
1986
+ if (!SUPPORTED_MEDIA_EXTENSIONS.has(extname(path).toLowerCase())) throw new MediaToolError("MEDIA_FORMAT_UNSUPPORTED", "This file extension is not supported for local media inspection.");
1987
+ return path;
1988
+ }
1989
+ function localInputArgs(path) {
1990
+ return ["-protocol_whitelist", PROTOCOLS, "-format_whitelist", FORMATS, "-i", resolveLocalMediaPath(path)];
1991
+ }
1992
+ async function runMediaTool(tool, args, options = {}) {
1993
+ if (tool !== "ffmpeg" && tool !== "ffprobe") throw new MediaToolError("MEDIA_TOOL_INVALID", "Unsupported media executable.");
1994
+ const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT;
1995
+ const maxOutputBytes = options.maxOutputBytes ?? DEFAULT_OUTPUT_LIMIT;
1996
+ if (!Number.isInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 10 * 6e4 || !Number.isInteger(maxOutputBytes) || maxOutputBytes < 1 || maxOutputBytes > 16 * 1024 * 1024) {
1997
+ throw new MediaToolError("MEDIA_TOOL_LIMITS_INVALID", "Media runtime limits are outside the supported range.");
1998
+ }
1999
+ if (!Array.isArray(args) || args.length > 200 || args.some((arg) => typeof arg !== "string" || arg.length > 8192 || arg.includes("\0"))) {
2000
+ throw new MediaToolError("MEDIA_TOOL_ARGUMENTS_INVALID", "Invalid media tool arguments.");
2001
+ }
2002
+ for (let i = 0; i < args.length; i++) {
2003
+ const arg = args[i];
2004
+ 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") {
2005
+ throw new MediaToolError("MEDIA_REMOTE_INPUT_FORBIDDEN", "Only self-contained local media inputs are allowed.");
2006
+ }
2007
+ if (arg === "-protocol_whitelist" && args[i + 1] !== PROTOCOLS || arg === "-format_whitelist" && args[i + 1] !== FORMATS) {
2008
+ throw new MediaToolError("MEDIA_TOOL_ARGUMENTS_INVALID", "The local media allowlists cannot be widened.");
2009
+ }
2010
+ if (arg === "-i") {
2011
+ await assertLocalMediaFile(args[i + 1] ?? "");
2012
+ if (args[i - 4] !== "-protocol_whitelist" || args[i - 3] !== PROTOCOLS || args[i - 2] !== "-format_whitelist" || args[i - 1] !== FORMATS) {
2013
+ throw new MediaToolError("MEDIA_TOOL_ARGUMENTS_INVALID", "Every media input must use localInputArgs.");
2014
+ }
2015
+ }
2016
+ }
2017
+ return new Promise((resolveResult, reject) => {
2018
+ const stdout = [], stderr = [];
2019
+ let size = 0, done = false;
2020
+ const child = spawn3(tool, ["-hide_banner", ...tool === "ffmpeg" ? ["-nostdin"] : [], ...args], { shell: false, stdio: ["ignore", "pipe", "pipe"], windowsHide: true });
2021
+ const fail = (error) => {
2022
+ if (done) return;
2023
+ done = true;
2024
+ clearTimeout(timer);
2025
+ child.kill("SIGKILL");
2026
+ reject(error);
2027
+ };
2028
+ const timer = setTimeout(() => fail(new MediaToolError("MEDIA_TOOL_TIMEOUT", "The local media check exceeded its time limit. No complete result is available.")), timeoutMs);
2029
+ const collect = (target) => (chunk) => {
2030
+ if (done) return;
2031
+ size += chunk.length;
2032
+ if (size > maxOutputBytes) return fail(new MediaToolError("MEDIA_TOOL_OUTPUT_LIMIT", "The local media check exceeded its output limit. No complete result is available."));
2033
+ target.push(chunk);
2034
+ };
2035
+ child.stdout.on("data", collect(stdout));
2036
+ child.stderr.on("data", collect(stderr));
2037
+ child.on("error", (error) => fail(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.")));
2038
+ child.on("close", (code) => {
2039
+ if (done) return;
2040
+ done = true;
2041
+ clearTimeout(timer);
2042
+ 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."));
2043
+ resolveResult({ stdout: Buffer.concat(stdout).toString("utf8"), stderr: Buffer.concat(stderr).toString("utf8") });
2044
+ });
2045
+ });
2046
+ }
2047
+ var finite2 = (value) => {
2048
+ if (typeof value !== "number" && typeof value !== "string" || value === "") return null;
2049
+ const n = Number(value);
2050
+ return Number.isFinite(n) ? n : null;
2051
+ };
2052
+ var nonnegative2 = (value) => {
2053
+ const n = finite2(value);
2054
+ return n !== null && n >= 0 ? n : null;
2055
+ };
2056
+ var positiveInteger = (value) => {
2057
+ const n = finite2(value);
2058
+ return n !== null && Number.isInteger(n) && n > 0 ? n : void 0;
2059
+ };
2060
+ var rate = (value) => {
2061
+ if (typeof value !== "string" || !/^\d+(?:\/\d+)?$/.test(value)) return null;
2062
+ const [n, d = "1"] = value.split("/");
2063
+ const result = Number(n) / Number(d);
2064
+ return Number.isFinite(result) && result > 0 ? result : null;
2065
+ };
2066
+ var sameFile = (a, b) => a.dev === b.dev && a.ino === b.ino && a.size === b.size && a.mtimeMs === b.mtimeMs && a.ctimeMs === b.ctimeMs;
2067
+ async function probeLocalMedia(value) {
2068
+ const path = await assertLocalMediaFile(value);
2069
+ const handle = await open5(path, constants5.O_RDONLY | (constants5.O_NOFOLLOW ?? 0));
2070
+ try {
2071
+ const before = await handle.stat();
2072
+ if (!before.isFile()) throw new MediaToolError("MEDIA_NOT_REGULAR", "The media input must be a regular local file.");
2073
+ 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";
2074
+ const { stdout } = await runMediaTool("ffprobe", ["-v", "error", "-show_entries", entries, "-of", "json", ...localInputArgs(path)]);
2075
+ const hash2 = createHash3("sha256");
2076
+ for await (const chunk of handle.createReadStream({ autoClose: false })) hash2.update(chunk);
2077
+ const after = await handle.stat();
2078
+ await assertNoMediaSymlinks(path);
2079
+ const current = await lstat5(path);
2080
+ if (!sameFile(before, after) || !sameFile(after, current)) throw new MediaToolError("MEDIA_FILE_CHANGED", "The media file changed while it was being inspected. Retry with a stable source.");
2081
+ let raw;
2082
+ try {
2083
+ raw = JSON.parse(stdout);
2084
+ } catch {
2085
+ throw new MediaToolError("MEDIA_PROBE_INVALID", "The media probe returned invalid metadata.");
2086
+ }
2087
+ 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.");
2088
+ const streams = raw.streams.map((stream, offset) => {
2089
+ const type = typeof stream.codec_type === "string" && /^(video|audio|subtitle|data|attachment|unknown)$/.test(stream.codec_type) ? stream.codec_type : "unknown";
2090
+ const codec = typeof stream.codec_name === "string" && /^[a-zA-Z0-9_]{1,64}$/.test(stream.codec_name) ? stream.codec_name : null;
2091
+ const index = finite2(stream.index);
2092
+ return {
2093
+ index: index !== null && Number.isInteger(index) && index >= 0 ? index : offset,
2094
+ type,
2095
+ codec,
2096
+ durationSeconds: nonnegative2(stream.duration),
2097
+ ...type === "video" ? { width: positiveInteger(stream.width), height: positiveInteger(stream.height), fps: rate(stream.avg_frame_rate), attachedPicture: stream.disposition?.attached_pic === 1 } : {},
2098
+ ...type === "audio" ? { sampleRate: nonnegative2(stream.sample_rate), channels: positiveInteger(stream.channels) ?? null } : {}
2099
+ };
2100
+ });
2101
+ return { path, sha256: hash2.digest("hex"), bytes: after.size, durationSeconds: nonnegative2(raw.format?.duration), startTimeSeconds: finite2(raw.format?.start_time), streams };
2102
+ } finally {
2103
+ await handle.close();
2104
+ }
2105
+ }
2106
+
2107
+ // packages/cli/src/media-inventory.ts
2108
+ var GENERATED_DIRECTORIES = /* @__PURE__ */ new Set(["node_modules", "dist", "build", "out", "output", "outputs", "renders", "render", "cache", "caches", "proxies", "proxy", "samples", "thumbnails", "coverage", "venv", "__pycache__"]);
2109
+ function mediaFailure(error) {
2110
+ if (error instanceof MediaToolError) return { code: error.code, message: error.message };
2111
+ const code = error?.code;
2112
+ if (code === "ENOENT") return { code: "MEDIA_FILE_MISSING", message: "The local path is missing or changed during inspection." };
2113
+ if (code === "EACCES" || code === "EPERM") return { code: "MEDIA_FILE_UNREADABLE", message: "The local path cannot be read with the current filesystem permissions." };
2114
+ return { code: "MEDIA_INSPECTION_FAILED", message: "Local media inspection failed. No complete result is available for this path." };
2115
+ }
2116
+ async function inventoryMedia(options) {
2117
+ const maxFiles = options.maxFiles ?? 200;
2118
+ if (!Number.isInteger(maxFiles) || maxFiles < 1 || maxFiles > 1e3) throw new MediaToolError("MEDIA_LIMIT_INVALID", "maxFiles must be an integer between 1 and 1000.");
2119
+ const input = resolveLocalMediaPath(options.input);
2120
+ const report = { schemaVersion: 1, kind: "editorbrain.media-inventory", generatedAt: (/* @__PURE__ */ new Date()).toISOString(), assets: [], failures: [], skipped: [], complete: true, scannedEntries: 0, limits: { maxFiles, maxEntries: 1e4, maxDepth: 32 }, contentReviewed: false, mediaUploaded: false };
2121
+ const candidates = [];
2122
+ const skip = (path, reason, incomplete = false) => {
2123
+ report.skipped.push({ path, reason });
2124
+ if (incomplete) report.complete = false;
2125
+ };
2126
+ const visit = async (path, depth, explicit = false) => {
2127
+ if (report.scannedEntries >= report.limits.maxEntries) {
2128
+ skip(path, "entry_limit_reached", true);
2129
+ return;
2130
+ }
2131
+ report.scannedEntries++;
2132
+ try {
2133
+ const stat = await lstat6(path);
2134
+ if (stat.isSymbolicLink()) {
2135
+ if (explicit) throw new MediaToolError("MEDIA_SYMLINK", "Symlink inputs are not followed. Use the original local path.");
2136
+ skip(path, "symlink_not_followed");
2137
+ return;
2138
+ }
2139
+ if (!explicit && basename(path).startsWith(".")) {
2140
+ skip(path, "hidden_entry");
2141
+ return;
2142
+ }
2143
+ await assertNoMediaSymlinks(path);
2144
+ if (stat.isDirectory()) {
2145
+ if (!explicit && GENERATED_DIRECTORIES.has(basename(path).toLowerCase())) {
2146
+ skip(path, "generated_directory");
2147
+ return;
2148
+ }
2149
+ if (depth >= report.limits.maxDepth) {
2150
+ skip(path, "depth_limit_reached", true);
2151
+ return;
2152
+ }
2153
+ const directory = await opendir(path);
2154
+ for await (const entry of directory) {
2155
+ if (report.scannedEntries >= report.limits.maxEntries) {
2156
+ skip(path, "remaining_entries_not_scanned", true);
2157
+ break;
2158
+ }
2159
+ await visit(join2(path, entry.name), depth + 1);
2160
+ }
2161
+ } else if (!stat.isFile()) {
2162
+ if (explicit) throw new MediaToolError("MEDIA_NOT_REGULAR", "The explicit media input must be a regular local file or directory.");
2163
+ skip(path, "not_regular_file");
2164
+ } else if (!SUPPORTED_MEDIA_EXTENSIONS.has(extname2(path).toLowerCase())) {
2165
+ if (explicit) throw new MediaToolError("MEDIA_FORMAT_UNSUPPORTED", "This explicit input is not a supported media format.");
2166
+ skip(path, "unsupported_extension");
2167
+ } else if (candidates.length >= maxFiles) skip(path, "file_limit_reached", true);
2168
+ else candidates.push(path);
2169
+ } catch (error) {
2170
+ report.failures.push({ path, ...mediaFailure(error) });
2171
+ report.complete = false;
2172
+ }
2173
+ };
2174
+ await visit(input, 0, true);
2175
+ for (const path of candidates.sort()) {
2176
+ try {
2177
+ report.assets.push(await probeLocalMedia(path));
2178
+ } catch (error) {
2179
+ report.failures.push({ path, ...mediaFailure(error) });
2180
+ report.complete = false;
2181
+ }
2182
+ }
2183
+ return report;
2184
+ }
2185
+ async function sampleMedia(options) {
2186
+ if (!Array.isArray(options.times) || options.times.length < 1 || options.times.length > 12 || options.times.some((time) => typeof time !== "number" || !Number.isFinite(time) || time < 0 || time > 86400)) {
2187
+ throw new MediaToolError("MEDIA_SAMPLE_TIMES_INVALID", "Provide 1\u201312 sample times in seconds, between 0 and 86400.");
2188
+ }
2189
+ const file = await assertLocalMediaFile(options.file);
2190
+ const source = await probeLocalMedia(file);
2191
+ if (!source.streams.some((stream) => stream.type === "video")) throw new MediaToolError("MEDIA_VIDEO_REQUIRED", "Frame sampling requires a video or image stream.");
2192
+ 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.");
2193
+ const outputDirectory = resolveLocalMediaPath(options.outputDirectory);
2194
+ await assertNoMediaSymlinks(dirname4(outputDirectory));
2195
+ try {
2196
+ await mkdir3(outputDirectory);
2197
+ } catch (error) {
2198
+ if (error.code !== "EEXIST") throw error;
2199
+ }
2200
+ await assertNoMediaSymlinks(outputDirectory);
2201
+ if (!(await lstat6(outputDirectory)).isDirectory()) throw new MediaToolError("MEDIA_OUTPUT_DIRECTORY_INVALID", "Frame output must be a local directory.");
2202
+ const temp = await mkdtemp(join2(outputDirectory, ".editorbrain-samples-"));
2203
+ const tempStat = await lstat6(temp);
2204
+ const frames = [];
2205
+ const created = [];
2206
+ try {
2207
+ for (let index = 0; index < options.times.length; index++) {
2208
+ const time = options.times[index];
2209
+ const filename = `${source.sha256.slice(0, 12)}-${String(index + 1).padStart(2, "0")}-${Math.round(time * 1e3)}ms.jpg`;
2210
+ const temporary = join2(temp, filename), output2 = join2(outputDirectory, filename);
2211
+ 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 });
2212
+ const bytes = await readFile2(temporary);
2213
+ if (!bytes.length) throw new MediaToolError("MEDIA_SAMPLE_EMPTY", "The requested frame did not produce a usable image.");
2214
+ try {
2215
+ await link(temporary, output2);
2216
+ } catch (error) {
2217
+ 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.");
2218
+ throw error;
2219
+ }
2220
+ const outputStat = await lstat6(output2);
2221
+ created.push({ path: output2, ino: outputStat.ino, dev: outputStat.dev, size: outputStat.size, mtimeMs: outputStat.mtimeMs, ctimeMs: outputStat.ctimeMs });
2222
+ frames.push({ path: output2, sha256: createHash4("sha256").update(bytes).digest("hex"), bytes: bytes.length, requestedTimeSeconds: time, actualPresentationTimeSeconds: null });
2223
+ }
2224
+ const after = await probeLocalMedia(file);
2225
+ if (source.sha256 !== after.sha256) throw new MediaToolError("MEDIA_FILE_CHANGED", "The source changed while frames were being sampled. Retry with a stable source.");
2226
+ return { schemaVersion: 1, kind: "editorbrain.media-samples", generatedAt: (/* @__PURE__ */ new Date()).toISOString(), source, frames, metadataOnly: false, contentReviewed: false, mediaUploaded: false };
2227
+ } catch (error) {
2228
+ for (const file2 of created) {
2229
+ try {
2230
+ await assertNoMediaSymlinks(file2.path);
2231
+ const current = await lstat6(file2.path);
2232
+ if (current.ino === file2.ino && current.dev === file2.dev && current.size === file2.size && current.mtimeMs === file2.mtimeMs && current.ctimeMs === file2.ctimeMs) await rm(file2.path, { force: true });
2233
+ } catch {
2234
+ }
2235
+ }
2236
+ throw error;
2237
+ } finally {
2238
+ try {
2239
+ await assertNoMediaSymlinks(temp);
2240
+ const current = await lstat6(temp);
2241
+ if (current.ino === tempStat.ino && current.dev === tempStat.dev) await rm(temp, { recursive: true, force: true });
2242
+ } catch {
2243
+ }
2244
+ }
2245
+ }
2246
+
2247
+ // packages/cli/src/render-check.ts
2248
+ import { resolve as resolve5 } from "node:path";
2249
+ function invalidProfile(message) {
2250
+ throw new CliError("RENDER_PROFILE_INVALID", message, "Use a schemaVersion: 1 render profile with explicit technical requirements.");
2251
+ }
2252
+ function object3(value, path, keys) {
2253
+ if (!value || typeof value !== "object" || Array.isArray(value)) invalidProfile(`${path} must be an object.`);
2254
+ const result = value;
2255
+ for (const key of Object.keys(result)) if (!keys.includes(key)) invalidProfile(`${path} contains unsupported field ${key}.`);
2256
+ return result;
2257
+ }
2258
+ function validateRenderCheckProfile(value) {
2259
+ const root = object3(value, "profile", ["schemaVersion", "expected", "tolerance", "audio", "silence", "black", "peak", "limits"]);
2260
+ if (root.schemaVersion !== 1) invalidProfile("profile.schemaVersion must be 1.");
2261
+ const expected = root.expected === void 0 ? {} : object3(root.expected, "expected", ["width", "height", "fps", "durationSeconds"]);
2262
+ const tolerance = root.tolerance === void 0 ? {} : object3(root.tolerance, "tolerance", ["fps", "durationSeconds"]);
2263
+ const silence = root.silence === void 0 ? {} : object3(root.silence, "silence", ["enabled", "minDurationSeconds", "thresholdDb"]);
2264
+ const black = root.black === void 0 ? {} : object3(root.black, "black", ["enabled", "minDurationSeconds", "pixelThreshold", "pictureBlackRatio"]);
2265
+ const peak = root.peak === void 0 ? {} : object3(root.peak, "peak", ["enabled", "thresholdDbFS"]);
2266
+ const limits = root.limits === void 0 ? {} : object3(root.limits, "limits", ["timeoutMs", "maxOutputBytes"]);
2267
+ const number = (v, name, min, max, fallback, integer = false) => {
2268
+ if (v === void 0 && fallback !== void 0) return fallback;
2269
+ if (typeof v !== "number" || !Number.isFinite(v) || v < min || v > max || integer && !Number.isInteger(v)) invalidProfile(`${name} must be ${integer ? "an integer" : "a finite number"} between ${min} and ${max}.`);
2270
+ return v;
2271
+ };
2272
+ const enabled = (v, name) => {
2273
+ if (v === void 0) return true;
2274
+ if (typeof v !== "boolean") invalidProfile(`${name} must be a boolean.`);
2275
+ return v;
2276
+ };
2277
+ if (root.audio !== void 0 && !["required", "optional", "forbidden"].includes(root.audio)) invalidProfile("audio must be required, optional or forbidden.");
2278
+ const validatedExpected = {};
2279
+ for (const key of ["width", "height"]) if (expected[key] !== void 0) validatedExpected[key] = number(expected[key], `expected.${key}`, 1, 32768, void 0, true);
2280
+ if (expected.fps !== void 0) validatedExpected.fps = number(expected.fps, "expected.fps", 1e-6, 1e3);
2281
+ if (expected.durationSeconds !== void 0) validatedExpected.durationSeconds = number(expected.durationSeconds, "expected.durationSeconds", 1e-6, 864e4);
2282
+ return {
2283
+ schemaVersion: 1,
2284
+ expected: validatedExpected,
2285
+ tolerance: { fps: number(tolerance.fps, "tolerance.fps", 0, 1e3, 0.01), durationSeconds: number(tolerance.durationSeconds, "tolerance.durationSeconds", 0, 86400, 0.1) },
2286
+ audio: root.audio ?? "optional",
2287
+ silence: { enabled: enabled(silence.enabled, "silence.enabled"), minDurationSeconds: number(silence.minDurationSeconds, "silence.minDurationSeconds", 1e-3, 86400, 0.75), thresholdDb: number(silence.thresholdDb, "silence.thresholdDb", -150, 0, -50) },
2288
+ black: { enabled: enabled(black.enabled, "black.enabled"), minDurationSeconds: number(black.minDurationSeconds, "black.minDurationSeconds", 1e-3, 86400, 0.15), pixelThreshold: number(black.pixelThreshold, "black.pixelThreshold", 0, 1, 0.1), pictureBlackRatio: number(black.pictureBlackRatio, "black.pictureBlackRatio", 0, 1, 0.98) },
2289
+ peak: { enabled: enabled(peak.enabled, "peak.enabled"), thresholdDbFS: number(peak.thresholdDbFS, "peak.thresholdDbFS", -150, 0, -0.1) },
2290
+ limits: { timeoutMs: number(limits.timeoutMs, "limits.timeoutMs", 1, 6e5, 6e5, true), maxOutputBytes: number(limits.maxOutputBytes, "limits.maxOutputBytes", 1024, 16777216, 4194304, true) }
2291
+ };
2292
+ }
2293
+ function errorCode(error) {
2294
+ if (error && typeof error === "object" && "code" in error && typeof error.code === "string" && /^[A-Z_0-9]+$/.test(error.code)) return error.code;
2295
+ return "MEDIA_CHECK_FAILED";
2296
+ }
2297
+ function addCheck(report, check) {
2298
+ report.checks.push(check);
2299
+ }
2300
+ function addFinding(report, finding) {
2301
+ report.findingsSummary.total++;
2302
+ report.findingsSummary.byKind[finding.kind]++;
2303
+ if (report.findings.length < 1e3) report.findings.push(finding);
2304
+ else {
2305
+ report.findingsSummary.truncated = true;
2306
+ report.complete = false;
2307
+ }
2308
+ report.findingsSummary.returned = report.findings.length;
2309
+ }
2310
+ function finish(report) {
2311
+ report.technicalStatus = report.checks.some((check) => check.status === "fail") ? "fail" : !report.complete ? "incomplete" : report.checks.some((check) => check.status === "warning") ? "warning" : "pass";
2312
+ return report;
2313
+ }
2314
+ function checkExpected(report, probe, profile) {
2315
+ const expected = profile.expected;
2316
+ if (!Object.keys(expected).length) return addCheck(report, { id: "expected_format", status: "not_applicable", detail: "No target dimensions, frame rate or duration were specified." });
2317
+ const video = probe.streams.find((stream) => stream.type === "video" && !stream.attachedPicture);
2318
+ const mismatches = [];
2319
+ const unknown = [];
2320
+ for (const key of ["width", "height", "fps"]) {
2321
+ const target = expected[key];
2322
+ if (target === void 0) continue;
2323
+ if (!video) {
2324
+ mismatches.push(`Expected ${key}, but the file has no video stream.`);
2325
+ continue;
2326
+ }
2327
+ const actual = video[key];
2328
+ if (actual === void 0 || actual === null || actual <= 0) unknown.push(`Could not measure ${key}.`);
2329
+ else if (Math.abs(actual - target) > (key === "fps" ? profile.tolerance.fps : 0)) mismatches.push(`${key}: measured ${actual}, expected ${target}.`);
2330
+ }
2331
+ if (expected.durationSeconds !== void 0) {
2332
+ if (probe.durationSeconds === null) unknown.push("Could not measure container duration.");
2333
+ else if (Math.abs(probe.durationSeconds - expected.durationSeconds) > profile.tolerance.durationSeconds) mismatches.push(`durationSeconds: measured ${probe.durationSeconds}, expected ${expected.durationSeconds}.`);
2334
+ }
2335
+ if (unknown.length) report.complete = false;
2336
+ addCheck(report, { id: "expected_format", status: mismatches.length ? "fail" : unknown.length ? "not_checked" : "pass", detail: [...mismatches, ...unknown].join(" ") || "Explicit requirements matched; dimensions and reported average frame rate refer to the first video stream excluding attached cover art." });
2337
+ }
2338
+ async function toolVersion(tool) {
2339
+ try {
2340
+ const result = await runMediaTool(tool, ["-version"], { timeoutMs: 1e4, maxOutputBytes: 65536 });
2341
+ return { available: true, version: new RegExp(`^${tool} version ([A-Za-z0-9._+~-]+)`, "m").exec(result.stdout)?.[1] ?? null };
2342
+ } catch {
2343
+ return { available: false, version: null };
2344
+ }
2345
+ }
2346
+ function readDetectorOutput(report, stderr, audioStreams) {
2347
+ const samples = /* @__PURE__ */ new Map();
2348
+ for (const stream of audioStreams) samples.set(stream.index, { streamIndex: stream.index, sampleCount: null, samplePeakDbFS: null, peakState: "unavailable" });
2349
+ const pendingSilence = /* @__PURE__ */ new Map();
2350
+ for (const line of stderr.split("\n")) {
2351
+ const tag = /\[(blackdetect@v|silencedetect@a|astats@a)(\d+)\s+@/.exec(line);
2352
+ if (!tag) continue;
2353
+ const streamIndex = Number(tag[2]);
2354
+ if (tag[1] === "blackdetect@v") {
2355
+ const match = /black_start:([\d.e+-]+)\s+black_end:([\d.e+-]+)\s+black_duration:([\d.e+-]+)/i.exec(line);
2356
+ if (match && match.slice(1).every((value) => Number.isFinite(Number(value)))) addFinding(report, { kind: "black_candidate", streamIndex, startSeconds: Number(match[1]), endSeconds: Number(match[2]), durationSeconds: Number(match[3]), requiresReview: true, message: "A mostly black interval met the configured detector threshold. It may be intentional; review in context." });
2357
+ } else if (tag[1] === "silencedetect@a") {
2358
+ const start = /silence_start:\s*([\d.e+-]+)/i.exec(line);
2359
+ if (start && Number.isFinite(Number(start[1]))) pendingSilence.set(streamIndex, Number(start[1]));
2360
+ const end = /silence_end:\s*([\d.e+-]+)\s*\|\s*silence_duration:\s*([\d.e+-]+)/i.exec(line);
2361
+ if (end && end.slice(1).every((value) => Number.isFinite(Number(value)))) {
2362
+ addFinding(report, { kind: "silence_candidate", streamIndex, startSeconds: pendingSilence.get(streamIndex) ?? Number(end[1]) - Number(end[2]), endSeconds: Number(end[1]), durationSeconds: Number(end[2]), requiresReview: true, message: "A quiet interval met the configured detector threshold. Pauses and silent edits may be intentional." });
2363
+ pendingSilence.delete(streamIndex);
2364
+ }
2365
+ } else {
2366
+ const sample = samples.get(streamIndex);
2367
+ if (!sample) continue;
2368
+ const peak = /Peak level dB:\s*(-inf|[\d.e+-]+)/i.exec(line);
2369
+ if (peak?.[1] === "-inf") sample.peakState = "negative_infinity";
2370
+ else if (peak && Number.isFinite(Number(peak[1]))) {
2371
+ sample.peakState = "measured";
2372
+ sample.samplePeakDbFS = Number(peak[1]);
2373
+ }
2374
+ const count = /Number of samples:\s*([\d.e+-]+)/i.exec(line);
2375
+ if (count && Number.isFinite(Number(count[1]))) sample.sampleCount = Number(count[1]);
2376
+ }
2377
+ }
2378
+ report.measurements.audio = [...samples.values()];
2379
+ if (pendingSilence.size) {
2380
+ report.complete = false;
2381
+ addCheck(report, { id: "silence", status: "not_checked", code: "DETECTOR_OUTPUT_INCOMPLETE", detail: "At least one detected silence interval had no closing timestamp; results are incomplete." });
2382
+ }
2383
+ }
2384
+ async function checkRender(options) {
2385
+ const profile = validateRenderCheckProfile(options.profile ?? { schemaVersion: 1 });
2386
+ const report = {
2387
+ schemaVersion: 1,
2388
+ kind: "editorbrain.render-check",
2389
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
2390
+ mediaUploaded: false,
2391
+ file: { path: resolve5(options.file), sha256: null, bytes: null },
2392
+ effectiveProfile: profile,
2393
+ complete: true,
2394
+ technicalStatus: "incomplete",
2395
+ measurements: { probe: null, decodedThroughSeconds: null, audio: [] },
2396
+ capabilities: { ffprobe: { available: false, version: null }, ffmpeg: { available: false, version: null }, decodeScope: "all_audio_video_streams", timestampBasis: "seconds_from_container_start", peakMethod: "sample_peak_whole_mix_per_audio_stream", truePeak: "not_assessed", loudness: "not_assessed", repeatedFootage: "not_assessed", subtitleStreams: "not_assessed" },
2397
+ checks: [],
2398
+ findings: [],
2399
+ findingsSummary: { total: 0, returned: 0, truncated: false, byKind: { silence_candidate: 0, black_candidate: 0, peak_candidate: 0 } },
2400
+ creativeQuality: { narrative: "not_assessed", readability: "not_assessed", sfxAudibility: "not_assessed", style: "not_assessed" },
2401
+ limitations: [
2402
+ "This report is a technical inspection of the identified file, not approval of its editing or creative quality.",
2403
+ "Default detector thresholds are sensitivities, not requirements for a platform, niche, style, music, pacing or frame rate.",
2404
+ "Silence, black and peak findings require contextual review; no automatic cuts or audio changes are performed.",
2405
+ "Whole-mix sample peaks cannot establish SFX audibility, true-peak clipping, speech clarity or perceived loudness.",
2406
+ "Frame rate is container-reported average; constant frame cadence, HDR, color appearance, rotation and subtitle rendering are not certified.",
2407
+ "Black interval boundaries follow decoded frame timestamps and may differ from presentation duration by a frame.",
2408
+ "Attached cover art is decoded but excluded from video requirements and black-interval detection.",
2409
+ "Reports retain at most 1,000 timestamped findings; exceeding this cap marks the result incomplete and preserves total counts."
2410
+ ]
2411
+ };
2412
+ let probe;
2413
+ try {
2414
+ probe = await probeLocalMedia(options.file);
2415
+ report.file = { path: probe.path, sha256: probe.sha256, bytes: probe.bytes };
2416
+ report.measurements.probe = probe;
2417
+ report.capabilities.ffprobe.available = true;
2418
+ addCheck(report, { id: "probe", status: "pass", detail: "Read stream metadata and SHA-256 from the local file." });
2419
+ } catch (error) {
2420
+ report.complete = false;
2421
+ addCheck(report, { id: "probe", status: "not_checked", code: errorCode(error), detail: "The local file could not be probed. No successful inspection is implied." });
2422
+ for (const id of ["expected_format", "audio_requirement", "decode", "silence", "black", "peak", "file_stability"]) addCheck(report, { id, status: "not_checked", detail: "Requires a successful local media probe." });
2423
+ return finish(report);
2424
+ }
2425
+ const versions = await Promise.all([toolVersion("ffprobe"), toolVersion("ffmpeg")]);
2426
+ report.capabilities.ffprobe = { ...versions[0], available: true };
2427
+ report.capabilities.ffmpeg = versions[1];
2428
+ const video = probe.streams.filter((stream) => stream.type === "video");
2429
+ const motionVideo = video.filter((stream) => !stream.attachedPicture);
2430
+ const audio = probe.streams.filter((stream) => stream.type === "audio");
2431
+ checkExpected(report, probe, profile);
2432
+ const audioMismatch = profile.audio === "required" && !audio.length || profile.audio === "forbidden" && audio.length > 0;
2433
+ addCheck(report, { id: "audio_requirement", status: audioMismatch ? "fail" : profile.audio === "optional" ? "not_applicable" : "pass", detail: `Audio policy: ${profile.audio}; found ${audio.length} audio stream(s). This checks stream presence, not audibility.` });
2434
+ const filters = [];
2435
+ const maps = [];
2436
+ for (const stream of video) {
2437
+ filters.push(`[0:${stream.index}]${profile.black.enabled && !stream.attachedPicture ? `blackdetect@v${stream.index}=d=${profile.black.minDurationSeconds}:pix_th=${profile.black.pixelThreshold}:pic_th=${profile.black.pictureBlackRatio}` : "null"}[v${stream.index}]`);
2438
+ maps.push("-map", `[v${stream.index}]`);
2439
+ }
2440
+ for (const stream of audio) {
2441
+ filters.push(`[0:${stream.index}]${profile.silence.enabled ? `silencedetect@a${stream.index}=n=${profile.silence.thresholdDb}dB:d=${profile.silence.minDurationSeconds},` : ""}astats@a${stream.index}=metadata=0:reset=0:measure_perchannel=none:measure_overall=Peak_level+Number_of_samples[a${stream.index}]`);
2442
+ maps.push("-map", `[a${stream.index}]`);
2443
+ }
2444
+ let decoded = false;
2445
+ if (!filters.length) {
2446
+ addCheck(report, { id: "decode", status: "fail", code: "NO_PLAYABLE_STREAMS", detail: "No audio or video streams are available for inspection." });
2447
+ } else {
2448
+ try {
2449
+ const result = await runMediaTool("ffmpeg", ["-hide_banner", "-nostdin", "-nostats", "-loglevel", "info", "-xerror", "-copyts", "-start_at_zero", ...localInputArgs(probe.path), "-filter_complex", filters.join(";"), ...maps, "-progress", "pipe:1", "-stats_period", "60", "-f", "null", "-"], profile.limits);
2450
+ report.capabilities.ffmpeg.available = true;
2451
+ readDetectorOutput(report, result.stderr, audio);
2452
+ const progress = [...result.stdout.matchAll(/^out_time_us=(-?\d+)$/gm)].at(-1);
2453
+ report.measurements.decodedThroughSeconds = progress ? Number(progress[1]) / 1e6 : null;
2454
+ const frames = [...result.stdout.matchAll(/^frame=(\d+)$/gm)].at(-1);
2455
+ const empty = video.length > 0 && !!frames && Number(frames[1]) <= 0 || audio.length > 0 && report.measurements.audio.some((stream) => stream.sampleCount === 0);
2456
+ const missingEvidence = !/^progress=end$/m.test(result.stdout) || video.length > 0 && !frames || audio.length > 0 && report.measurements.audio.some((stream) => stream.sampleCount === null);
2457
+ if (empty) addCheck(report, { id: "decode", status: "fail", code: "EMPTY_DECODE", detail: "An expected stream produced no decoded frames or samples." });
2458
+ else if (missingEvidence) {
2459
+ report.complete = false;
2460
+ addCheck(report, { id: "decode", status: "not_checked", code: "DECODE_EVIDENCE_MISSING", detail: "The tool exited without complete decode evidence. Empty or incomplete logs are not a pass." });
2461
+ } else {
2462
+ decoded = true;
2463
+ addCheck(report, { id: "decode", status: "pass", detail: "FFmpeg decoded the complete file to end-of-input with explicit audio/video maps and no reported fatal decoding error." });
2464
+ }
2465
+ } catch (error) {
2466
+ const code = errorCode(error);
2467
+ const failed = code === "MEDIA_TOOL_FAILED";
2468
+ report.complete = false;
2469
+ addCheck(report, { id: "decode", status: failed ? "fail" : "not_checked", code, detail: failed ? "FFmpeg did not finish successfully. Inspect the file or local tool compatibility; this does not establish creative quality." : "Full-file decoding did not complete within the available tools or limits; detector results are incomplete." });
2470
+ }
2471
+ }
2472
+ for (const id of ["silence", "black", "peak"]) {
2473
+ if (report.checks.some((check) => check.id === id)) continue;
2474
+ const applicable = id === "black" ? motionVideo.length > 0 : audio.length > 0;
2475
+ if (!applicable) {
2476
+ addCheck(report, { id, status: "not_applicable", detail: `No ${id === "black" ? "video streams excluding attached cover art" : "audio streams"}.` });
2477
+ continue;
2478
+ }
2479
+ if (!profile[id].enabled) {
2480
+ addCheck(report, { id, status: "not_checked", detail: "Explicitly disabled in the effective profile." });
2481
+ continue;
2482
+ }
2483
+ if (!decoded) {
2484
+ addCheck(report, { id, status: "not_checked", detail: "Requires successful full-file decoding." });
2485
+ continue;
2486
+ }
2487
+ if (id === "peak") {
2488
+ if (report.measurements.audio.some((stream) => stream.peakState === "unavailable")) {
2489
+ report.complete = false;
2490
+ addCheck(report, { id, status: "not_checked", code: "DETECTOR_OUTPUT_INCOMPLETE", detail: "A whole-mix sample peak could not be read for every audio stream." });
2491
+ continue;
2492
+ }
2493
+ for (const stream of report.measurements.audio) if (stream.samplePeakDbFS !== null && stream.samplePeakDbFS >= profile.peak.thresholdDbFS) addFinding(report, { kind: "peak_candidate", streamIndex: stream.streamIndex, measuredDbFS: stream.samplePeakDbFS, requiresReview: true, message: "Whole-mix sample peak reached the configured threshold. Check possible clipping or limiting; peak level alone is not proof of distortion." });
2494
+ }
2495
+ const count = report.findingsSummary.byKind[`${id}_candidate`];
2496
+ addCheck(report, { id, status: count ? "warning" : "pass", detail: count ? `${count} candidate(s) require contextual review; they are not automatic editing errors.` : "No candidates met the effective detector threshold during full-file decoding." });
2497
+ }
2498
+ try {
2499
+ const after = await probeLocalMedia(probe.path);
2500
+ const same = after.sha256 === probe.sha256 && after.bytes === probe.bytes;
2501
+ if (!same) report.complete = false;
2502
+ addCheck(report, { id: "file_stability", status: same ? "pass" : "fail", ...same ? {} : { code: "MEDIA_CHANGED_DURING_CHECK" }, detail: same ? "SHA-256 and byte length matched before and after inspection." : "The file changed during inspection; findings must not be treated as a verified report for the current file." });
2503
+ } catch (error) {
2504
+ report.complete = false;
2505
+ addCheck(report, { id: "file_stability", status: "not_checked", code: errorCode(error), detail: "The input fingerprint could not be verified after inspection." });
2506
+ }
2507
+ return finish(report);
2508
+ }
2509
+
2510
+ // packages/cli/src/production-output.ts
2511
+ import { constants as constants6 } from "node:fs";
2512
+ import { mkdir as mkdir4, open as open6 } from "node:fs/promises";
2513
+ import { dirname as dirname5 } from "node:path";
2514
+ async function productionOutputPath(root, relativePath) {
2515
+ if (!relativePath.startsWith(".editorbrain-local/") || relativePath.split(/[\\/]/).some((p) => !p || p === ".." || p === ".") || /[\\\x00-\x1f\x7f]/.test(relativePath)) {
2516
+ 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.");
2517
+ }
2518
+ return checkedPath(root, relativePath, false);
2519
+ }
2520
+ async function prepareSampleDirectory(root, path) {
2521
+ const target = await productionOutputPath(root, path);
2522
+ await mkdir4(dirname5(target), { recursive: true });
2523
+ await productionOutputPath(root, path);
2524
+ return target;
2525
+ }
2526
+ async function readProductionJson(root, path) {
2527
+ const target = await checkedPath(root, path, false);
2528
+ const handle = await open6(target, constants6.O_RDONLY | constants6.O_NOFOLLOW | constants6.O_NONBLOCK);
2529
+ try {
2530
+ const stat = await handle.stat();
2531
+ 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.");
2532
+ return JSON.parse(await handle.readFile("utf8"));
2533
+ } finally {
2534
+ await handle.close();
2535
+ }
2536
+ }
2537
+ async function saveProductionReport(root, path, report) {
2538
+ const target = await productionOutputPath(root, path);
2539
+ if (!path.endsWith(".json")) throw new CliError("INVALID_ARGUMENT", "Report output needs a .json extension.", "Use a new path under .editorbrain-local/.");
2540
+ await atomicWrite(root, path, stableJson(report), { document: false, overwrite: false, mode: 384 });
2541
+ return target;
2542
+ }
2543
+ function summarizeProductionReport(value) {
2544
+ const r = value;
2545
+ const base = { schemaVersion: r.schemaVersion, kind: r.kind, summary: true, mediaUploaded: false, quality: "not_assessed" };
2546
+ if (r.kind === "editorbrain.media-inventory") return {
2547
+ ...base,
2548
+ complete: r.complete,
2549
+ contentReviewed: false,
2550
+ counts: { assets: r.assets.length, failures: r.failures.length, skipped: r.skipped.length },
2551
+ assets: r.assets.slice(0, 8),
2552
+ failures: r.failures.slice(0, 5),
2553
+ omitted: { assets: Math.max(0, r.assets.length - 8), failures: Math.max(0, r.failures.length - 5), skipped: r.skipped.length },
2554
+ 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."
2555
+ };
2556
+ 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." };
2557
+ if (r.kind === "editorbrain.production-plan-check") return {
2558
+ ...base,
2559
+ valid: r.valid,
2560
+ inventoryComplete: r.inventoryComplete,
2561
+ observations: r.observations,
2562
+ sourceFilesReverified: false,
2563
+ counts: { sources: r.coverage.length, findings: r.findings.length, repeatedPairs: r.repeatedPairs ?? 0 },
2564
+ 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),
2565
+ omittedFindings: Math.max(0, r.findings.length - 8),
2566
+ next: "Review source reuse and inspection gaps in the saved report. Observations are agent-declared, not independently certified."
2567
+ };
2568
+ if (r.kind === "editorbrain.render-check") return {
2569
+ ...base,
2570
+ complete: r.complete,
2571
+ technicalStatus: r.technicalStatus,
2572
+ file: r.file,
2573
+ checks: r.checks,
2574
+ findings: r.findings.slice(0, 8),
2575
+ findingsSummary: r.findingsSummary,
2576
+ omittedFindings: Math.max(0, r.findings.length - 8),
2577
+ next: "Reconcile technical candidates with the brief; inspect the actual video/audio. Technical checks do not assess narrative, aesthetics or SFX audibility."
2578
+ };
2579
+ throw new CliError("INVALID_REPORT", "Unknown production report kind.", "Use media inventory/sample or production check/plan-check.");
2580
+ }
2581
+
1772
2582
  // packages/cli/src/index.ts
1773
2583
  var HELP = `EditorBrain \u2014 portable editorial context for AI video agents
1774
2584
 
@@ -1811,6 +2621,10 @@ editorbrain narrative get <video-id> [--sha256 document-hash] [--event-offset 0]
1811
2621
  editorbrain styles list
1812
2622
  editorbrain styles create <style-id> [--name name] [--dir folder]
1813
2623
  editorbrain styles check <style-id> [--dir folder]
2624
+ editorbrain media inventory --input file-or-folder [--max-files 200] [--dir folder] [--out .editorbrain-local/report.json] [--details]
2625
+ editorbrain media sample --file source --at 0,5,12 --out .editorbrain-local/frames [--dir folder] [--details]
2626
+ editorbrain production check --file render [--profile local-profile.json] [--dir folder] [--out .editorbrain-local/report.json] [--details]
2627
+ editorbrain production plan-check --inventory local-inventory.json --plan local-plan.json [--dir folder] [--out .editorbrain-local/report.json] [--details]
1814
2628
  editorbrain styles recommend <project-id> [--assets tag,tag] [--capabilities tag,tag] [--style pack-id] [--duration seconds]
1815
2629
  editorbrain direction draft <project-id> [--assets tag,tag] [--capabilities tag,tag] [--style pack-id] [--duration seconds] [--out new-folder]
1816
2630
  editorbrain assets list [--limit 200] [--cursor opaque-cursor]
@@ -1826,14 +2640,16 @@ Resolve writes the source-hash lock. Export produces a handoff, not a rendered v
1826
2640
  Push/pull preserve conflicting changes; media uploads use the separate assets command.
1827
2641
  Skill install works offline, preserves existing instructions, and defaults to both agents.
1828
2642
  Styles create/check work locally without login or media upload; checks do not certify render quality.
2643
+ Media/production commands run locally with ffprobe/ffmpeg, without automatic installation or media upload.
2644
+ Production output is compact by default; --details expands it. --out saves full private reports without overwriting.
1829
2645
  Keys create stores the returned secret in the private user config; stdout has no key.
1830
2646
  Output is JSON except help; login progress/code goes to stderr. --url overrides the configured endpoint.
1831
2647
  `;
1832
2648
  function parseArgs(argv) {
1833
2649
  const positional = [];
1834
2650
  const flags = {};
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]);
2651
+ const booleans = /* @__PURE__ */ new Set(["help", "version", "offline", "dry-run", "key-stdin", "invite-stdin", "no-browser", "skip-mcp", "all", "share-authorized", "details"]);
2652
+ const known = /* @__PURE__ */ new Set(["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", "file", "revision", "expected-revision", "before", "scope", "id", "text", "conditions", "status", "index", "title", "days", ...booleans]);
1837
2653
  for (let i = 0; i < argv.length; i++) {
1838
2654
  const arg = argv[i];
1839
2655
  if (!arg.startsWith("--")) {
@@ -1859,16 +2675,16 @@ function output(value) {
1859
2675
  process.stdout.write(stableJson(value));
1860
2676
  }
1861
2677
  async function configRoot(create = false) {
1862
- let path = process.env.EDITORBRAIN_CONFIG_DIR ?? process.env.EDITORHARNESS_CONFIG_DIR ?? join2(homedir(), ".config", "editorbrain");
2678
+ let path = process.env.EDITORBRAIN_CONFIG_DIR ?? process.env.EDITORHARNESS_CONFIG_DIR ?? join3(homedir(), ".config", "editorbrain");
1863
2679
  if (!create && !process.env.EDITORBRAIN_CONFIG_DIR && !process.env.EDITORHARNESS_CONFIG_DIR) {
1864
2680
  try {
1865
- await lstat5(path);
2681
+ await lstat7(path);
1866
2682
  } catch (error) {
1867
2683
  if (error.code !== "ENOENT") throw error;
1868
- path = join2(homedir(), ".config", "editorharness");
2684
+ path = join3(homedir(), ".config", "editorharness");
1869
2685
  }
1870
2686
  }
1871
- if (create) await mkdir3(path, { recursive: true, mode: 448 });
2687
+ if (create) await mkdir5(path, { recursive: true, mode: 448 });
1872
2688
  return ensureRoot(path);
1873
2689
  }
1874
2690
  async function loadConfig() {
@@ -1885,7 +2701,7 @@ async function loadConfig() {
1885
2701
  async function saveConfig(config2) {
1886
2702
  const root = await configRoot(true);
1887
2703
  await atomicWrite(root, "config.json", stableJson(config2), { document: false, mode: 384 });
1888
- await chmod(join2(root, "config.json"), 384);
2704
+ await chmod(join3(root, "config.json"), 384);
1889
2705
  }
1890
2706
  async function config(args, requireKey = true) {
1891
2707
  const stored = await loadConfig();
@@ -1913,7 +2729,7 @@ async function init(args) {
1913
2729
  for (const path of Object.keys(files)) {
1914
2730
  const target = await checkedPath(root, path);
1915
2731
  try {
1916
- await lstat5(target);
2732
+ await lstat7(target);
1917
2733
  throw new Error(`Refusing to replace existing file: ${path}`);
1918
2734
  } catch (error) {
1919
2735
  if (error.code !== "ENOENT") throw error;
@@ -1925,13 +2741,13 @@ async function init(args) {
1925
2741
  for (const path of directories) {
1926
2742
  const target = await checkedPath(root, path, false);
1927
2743
  try {
1928
- if (!(await lstat5(target)).isDirectory()) throw new Error(`Not a directory: ${path}`);
2744
+ if (!(await lstat7(target)).isDirectory()) throw new Error(`Not a directory: ${path}`);
1929
2745
  } catch (error) {
1930
2746
  if (error.code !== "ENOENT") throw error;
1931
2747
  }
1932
2748
  }
1933
2749
  for (const [path, content] of Object.entries(files)) await atomicWrite(root, path, content, { overwrite: false });
1934
- for (const path of directories) await mkdir3(await checkedPath(root, path, false), { recursive: true });
2750
+ for (const path of directories) await mkdir5(await checkedPath(root, path, false), { recursive: true });
1935
2751
  const guidance = await installGuidance();
1936
2752
  const entryPoints = guidance.readInstructions.filter((item) => item.entryPointStatus === "created").map((item) => item.entryPoint);
1937
2753
  output({ root, created: Object.keys(files), entryPoints, guidance, next: `Edit brand tokens and brief, then run editorbrain resolve ${project}` });
@@ -1991,6 +2807,49 @@ async function main() {
1991
2807
  process.stdout.write(HELP);
1992
2808
  return;
1993
2809
  }
2810
+ if (args.flags.details && !["media", "production"].includes(command)) throw new CliError("INVALID_ARGUMENT", "--details is supported only by media and production.", "Read editorbrain help.");
2811
+ if (command === "media" || command === "production") {
2812
+ const required = (name) => {
2813
+ const value = str(args, name);
2814
+ if (!value) throw new CliError("INVALID_ARGUMENT", `--${name} is required.`, "Read editorbrain help.");
2815
+ return value;
2816
+ };
2817
+ if (argument) throw new CliError("INVALID_ARGUMENT", "Media/production inputs use named flags.", "Read editorbrain help.");
2818
+ const allowed = /* @__PURE__ */ new Set(["dir", "out", "details", ...command === "media" && subcommand === "inventory" ? ["input", "max-files"] : command === "media" && subcommand === "sample" ? ["file", "at"] : command === "production" && subcommand === "check" ? ["file", "profile"] : command === "production" && subcommand === "plan-check" ? ["inventory", "plan"] : []]);
2819
+ for (const flag of Object.keys(args.flags)) if (!allowed.has(flag)) throw new CliError("INVALID_ARGUMENT", `--${flag} is not supported for this local command.`, "Read editorbrain help.");
2820
+ const root2 = await ensureRoot(str(args, "dir") ?? process.cwd());
2821
+ const out = str(args, "out");
2822
+ if (out) await productionOutputPath(root2, out);
2823
+ const run2 = async () => {
2824
+ let report;
2825
+ if (command === "media" && subcommand === "inventory") {
2826
+ const limit = str(args, "max-files");
2827
+ report = await inventoryMedia({ input: resolve6(root2, required("input")), ...limit ? { maxFiles: Number(limit) } : {} });
2828
+ if (!report.complete) process.exitCode = 2;
2829
+ } else if (command === "media" && subcommand === "sample") {
2830
+ const times = required("at").split(",");
2831
+ 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");
2832
+ const directory = await prepareSampleDirectory(root2, required("out"));
2833
+ report = await sampleMedia({ file: resolve6(root2, required("file")), times: times.map(Number), outputDirectory: directory });
2834
+ } else if (command === "production" && subcommand === "check") {
2835
+ const profilePath = str(args, "profile");
2836
+ const profile = profilePath ? validateRenderCheckProfile(await readProductionJson(root2, profilePath)) : void 0;
2837
+ report = await checkRender({ file: resolve6(root2, required("file")), profile });
2838
+ if (report.technicalStatus === "fail") process.exitCode = 1;
2839
+ else if (!report.complete) process.exitCode = 2;
2840
+ } else if (command === "production" && subcommand === "plan-check") {
2841
+ const [inventory, plan] = await Promise.all([readProductionJson(root2, required("inventory")), readProductionJson(root2, required("plan"))]);
2842
+ report = checkProductionPlan(inventory, plan);
2843
+ if (!report.valid) process.exitCode = 1;
2844
+ else if (!report.inventoryComplete) process.exitCode = 2;
2845
+ } else throw new CliError("INVALID_ARGUMENT", "Unknown media/production command.", "Use media inventory/sample or production check/plan-check.");
2846
+ const reportPath = out && subcommand !== "sample" ? await saveProductionReport(root2, out, report) : void 0;
2847
+ output({ ...args.flags.details ? report : summarizeProductionReport(report), ...reportPath ? { reportPath } : {} });
2848
+ };
2849
+ if (out) await withWorkspaceLock(root2, run2, { migrateLegacy: false });
2850
+ else await run2();
2851
+ return;
2852
+ }
1994
2853
  if (command === "doctor") {
1995
2854
  let selected, configError;
1996
2855
  if (!args.flags.offline) {
@@ -2073,10 +2932,10 @@ async function main() {
2073
2932
  const destination = str(args, "out");
2074
2933
  if (destination) {
2075
2934
  if (command !== "direction" || !result.files) throw new Error("--out is supported only for direction draft.");
2076
- const outRoot = await ensureRoot(resolve4(str(args, "dir") ?? process.cwd(), destination), true);
2935
+ const outRoot = await ensureRoot(resolve6(str(args, "dir") ?? process.cwd(), destination), true);
2077
2936
  for (const path of Object.keys(result.files)) {
2078
2937
  try {
2079
- await lstat5(await checkedPath(outRoot, path, false));
2938
+ await lstat7(await checkedPath(outRoot, path, false));
2080
2939
  throw new Error(`Refusing to replace existing proposal: ${path}`);
2081
2940
  } catch (error) {
2082
2941
  if (error.code !== "ENOENT") throw error;
@@ -2102,7 +2961,7 @@ async function main() {
2102
2961
  if (!/^[a-zA-Z0-9_-]{1,100}$/.test(result.id) || typeof result.apiKey !== "string") throw new Error("Invalid key response");
2103
2962
  const file = `keys/${result.id}.json`;
2104
2963
  await atomicWrite(root2, file, stableJson({ url: client.url, apiKey: result.apiKey, id: result.id, label, role }), { document: false, mode: 384, overwrite: false });
2105
- output({ id: result.id, label, role, credentialFile: join2(root2, file) });
2964
+ output({ id: result.id, label, role, credentialFile: join3(root2, file) });
2106
2965
  return;
2107
2966
  }
2108
2967
  if (subcommand === "revoke" && argument) {
@@ -2189,8 +3048,8 @@ async function main() {
2189
3048
  return;
2190
3049
  }
2191
3050
  if (subcommand === "upload" && argument) {
2192
- const target = resolve4(str(args, "dir") ?? process.cwd(), argument);
2193
- const handle = await open5(target, constants5.O_RDONLY | constants5.O_NOFOLLOW);
3051
+ const target = resolve6(str(args, "dir") ?? process.cwd(), argument);
3052
+ const handle = await open7(target, constants7.O_RDONLY | constants7.O_NOFOLLOW);
2194
3053
  let bytes;
2195
3054
  try {
2196
3055
  const info = await handle.stat();
@@ -2200,24 +3059,24 @@ async function main() {
2200
3059
  await handle.close();
2201
3060
  }
2202
3061
  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" };
2203
- const mimeType = str(args, "mime") ?? types[extname(target).toLowerCase()];
3062
+ const mimeType = str(args, "mime") ?? types[extname3(target).toLowerCase()];
2204
3063
  if (!mimeType) throw new Error("Unsupported extension; specify --mime for a supported media format");
2205
- const hash = await sha256(bytes);
2206
- const result = await client.media(`/v1/assets/${hash}?name=${encodeURIComponent(basename(target))}`, { content: bytes, mimeType });
3064
+ const hash2 = await sha256(bytes);
3065
+ const result = await client.media(`/v1/assets/${hash2}?name=${encodeURIComponent(basename2(target))}`, { content: bytes, mimeType });
2207
3066
  const body = await result.json();
2208
- if (body.sha256 !== hash) throw new Error("Upload response hash mismatch");
3067
+ if (body.sha256 !== hash2) throw new Error("Upload response hash mismatch");
2209
3068
  output(body);
2210
3069
  return;
2211
3070
  }
2212
3071
  if (subcommand === "download" && argument && str(args, "out")) {
2213
3072
  if (!/^[a-f0-9]{64}$/.test(argument)) throw new Error("Asset ID must be its SHA-256");
2214
- const destination = resolve4(str(args, "dir") ?? process.cwd(), str(args, "out"));
2215
- const root2 = await ensureRoot(dirname3(destination), true);
2216
- await checkedPath(root2, basename(destination), false);
3073
+ const destination = resolve6(str(args, "dir") ?? process.cwd(), str(args, "out"));
3074
+ const root2 = await ensureRoot(dirname6(destination), true);
3075
+ await checkedPath(root2, basename2(destination), false);
2217
3076
  const response = await client.media(`/v1/assets/${argument}`);
2218
3077
  const bytes = new Uint8Array(await response.arrayBuffer());
2219
3078
  if (await sha256(bytes) !== argument) throw new Error("Downloaded asset failed SHA-256 verification");
2220
- await atomicWrite(root2, basename(destination), bytes, { document: false, overwrite: false });
3079
+ await atomicWrite(root2, basename2(destination), bytes, { document: false, overwrite: false });
2221
3080
  output({ path: destination, sha256: argument, bytes: bytes.length });
2222
3081
  return;
2223
3082
  }
@@ -2247,7 +3106,7 @@ async function main() {
2247
3106
  if (command === "search" && subcommand === "brief") {
2248
3107
  const path = str(args, "file");
2249
3108
  if (!path) throw new Error("Provide --file with the editorial brief JSON after inspecting the actual material.");
2250
- const handle = await open5(resolve4(path), constants5.O_RDONLY | constants5.O_NOFOLLOW);
3109
+ const handle = await open7(resolve6(path), constants7.O_RDONLY | constants7.O_NOFOLLOW);
2251
3110
  let brief;
2252
3111
  try {
2253
3112
  const stat = await handle.stat();
@@ -2269,7 +3128,7 @@ async function main() {
2269
3128
  const scriptPath = str(args, "script"), videoPath = str(args, "video");
2270
3129
  let script, assetId = str(args, "asset");
2271
3130
  const readInput = async (path, max) => {
2272
- const handle = await open5(resolve4(path), constants5.O_RDONLY | constants5.O_NOFOLLOW);
3131
+ const handle = await open7(resolve6(path), constants7.O_RDONLY | constants7.O_NOFOLLOW);
2273
3132
  try {
2274
3133
  const stat = await handle.stat();
2275
3134
  if (!stat.isFile() || stat.size < 1 || stat.size > max) throw new Error(`Input must be a regular file of at most ${max} bytes.`);
@@ -2289,10 +3148,10 @@ async function main() {
2289
3148
  if (assetId && !/^[a-f0-9]{64}$/.test(assetId)) throw new Error("--asset requires an owned production asset SHA-256.");
2290
3149
  if (videoPath) {
2291
3150
  const bytes = await readInput(videoPath, 12 * 1024 * 1024);
2292
- const mimeType = { ".mp4": "video/mp4", ".mov": "video/quicktime", ".webm": "video/webm" }[extname(videoPath).toLowerCase()];
3151
+ const mimeType = { ".mp4": "video/mp4", ".mov": "video/quicktime", ".webm": "video/webm" }[extname3(videoPath).toLowerCase()];
2293
3152
  if (!mimeType) throw new Error("Use MP4, MOV or WebM.");
2294
3153
  assetId = await sha256(bytes);
2295
- const uploaded = await client.media(`/v1/assets/${assetId}?name=${encodeURIComponent(basename(videoPath))}`, { content: bytes, mimeType });
3154
+ const uploaded = await client.media(`/v1/assets/${assetId}?name=${encodeURIComponent(basename2(videoPath))}`, { content: bytes, mimeType });
2296
3155
  if ((await uploaded.json()).sha256 !== assetId) throw new Error("Upload hash mismatch.");
2297
3156
  }
2298
3157
  if (!script && !assetId && !str(args, "project")) throw new Error("Provide --script, --video, --asset or --project with a saved script.");
@@ -2307,20 +3166,20 @@ async function main() {
2307
3166
  }
2308
3167
  if (subcommand === "get" && argument) {
2309
3168
  const document = str(args, "document");
2310
- const hash = str(args, "sha256");
2311
- if (hash && !document) throw new Error("--sha256 requires --document for library get.");
2312
- const result = await client.request(`/v1/library/${encodeURIComponent(argument)}${document ? `/documents?${new URLSearchParams({ path: document, ...hash ? { sha256: hash } : {} })}` : ""}`);
2313
- if (document && (typeof result.content !== "string" || await sha256(result.content) !== result.sha256 || hash && result.sha256 !== hash)) throw new CliError("HASH_MISMATCH", "Reference document verification failed.", "Do not use this evidence; retry the pinned version.");
3169
+ const hash2 = str(args, "sha256");
3170
+ if (hash2 && !document) throw new Error("--sha256 requires --document for library get.");
3171
+ const result = await client.request(`/v1/library/${encodeURIComponent(argument)}${document ? `/documents?${new URLSearchParams({ path: document, ...hash2 ? { sha256: hash2 } : {} })}` : ""}`);
3172
+ if (document && (typeof result.content !== "string" || await sha256(result.content) !== result.sha256 || hash2 && result.sha256 !== hash2)) throw new CliError("HASH_MISMATCH", "Reference document verification failed.", "Do not use this evidence; retry the pinned version.");
2314
3173
  output(result);
2315
3174
  return;
2316
3175
  }
2317
3176
  if (subcommand === "media" && argument && str(args, "out")) {
2318
- const hash = str(args, "sha256");
2319
- if (!hash || !/^[a-f0-9]{64}$/.test(hash)) throw new Error("--sha256 must be a media SHA-256 from library get.");
2320
- const destination = resolve4(str(args, "dir") ?? process.cwd(), str(args, "out"));
2321
- const root2 = await ensureRoot(dirname3(destination), true);
2322
- await checkedPath(root2, basename(destination), false);
2323
- const response = await client.media(`/v1/library/${encodeURIComponent(argument)}/media/${hash}`);
3177
+ const hash2 = str(args, "sha256");
3178
+ if (!hash2 || !/^[a-f0-9]{64}$/.test(hash2)) throw new Error("--sha256 must be a media SHA-256 from library get.");
3179
+ const destination = resolve6(str(args, "dir") ?? process.cwd(), str(args, "out"));
3180
+ const root2 = await ensureRoot(dirname6(destination), true);
3181
+ await checkedPath(root2, basename2(destination), false);
3182
+ const response = await client.media(`/v1/library/${encodeURIComponent(argument)}/media/${hash2}`);
2324
3183
  const reader = response.body?.getReader();
2325
3184
  if (!reader) throw new Error("Media body missing.");
2326
3185
  const chunks = [];
@@ -2336,9 +3195,9 @@ async function main() {
2336
3195
  chunks.push(value);
2337
3196
  }
2338
3197
  const bytes = Buffer.concat(chunks);
2339
- if (await sha256(bytes) !== hash) throw new CliError("HASH_MISMATCH", "Reference media verification failed.", "Do not use this file as evidence.");
2340
- await atomicWrite(root2, basename(destination), bytes, { document: false, overwrite: false });
2341
- output({ videoId: argument, path: destination, sha256: hash, bytes: size, mimeType: response.headers.get("Content-Type"), reviewed: false });
3198
+ if (await sha256(bytes) !== hash2) throw new CliError("HASH_MISMATCH", "Reference media verification failed.", "Do not use this file as evidence.");
3199
+ await atomicWrite(root2, basename2(destination), bytes, { document: false, overwrite: false });
3200
+ output({ videoId: argument, path: destination, sha256: hash2, bytes: size, mimeType: response.headers.get("Content-Type"), reviewed: false });
2342
3201
  return;
2343
3202
  }
2344
3203
  throw new Error("Use library search, library get, or library media <video-id> --sha256 hash --out file.");
@@ -2373,7 +3232,7 @@ async function main() {
2373
3232
  const adapter = str(args, "adapter") ?? "neutral";
2374
3233
  if (!["neutral", "hyperframes", "remotion"].includes(adapter)) throw new Error("Adapter must be neutral, hyperframes or remotion");
2375
3234
  const generated = exportProject(resolved, adapter);
2376
- const destination = resolve4(root, str(args, "out") ?? `videos/${project}/plans/${adapter}`);
3235
+ const destination = resolve6(root, str(args, "out") ?? `videos/${project}/plans/${adapter}`);
2377
3236
  const outRoot = await ensureRoot(destination, true);
2378
3237
  for (const [path, content] of Object.entries(generated)) await atomicWrite(outRoot, path, content, { document: false });
2379
3238
  output({ adapter, kind: "editorial_handoff", rendered: false, directory: outRoot, files: Object.keys(generated), warnings: resolved.warnings });