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.
|
|
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.
|
|
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
|
|
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
|
|
package/bin/editorbrain.mjs
CHANGED
|
@@ -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]
|
|
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
|
|
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 &&
|
|
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
|
|
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
|
# Customer agent contract
|
|
2
2
|
|
|
3
|
-
Your responsibility is to turn
|
|
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
|
-
-
|
|
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.
|
|
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
|
|
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`
|
|
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.
|
|
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.
|
|
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.
|