editorbrain 0.6.0 → 0.8.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.
package/README.md CHANGED
@@ -5,20 +5,21 @@ Editorial references, brand context and portable video projects for Claude Code
5
5
  Inspect the CLI without configuring your agent:
6
6
 
7
7
  ```sh
8
- npx editorbrain@0.6.0 help
8
+ npx editorbrain@0.8.0 help
9
9
  ```
10
10
 
11
11
  For a persistent installation:
12
12
 
13
13
  ```sh
14
- npm install -g editorbrain@0.6.0
14
+ npm install -g editorbrain@0.8.0
15
15
  editorbrain login --url https://api.editorbrain.com
16
16
  editorbrain status --url https://api.editorbrain.com
17
- editorbrain pull --dir ./my-videos --url https://api.editorbrain.com
17
+ editorbrain projects list
18
+ editorbrain pull --project YOUR_PROJECT_ID --dir ./my-videos --url https://api.editorbrain.com
18
19
  editorbrain skill install --dir ./my-videos --agent both
19
20
  ```
20
21
 
21
- Create an account and follow connection instructions at https://editorbrain.com/app/settings. Login requires browser consent. Skills install locally; MCP authentication is configured separately. Preserve existing customized instructions and inspect installation conflicts. The package contains the CLI and skill documentation, not private library content or customer credentials. Editing and rendering use your agent's external tools.
22
+ Create an account and follow connection instructions at https://editorbrain.com/app/settings. Login requires browser consent. Pull downloads only the selected project and its brand/style context, remembers the selection, and leaves reference evidence remote. Use `documents list/get` for additional saved documents; `pull --all` is an explicit full-workspace operation. Skills install locally; MCP authentication is configured separately. Preserve existing customized instructions and inspect installation conflicts. The package contains the CLI and skill documentation, not private library content or customer credentials. Editing and rendering use your agent's external tools.
22
23
 
23
24
  ## Installation and permissions
24
25
 
@@ -1,9 +1,5 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- // packages/cli/src/setup.ts
4
- import { spawn } from "node:child_process";
5
- import { resolve as resolve3 } from "node:path";
6
-
7
3
  // packages/core/src/index.ts
8
4
  var SCHEMA_VERSION = 1;
9
5
  var MAX_DOCUMENT_BYTES = 128 * 1024;
@@ -107,6 +103,7 @@ function validateDocument(path, content) {
107
103
  if (isProject) {
108
104
  for (const key of ["id", "brand", "style", "outputProfile"]) if (typeof doc[key] !== "string" || !identifier.test(doc[key])) add(`${key} must be a valid identifier`);
109
105
  if (!object(doc.intent) || !nonempty(doc.intent.topic) || !nonempty(doc.intent.goal)) add("intent requires topic and goal");
106
+ if (doc.reviewMode !== void 0 && !["direct", "directions", "sample"].includes(doc.reviewMode)) add("reviewMode must be direct, directions or sample");
110
107
  if (doc.script !== void 0 && typeof doc.script !== "string") add("script must be text");
111
108
  if (doc.overrides !== void 0) {
112
109
  if (!object(doc.overrides)) add("overrides must be an object");
@@ -546,7 +543,7 @@ var Client = class {
546
543
  for (let attempt = 0; attempt < 3; attempt++) {
547
544
  let response;
548
545
  try {
549
- response = await this.fetcher(`${this.url}${path}`, { method, headers, body: options.body === void 0 ? void 0 : JSON.stringify(options.body), redirect: "error", signal: AbortSignal.timeout(2e4) });
546
+ response = await this.fetcher(`${this.url}${path}`, { method, headers, body: options.body === void 0 ? void 0 : JSON.stringify(options.body), redirect: "error", signal: AbortSignal.timeout(Math.min(18e4, Math.max(1e3, options.timeoutMs ?? 2e4))) });
550
547
  } catch {
551
548
  if (readOnly && attempt < 2) {
552
549
  await this.runtime.sleep(250 * 2 ** attempt + this.runtime.random() * 100);
@@ -575,8 +572,8 @@ var Client = class {
575
572
  async me() {
576
573
  return this.request("/v1/me");
577
574
  }
578
- async list() {
579
- const data = await this.request("/v1/documents");
575
+ async list(prefix = "") {
576
+ const data = await this.request("/v1/documents" + (prefix ? "?" + new URLSearchParams({ prefix }) : ""));
580
577
  return data.documents;
581
578
  }
582
579
  async get(path) {
@@ -604,6 +601,7 @@ async function loadSyncState(root, endpoint, workspaceId) {
604
601
  return readText(root, ".editorharness/.sync.json", false);
605
602
  });
606
603
  const state = JSON.parse(raw);
604
+ if (state.projectId !== void 0) assertIdentifier(state.projectId);
607
605
  if (state.schemaVersion !== 1 || typeof state.documents !== "object") throw new Error("Invalid local sync state");
608
606
  if (state.endpoint !== endpoint || state.workspaceId !== workspaceId) throw new CliError("WORKSPACE_MISMATCH", "This folder is bound to a different endpoint/workspace.", "Use a separate folder; never merge private documents across spaces.");
609
607
  for (const [path, item] of Object.entries(state.documents)) {
@@ -679,7 +677,7 @@ ${conflicts.join("\n")}`, "Run push --dry-run and reconcile the reported paths."
679
677
  async function pullWorkspace(client, root, local, options = {}) {
680
678
  const me = await client.me();
681
679
  const state = await loadSyncState(root, client.url, me.workspaceId);
682
- const summaries = await client.list();
680
+ const summaries = options.summaries ?? await client.list();
683
681
  const pending = [];
684
682
  const conflicts = [];
685
683
  let unchanged = 0;
@@ -704,12 +702,13 @@ async function pullWorkspace(client, root, local, options = {}) {
704
702
  ${conflicts.join("\n")}`, "Run pull --dry-run and reconcile the reported paths.", false, { conflicts });
705
703
  const documents = [];
706
704
  for (const summary of pending) {
707
- const doc = await client.get(summary.path);
705
+ const doc = options.prefetched?.get(summary.path) ?? await client.get(summary.path);
708
706
  checkRemote(doc);
709
707
  const validation = validateDocument(doc.path, doc.content);
710
708
  if (!validation.valid || doc.path !== summary.path || doc.revision !== summary.revision || doc.sha256 !== summary.sha256 || await sha256(doc.content) !== doc.sha256) throw new Error(`Document changed during pull or failed integrity: ${summary.path}. Retry.`);
711
709
  documents.push(doc);
712
710
  }
711
+ await options.beforeWrite?.();
713
712
  const downloaded = [];
714
713
  for (const doc of documents) {
715
714
  let current;
@@ -728,12 +727,57 @@ ${conflicts.join("\n")}`, "Run pull --dry-run and reconcile the reported paths."
728
727
  return { downloaded, unchanged };
729
728
  }
730
729
 
730
+ // packages/cli/src/project-selection.ts
731
+ async function listProjects(client) {
732
+ const documents = await client.list("videos/");
733
+ return documents.filter((row) => /^videos\/[^/]+\/project\.json$/.test(row.path)).map((row) => ({ id: assertIdentifier(row.path.split("/")[1]), revision: row.revision, updatedAt: row.updatedAt }));
734
+ }
735
+ async function pullProject(client, root, local, options = {}) {
736
+ const me = await client.me(), state = await loadSyncState(root, client.url, me.workspaceId);
737
+ let projectId = options.projectId ?? state.projectId;
738
+ if (!projectId) {
739
+ const localIds = Object.keys(local).filter((path) => /^videos\/[^/]+\/project\.json$/.test(path)).map((path) => path.split("/")[1]);
740
+ if (localIds.length === 1) projectId = localIds[0];
741
+ }
742
+ if (!projectId) {
743
+ const projects = await listProjects(client);
744
+ if (projects.length !== 1) throw new CliError("PROJECT_SELECTION_REQUIRED", projects.length ? "Choose the project to prepare." : "No saved projects are available.", "Use editorbrain projects list, then setup or pull --project <id>. Create your first project in the app if needed.", false, { projects });
745
+ projectId = projects[0].id;
746
+ }
747
+ assertIdentifier(projectId);
748
+ const projectPath = `videos/${projectId}/project.json`, projectDoc = await client.get(projectPath);
749
+ if (projectDoc.path !== projectPath || !validateDocument(projectPath, projectDoc.content).valid || await sha256(projectDoc.content) !== projectDoc.sha256) throw new CliError("INVALID_PROJECT", "Project content failed validation.", "Check the saved project before retrying.");
750
+ const project = JSON.parse(projectDoc.content);
751
+ if (project.id !== projectId) throw new CliError("INVALID_PROJECT", "Project ID does not match its path.", "Correct the project in the app.");
752
+ const wanted = /* @__PURE__ */ new Set([...projectSources(project), ...optionalProjectSources(project)]);
753
+ const prefixes = [`.editorbrain/brands/${project.brand}/`, `.editorbrain/styles/${project.style}/`, `.editorbrain/output-profiles/${project.outputProfile}.json`, `videos/${projectId}/`];
754
+ const documents = [];
755
+ for (const prefix of prefixes) for (const doc of await client.list(prefix)) if (wanted.has(doc.path) && !documents.some((row) => row.path === doc.path)) documents.push(doc);
756
+ const initial = documents.find((row) => row.path === projectPath);
757
+ if (!initial || initial.revision !== projectDoc.revision || initial.sha256 !== projectDoc.sha256) throw new CliError("SYNC_CONFLICT", "Project selection changed during preparation.", "Retry the current project.");
758
+ const missing = projectSources(project).filter((path) => !documents.some((row) => row.path === path));
759
+ if (missing.length) throw new CliError("INCOMPLETE_PROJECT", "The project is missing required context.", "Complete these files in the app before preparing the project.", false, { missing });
760
+ const result = await pullWorkspace(client, root, local, { dryRun: options.dryRun, summaries: documents, prefetched: /* @__PURE__ */ new Map([[projectPath, projectDoc]]), beforeWrite: async () => {
761
+ const fresh = await client.get(projectPath);
762
+ if (fresh.sha256 !== projectDoc.sha256 || fresh.revision !== projectDoc.revision) throw new CliError("SYNC_CONFLICT", "Project selection changed during download.", "Retry; no downloaded files were written.");
763
+ } });
764
+ if (!options.dryRun) {
765
+ const current = await loadSyncState(root, client.url, me.workspaceId);
766
+ await saveSyncState(root, { ...current, projectId });
767
+ }
768
+ return { ...result, projectId, scope: "project_context", libraryDownloaded: false };
769
+ }
770
+
771
+ // packages/cli/src/setup.ts
772
+ import { spawn } from "node:child_process";
773
+ import { resolve as resolve3 } from "node:path";
774
+
731
775
  // packages/cli/src/agent-guidance.ts
732
776
  import { createHash } from "node:crypto";
733
777
  import { constants as constants2 } from "node:fs";
734
778
  import { lstat as lstat2, open as open2, readdir as readdir2, rename as rename2 } from "node:fs/promises";
735
779
  import { fileURLToPath } from "node:url";
736
- var previousSkillHashes = { "SKILL.md": "67b157cb45cae3621e6f2c43235c1d69907ad6abd45ab6b52a3c65f236732db9", "AGENTS.md": "4f3ef860ade3a31aa7c6a6aab5cf6ec2a700850bf19ea2320324c12c2e0fbf5b", "references/cli-mcp.md": "8e011c31368cef2117a28ee972b39aab9535eeb0f601a4a0f9d6fc019b22c1c2", "references/production-and-review.md": "055c6df9e0f167be148b0a5c0bba5b43e108eb1f58f478d88b3536c6589ac7b3", "references/workspace-files.md": "e72af2871f42cd776be1bfff8009ed5562780e7f8a65b12c27887f3880631f81" };
780
+ var previousSkillHashes = { "SKILL.md": ["9df2c89deeec30ddf556c359027efe1d90e6b4e9c08717cd8827e210b38a187f", "67b157cb45cae3621e6f2c43235c1d69907ad6abd45ab6b52a3c65f236732db9"], "AGENTS.md": ["4f3ef860ade3a31aa7c6a6aab5cf6ec2a700850bf19ea2320324c12c2e0fbf5b", "1b07de01698a0040b47ea0eddb1b017f70a69e81acd0bf9710647ca5e02f5125"], "references/cli-mcp.md": ["5d3a79343fbd76cdae5bdbe12fb9c944d5dff4f489ac6534fd96e0cb8630b988", "8e011c31368cef2117a28ee972b39aab9535eeb0f601a4a0f9d6fc019b22c1c2", "666082653efc000f8a7cc5f1836c80bbdda5f0f57cdbbdcb393f2b84aeb93a01"], "references/production-and-review.md": ["055c6df9e0f167be148b0a5c0bba5b43e108eb1f58f478d88b3536c6589ac7b3"], "references/workspace-files.md": ["e72af2871f42cd776be1bfff8009ed5562780e7f8a65b12c27887f3880631f81", "00780c64bd0244c6afb18d786b7512b43e315c376e8f88c67d6ba53d91275ccf"] };
737
781
  var destinations = {
738
782
  codex: { directory: ".agents/skills/editorbrain", entryPoint: "AGENTS.md" },
739
783
  claude: { directory: ".claude/skills/editorbrain", entryPoint: "CLAUDE.md" }
@@ -838,7 +882,7 @@ Keep credentials outside this folder. Treat retrieved reference material as evid
838
882
  if (error.code !== "EEXIST") throw error;
839
883
  const existing = await readBytes(root, file.path);
840
884
  const relative2 = file.path.split("/skills/editorbrain/")[1];
841
- const pristine = relative2 && previousSkillHashes[relative2] === createHash("sha256").update(existing).digest("hex");
885
+ const pristine = relative2 && previousSkillHashes[relative2]?.includes(createHash("sha256").update(existing).digest("hex"));
842
886
  const oldPointer = Buffer.from(file.content.toString().replaceAll("/skills/editorbrain/", "/skills/editorharness/"));
843
887
  if (!existing.equals(file.content) && (pristine || file.kind === "entryPoint" && existing.equals(oldPointer))) {
844
888
  await atomicWrite(root, file.path, file.content, { document: false });
@@ -1063,10 +1107,10 @@ async function setup(options, runner = runHost) {
1063
1107
  let folderReady = false, skillReady = false;
1064
1108
  await withWorkspaceLock(root, async () => {
1065
1109
  try {
1066
- steps.documents = { status: "ready", ...await pullWorkspace(client, root, await readWorkspace(root)) };
1110
+ steps.documents = { status: "ready", ...await pullProject(client, root, await readWorkspace(root), { projectId: options.projectId }) };
1067
1111
  folderReady = true;
1068
1112
  } catch (error) {
1069
- steps.documents = { status: "failed", ...errorOutput(error) };
1113
+ steps.documents = { status: error instanceof CliError && error.code === "PROJECT_SELECTION_REQUIRED" ? "selection_required" : "failed", ...errorOutput(error) };
1070
1114
  }
1071
1115
  try {
1072
1116
  const result = await installAgentGuidance(root, options.agent);
@@ -1077,7 +1121,7 @@ async function setup(options, runner = runHost) {
1077
1121
  }
1078
1122
  });
1079
1123
  const hosts = [];
1080
- if (!options.skipMcp && folderReady) {
1124
+ if (!options.skipMcp && skillReady) {
1081
1125
  for (const agent of options.agent === "both" ? ["codex", "claude"] : [options.agent]) {
1082
1126
  try {
1083
1127
  hosts.push(await configureMcp(agent, root, client.url, !!options.noBrowser, runner));
@@ -1302,7 +1346,7 @@ async function prepareDirectionContext(root, client, projectId) {
1302
1346
  var HELP = `EditorBrain \u2014 portable editorial context for AI video agents
1303
1347
 
1304
1348
  editorbrain version
1305
- editorbrain setup --dir folder --agent codex|claude|both [--workspace id] [--no-browser] [--skip-mcp]
1349
+ editorbrain setup --dir folder --agent codex|claude|both [--project id] [--workspace id] [--no-browser] [--skip-mcp]
1306
1350
  editorbrain doctor [--dir folder] [--agent codex|claude|both] [--offline]
1307
1351
  editorbrain init [--dir folder] [--brand id] [--style id] [--project id] [--topic text] [--goal text] [--agent codex|claude|both]
1308
1352
  editorbrain skill install [--dir folder] [--agent codex|claude|both]
@@ -1313,7 +1357,13 @@ editorbrain login [--url https://server] [--no-browser] [--timeout seconds]
1313
1357
  editorbrain login [--url https://server] --key-stdin
1314
1358
  editorbrain register --url https://server --name workspace --invite-stdin
1315
1359
  editorbrain status
1316
- editorbrain push|pull [--dir folder] [--dry-run]
1360
+ editorbrain pull [--project id | --all] [--dir folder] [--dry-run]
1361
+ editorbrain push [--dir folder] [--dry-run]
1362
+ editorbrain projects list
1363
+ editorbrain documents list [--prefix path]
1364
+ editorbrain documents get <path>
1365
+ editorbrain workflow check [--project id] [--previous-version hash]
1366
+ editorbrain search edit [--script file | --video file | --asset sha256 | --project id] [--requirements text] [--assets tag,tag] [--mode styles|events|both] [--review direct|directions|sample]
1317
1367
  editorbrain library search [query]
1318
1368
  editorbrain library get <video-id> [--document relative-source-path] [--sha256 source-hash]
1319
1369
  editorbrain library media <video-id> --sha256 media-hash --out file
@@ -1340,8 +1390,8 @@ Output is JSON except help; login progress/code goes to stderr. --url overrides
1340
1390
  function parseArgs(argv) {
1341
1391
  const positional = [];
1342
1392
  const flags = {};
1343
- const booleans = /* @__PURE__ */ new Set(["help", "version", "offline", "dry-run", "key-stdin", "invite-stdin", "no-browser", "skip-mcp"]);
1344
- 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", "sha256", "kind", "channel", "epistemic-status", "event-offset", "relation-offset", ...booleans]);
1393
+ const booleans = /* @__PURE__ */ new Set(["help", "version", "offline", "dry-run", "key-stdin", "invite-stdin", "no-browser", "skip-mcp", "all"]);
1394
+ 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", ...booleans]);
1345
1395
  for (let i = 0; i < argv.length; i++) {
1346
1396
  const arg = argv[i];
1347
1397
  if (!arg.startsWith("--")) {
@@ -1487,6 +1537,8 @@ Waiting for authorization. Press Ctrl+C to cancel.
1487
1537
  async function main() {
1488
1538
  const args = parseArgs(process.argv.slice(2));
1489
1539
  const [command, subcommand, argument] = args.positional;
1540
+ if (args.flags.all && command !== "pull") throw new Error("--all is supported only by pull.");
1541
+ if (args.flags.all && str(args, "project")) throw new Error("Choose --project or --all, not both.");
1490
1542
  if (command === "version" || args.flags.version) {
1491
1543
  output({ name: "EditorBrain", version: await cliVersion() });
1492
1544
  return;
@@ -1520,7 +1572,7 @@ async function main() {
1520
1572
  return;
1521
1573
  }
1522
1574
  if (command === "setup") {
1523
- const result = await setup({ directory: str(args, "dir") ?? "./mis-videos", agent: parseAgentTarget(str(args, "agent")), workspaceId: str(args, "workspace"), skipMcp: !!args.flags["skip-mcp"], noBrowser: !!args.flags["no-browser"], getConfig: () => config(args, false), login: () => login(args, true) });
1575
+ const result = await setup({ directory: str(args, "dir") ?? "./mis-videos", agent: parseAgentTarget(str(args, "agent")), workspaceId: str(args, "workspace"), projectId: str(args, "project"), skipMcp: !!args.flags["skip-mcp"], noBrowser: !!args.flags["no-browser"], getConfig: () => config(args, false), login: () => login(args, true) });
1524
1576
  output(result);
1525
1577
  if (result.status !== "ready") process.exitCode = 2;
1526
1578
  return;
@@ -1603,6 +1655,24 @@ async function main() {
1603
1655
  }
1604
1656
  throw new Error("Use keys list, keys create --label name, or keys revoke <key-id>");
1605
1657
  }
1658
+ if (command === "projects" && subcommand === "list") {
1659
+ output({ projects: await listProjects(new Client(await config(args))) });
1660
+ return;
1661
+ }
1662
+ if (command === "documents") {
1663
+ const client = new Client(await config(args));
1664
+ if (subcommand === "list") {
1665
+ output({ documents: await client.list(str(args, "prefix") ?? "") });
1666
+ return;
1667
+ }
1668
+ if (subcommand === "get" && argument) {
1669
+ const doc = await client.get(argument);
1670
+ if (await sha256(doc.content) !== doc.sha256) throw new Error("Document integrity failed");
1671
+ output(doc);
1672
+ return;
1673
+ }
1674
+ throw new Error("Use documents list --prefix path or documents get <path>.");
1675
+ }
1606
1676
  if (command === "assets") {
1607
1677
  const client = new Client(await config(args));
1608
1678
  if (subcommand === "list") {
@@ -1668,6 +1738,46 @@ async function main() {
1668
1738
  }
1669
1739
  throw new Error("Use narrative search [query] or narrative get <video-id>.");
1670
1740
  }
1741
+ if (command === "workflow" && subcommand === "check") {
1742
+ const client = new Client(await config(args));
1743
+ output(await client.request("/v1/workflow/check", { method: "POST", body: { projectId: str(args, "project"), previousVersion: str(args, "previous-version") } }));
1744
+ return;
1745
+ }
1746
+ if (command === "search" && subcommand === "edit") {
1747
+ const client = new Client(await config(args));
1748
+ const scriptPath = str(args, "script"), videoPath = str(args, "video");
1749
+ let script, assetId = str(args, "asset");
1750
+ const readInput = async (path, max) => {
1751
+ const handle = await open4(resolve4(path), constants4.O_RDONLY | constants4.O_NOFOLLOW);
1752
+ try {
1753
+ const stat = await handle.stat();
1754
+ if (!stat.isFile() || stat.size < 1 || stat.size > max) throw new Error(`Input must be a regular file of at most ${max} bytes.`);
1755
+ return await handle.readFile();
1756
+ } finally {
1757
+ await handle.close();
1758
+ }
1759
+ };
1760
+ if (videoPath && assetId) throw new Error("Use --video or --asset, not both.");
1761
+ if (scriptPath) {
1762
+ script = new TextDecoder("utf-8", { fatal: true }).decode(await readInput(scriptPath, 64e3));
1763
+ if (script.length > 16e3) throw new Error("Script must contain at most 16000 characters.");
1764
+ }
1765
+ const mode = str(args, "mode") ?? "both", reviewMode = str(args, "review");
1766
+ if (!["styles", "events", "both"].includes(mode)) throw new Error("--mode must be styles, events or both.");
1767
+ if (reviewMode && !["direct", "directions", "sample"].includes(reviewMode)) throw new Error("--review must be direct, directions or sample.");
1768
+ if (assetId && !/^[a-f0-9]{64}$/.test(assetId)) throw new Error("--asset requires an owned production asset SHA-256.");
1769
+ if (videoPath) {
1770
+ const bytes = await readInput(videoPath, 12 * 1024 * 1024);
1771
+ const mimeType = { ".mp4": "video/mp4", ".mov": "video/quicktime", ".webm": "video/webm" }[extname(videoPath).toLowerCase()];
1772
+ if (!mimeType) throw new Error("Use MP4, MOV or WebM.");
1773
+ assetId = await sha256(bytes);
1774
+ const uploaded = await client.media(`/v1/assets/${assetId}?name=${encodeURIComponent(basename(videoPath))}`, { content: bytes, mimeType });
1775
+ if ((await uploaded.json()).sha256 !== assetId) throw new Error("Upload hash mismatch.");
1776
+ }
1777
+ if (!script && !assetId && !str(args, "project")) throw new Error("Provide --script, --video, --asset or --project with a saved script.");
1778
+ output(await client.request("/v1/search/edit", { method: "POST", timeoutMs: 18e4, body: { script, assetId, projectId: str(args, "project"), requirements: str(args, "requirements") ?? "", availableAssets: (str(args, "assets") ?? "").split(",").filter(Boolean), mode, reviewMode } }));
1779
+ return;
1780
+ }
1671
1781
  if (command === "library") {
1672
1782
  const client = new Client(await config(args));
1673
1783
  if (subcommand === "search") {
@@ -1717,7 +1827,7 @@ async function main() {
1717
1827
  const run = async () => {
1718
1828
  const files = await readWorkspace(root);
1719
1829
  if (command === "pull") {
1720
- output(await pullWorkspace(new Client(await config(args)), root, files, { dryRun: !!args.flags["dry-run"] }));
1830
+ output(await (args.flags.all ? pullWorkspace(new Client(await config(args)), root, files, { dryRun: !!args.flags["dry-run"] }) : pullProject(new Client(await config(args)), root, files, { projectId: str(args, "project"), dryRun: !!args.flags["dry-run"] })));
1721
1831
  return;
1722
1832
  }
1723
1833
  if (Object.keys(files).length === 0) throw new Error("No portable documents found. Run init or pull first.");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "editorbrain",
3
- "version": "0.6.0",
3
+ "version": "0.8.0",
4
4
  "description": "EditorBrain editorial knowledge, portable workspaces and skills for AI video agents",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",
@@ -1,6 +1,6 @@
1
1
  # Customer agent contract
2
2
 
3
- Your responsibility is to turn Harness evidence into editorial decisions suitable for this project, execute the requested work, and check the result. Do not stop at matching a palette or exporting a plan when the user asked for an edited video.
3
+ Your responsibility is to turn EditorBrain evidence into editorial decisions suitable for this project, execute the requested work, and check the result. Do not stop at matching a palette or exporting a plan when the user asked for an edited video.
4
4
 
5
5
  ## Understand the project
6
6
 
@@ -10,12 +10,23 @@ Your responsibility is to turn Harness evidence into editorial decisions suitabl
10
10
  - Separate a faithful preset transfer, an adaptation of an editorial family, and inspiration from selected techniques. Infer the intended degree from the request and record it briefly. Preserve the distinctive features the user asked for; do not silently substitute a generic approximation.
11
11
  - If evidence supports only part of a faithful transfer, name that part and the unverified details. A provisional plan can be useful, but missing media or final timing must remain explicit production dependencies rather than a claim that it is fully ready to execute.
12
12
 
13
+ ## Selective access
14
+
15
+ Prepare only the active project's immediate context with `editorbrain pull --project <id>`.
16
+ Use `editorbrain projects list` to discover IDs; ask which project if the intent is ambiguous.
17
+ Default pull remembers the successful selection; never use `--all` during normal onboarding.
18
+ Reference-library documents/media remain remote and are retrieved to answer a concrete
19
+ editing question. Existing proposals or other project documents can be discovered through
20
+ `documents list --prefix videos/<id>/` and read with `documents get <path>` or MCP
21
+ `list_documents` / `read_document`. No need to enumerate or read the whole workspace.
22
+
13
23
  ## Read context and evidence
14
24
 
15
25
  Read [the workspace file map](references/workspace-files.md) for the full structure, required/optional files and token precedence. Start with `videos/<project-id>/project.json` to discover the selected brand, style and output profile. Read their actual files plus the brief and direction; do not assume that all documents under the workspace are included automatically by `resolve`.
16
26
 
17
27
  - Consult style packs for methods and the reference library for their execution. Search by communicative function as well as topic: comparison, demonstration, reveal, cumulative typography, collage, atmosphere, callback.
18
- - A literal search is not a quality ranking. Recommendations use explicit rules and declared resources; they do not inspect footage or prove fit. Review their reasons, limitations and required assets against the actual project.
28
+ - Start with `search_for_edit` / `editorbrain search edit`, supplying the actual script or owned video and the project ID for brand constraints. It analyzes new material before semantic retrieval and candidate review; inspect its uncertainty, source hashes, coverage and missing assets. Its model reading is not human verification. Legacy literal search and rule-based recommendations remain lookup tools, not equivalents to this content-first flow.
29
+ - Read `check_workflow` at the start, before proposals, before paid resource generation, and before the full render. Precedence: current explicit user request > project reviewMode > workspace default. Direct means proceed and review; directions means offer 2–3 fitting options with labelled library evidence; sample means show a representative customer-material frame or short clip and obtain approval. Do not require a new approval when the relevant version is already approved. Use a moving sample when rhythm or sound is what needs review. Save the workflow version and any approval with the project direction. A receipt acknowledges reading, not actual compliance.
19
30
  - Retrieve evidence to answer a concrete question. Use representative images for composition; intervals and source video for movement; the audio review and accessible audio for sound; narrative events and relations for hooks, development, payoff and CTA.
20
31
  - Read source documents using paths actually returned by the reference manifest. Pin hashes where supported. A script summary is not word-level alignment, and a provider candidate is not a verified measurement.
21
32
  - Check coverage, truncation and pagination. Retrieve the relevant remaining pages or source document before generalizing to an entire reference. Do not claim complete analysis from a partial tool response.
@@ -9,6 +9,8 @@ The public product is EditorBrain. The npm package is `editorbrain`; `editorbrai
9
9
 
10
10
  Convert reference evidence into an appropriate edit for this project. Reading the library starts the work; quality is demonstrated in the delivered video.
11
11
 
12
+ Before selecting a style, use `check_workflow` (CLI: `editorbrain workflow check`) and `search_for_edit` (CLI: `editorbrain search edit`) with the actual script or owned production video and project ID. Respect the returned review mode: direct editing, a choice of directions, or approval of a customer-material sample. Check the preference version again before costly generation and full rendering; a web change cannot interrupt a running tool. Honor current explicit user instructions and existing approval. If these tools are unavailable, report the version limitation and inspect material and evidence explicitly; literal search is not an equivalent quality ranking.
13
+
12
14
  1. Read the bundled [AGENTS.md](AGENTS.md) before using the workflow. It is this skill's editorial contract; do not assume your host auto-loads it.
13
15
  2. Read [CLI and MCP](references/cli-mcp.md) when connecting, locating documents, retrieving reference media or saving work. Discover the installed CLI help and connected MCP tool schemas; they take precedence over examples for version-specific arguments.
14
16
  3. Read [Workspace files](references/workspace-files.md) when configuring a brand/style, creating projects, resolving preferences, saving feedback or upgrading an installed skill.
@@ -1,6 +1,6 @@
1
1
  # CLI and MCP: read, retrieve, save
2
2
 
3
- Use the connected MCP schema and `editorbrain help` to confirm the installed contract. Examples below describe the 0.6.0 customer workflow; older installations may lack the skill installer or reference-media tool. Do not invent commands from historical architecture documents.
3
+ Use the connected MCP schema and `editorbrain help` to confirm the installed contract. Examples below describe the 0.7.0 customer workflow; older installations may lack the skill installer or reference-media tool. Do not invent commands from historical architecture documents.
4
4
 
5
5
  API/MCP: `https://api.editorbrain.com/mcp`. The host must connect this server separately; installing a skill does not register or authorize MCP.
6
6
 
@@ -10,7 +10,8 @@ An existing account normally starts with:
10
10
 
11
11
  ```sh
12
12
  editorbrain status
13
- editorbrain pull --dir ./my-videos
13
+ editorbrain projects list
14
+ editorbrain pull --project <id> --dir ./my-videos
14
15
  editorbrain skill install --dir ./my-videos --agent both
15
16
  ```
16
17
 
@@ -20,13 +21,20 @@ Skill installation is offline and independent of login. It creates `.agents/skil
20
21
 
21
22
  Existing root `AGENTS.md`/`CLAUDE.md` are preserved. Read the install result's instruction paths and conflict reports. A preserved root guide does not mean the native skill failed; a conflicting existing skill needs a deliberate merge, not an overwrite. Do not replace the user's custom instructions to finish setup.
22
23
 
23
- For a separate local workspace, `editorbrain init --dir ./new-video --brand <id> --style <id> --project <id>` creates portable context and installs guidance. Do not initialize over a pulled workspace to reset it. `pull` and `push` synchronize documents, not the installed native skill folders or local render media.
24
+ For a separate local workspace, `editorbrain init --dir ./new-video --brand <id> --style <id> --project <id>` creates portable context and installs guidance. Do not initialize over a pulled workspace to reset it. `pull` prepares selected project documents. `push` uploads changed documents already present in the local folder, not the entire remote workspace. Keep unrelated projects in separate local folders and inspect `push --dry-run` before uploading. Neither command synchronizes installed native skill folders or local render media.
25
+
26
+ `setup --project <id>` prepares only that project's immediate brand/style/output
27
+ context. With multiple projects and no remembered selection it reports
28
+ `PROJECT_SELECTION_REQUIRED`, without downloading their contents. Normal `pull`
29
+ remembers the selected project; full-space `pull --all` is an explicit advanced action.
30
+ Use `documents list --prefix videos/<id>/` and `documents get <path>` to consult
31
+ additional saved plans/reviews when needed. These reads do not materialize or sync files.
24
32
 
25
33
  ## Reading the project
26
34
 
27
35
  MCP sequence:
28
36
 
29
- 1. `workspace_info({})`, `list_documents({})`.
37
+ 1. `workspace_info({})`, then `list_documents({prefix:"videos/"})` to discover project IDs only when needed. Prefer a known project ID; use narrower brand/style/project prefixes for additional metadata. Never read every listed document by default.
30
38
  2. `read_document({path})` for discovered project, brief, brand, style and optional media manifest.
31
39
  3. `get_asset({assetId})` for production media IDs. Download the authorized asset for inspection; preserve its ID/hash, not its temporary URL.
32
40
  4. `resolve_project({projectId})` to inspect effective tokens, exceptions, source hashes and pending requirements.
@@ -105,7 +113,7 @@ editorbrain push --dir ./my-videos
105
113
  `push` is not a media upload. Use `assets upload <file>` for authorized production files within current limits and then record returned IDs/hashes. Keep large renders/evidence in the appropriate media store, with permanent identifiers in the project. Preserve old revisions and disclose unsynchronized local work if saving fails.
106
114
 
107
115
 
108
- ## Recovery and diagnostics (CLI 0.6.0)
116
+ ## Recovery and diagnostics (CLI 0.7.0)
109
117
 
110
118
  Use `editorbrain version` to discover the installed release. Run `editorbrain doctor --dir <folder> --agent codex|claude|both` before troubleshooting; `--offline` deliberately skips credentials and remote checks. Doctor is read-only. Its `mcpClient: not_verified` is not a claim that the host is connected. A customized skill is reported as a warning, not replaced automatically.
111
119
 
@@ -115,7 +123,7 @@ Errors include `code`, `hint`, `retryable` and the legacy text `error`. `AUTH_RE
115
123
 
116
124
  GET requests use bounded retries and respect Retry-After. Writes are not retried automatically: after a network error, inspect remote state before repeating them. Do not treat `retryable` as permission for an unbounded loop. HTTP error bodies are not printed. The MCP now provides matching structuredContent and text; consume either without counting them as separate results. Writing a document is declared mutating and may update existing content even though revision history is preserved.
117
125
 
118
- ## Guided setup and OAuth (0.6.0)
126
+ ## Guided setup and OAuth (0.7.0)
119
127
 
120
128
  `editorbrain setup --dir ./mis-videos --agent codex --workspace <expected-id>` authorizes the CLI when necessary, pulls documents, installs the complete skill and registers an absent MCP connection. Use `claude` or `both` for other hosts. The expected ID is a check, never authorization. Setup preserves existing MCP entries and customized guidance. Exit 2 means action is still required; inspect each step instead of repeating blindly. `--no-browser` defers MCP registration; `--skip-mcp` prepares only the CLI/skill.
121
129
 
@@ -124,3 +132,37 @@ Codex can use `codex mcp login editorbrain`; Claude Code completes OAuth from `/
124
132
  `editorbrain narrative search [query] --kind hook --channel visual` finds candidates. `narrative get <video-id> --sha256 <document-hash> --event-offset 0 --relation-offset 0 --limit 50` pages through one pinned narrative document; follow returned offsets. Missing narrative coverage stays explicit.
125
133
 
126
134
  `editorbrain library media <video-id> --sha256 <media-hash> --out <new-file>` downloads an authorized frame/audio/video and verifies its hash (90 MiB maximum). It will not overwrite an existing file. Downloading is not reviewing: inspect it with available visual/audio tools and record the intervals actually checked. Neither CLI nor MCP ingest customer references.
135
+
136
+
137
+ ## Content-first search and web preferences (0.8+)
138
+
139
+ ```sh
140
+ editorbrain workflow check --project my-video
141
+ editorbrain search edit --script ./script.txt --project my-video --mode both
142
+ editorbrain search edit --video ./analysis-proxy.mp4 --project my-video
143
+ editorbrain workflow check --project my-video --previous-version <returned-hash>
144
+ ```
145
+
146
+ MCP equivalents: `check_workflow` and `search_for_edit`. Use their current schemas.
147
+ A topic alone is insufficient. A project may supply its saved script; passing a video
148
+ uploads that selected production asset to the workspace, then sends it to the configured
149
+ analysis provider. This is customer material, not a new library reference. No automatic
150
+ scraping, FFmpeg installation, asset generation or rendering occurs.
151
+
152
+ Video inputs currently accept MP4/MOV/WebM up to 12 MiB and 24,000 analysis input tokens.
153
+ For a larger source, prepare a complete analysis proxy with the available media tools;
154
+ keep the original and record duration/coverage. If using excerpts, disclose omitted
155
+ content. Never treat coarse model timing as frame-accurate or inferred sound as verified.
156
+ Scripts accept up to 16,000 characters. Search may take longer than a literal lookup.
157
+
158
+ Results include source hashes, observed/inferred limitations, applicable evidence,
159
+ missing resources, current workflow version and per-stage elapsed time/token usage.
160
+ A null token count means the provider did not report it, not zero. Follow pinned
161
+ source paths and retrieve needed example media; do not enumerate the entire library.
162
+ If no fit exists, disclose the gap rather than fabricate a matching style. Source,
163
+ reference access or preference changes during a search invalidate that result.
164
+
165
+ Web preferences are pulled at checkpoints, not pushed into arbitrary agent chats.
166
+ `check_workflow` records a receipt; it cannot stop a tool already running. After a
167
+ version change, reconcile the next step and relevant approval with the current user
168
+ instruction. Do not repeat approval just because an unrelated preference changed.
@@ -79,3 +79,13 @@ Save reusable feedback at its stated scope: project-only changes in the project,
79
79
  Updating the global CLI does not replace skills already copied into a project. Run `editorbrain skill install --dir <workspace> --agent both` and inspect `conflicts`, `preserved`, `warnings` and `readInstructions`.
80
80
 
81
81
  If skill files differ, the installer keeps them. Prepare the new version in a separate temporary directory, compare it with the project's installed copy, and merge changes while preserving custom instructions. Do not assume a successful exit means all guidance was upgraded. Root `AGENTS.md`/`CLAUDE.md` remain user-owned: if already present, use the returned skill path explicitly, or add a narrowly scoped pointer when the user asks to integrate it. There is no automatic global installation for every future project.
82
+
83
+
84
+ ## Review preference
85
+
86
+ `videos/<id>/project.json` optionally stores `reviewMode`: `direct`, `directions`, or
87
+ `sample`. Omit it to inherit the workspace default saved in the web app. The current
88
+ explicit user instruction takes precedence. Read the effective value with
89
+ `check_workflow` / `editorbrain workflow check`; local files alone cannot reveal a
90
+ new web preference. Store received versions and scoped approvals in `direction.md`.
91
+ Brand colors/fonts and editing effects/music remain in their existing files.