editorbrain 0.5.0 → 0.7.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 +7 -8
- package/bin/{editorharness.mjs → editorbrain.mjs} +473 -151
- package/package.json +2 -3
- package/skill/{editorharness → editorbrain}/AGENTS.md +11 -1
- package/skill/{editorharness → editorbrain}/SKILL.md +3 -3
- package/skill/{editorharness → editorbrain}/references/cli-mcp.md +44 -26
- package/skill/{editorharness → editorbrain}/references/workspace-files.md +7 -7
- /package/skill/{editorharness → editorbrain}/references/production-and-review.md +0 -0
package/README.md
CHANGED
|
@@ -5,30 +5,29 @@ 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.7.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.7.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
|
-
|
|
22
|
-
|
|
23
|
-
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.
|
|
24
23
|
|
|
25
24
|
## Installation and permissions
|
|
26
25
|
|
|
27
26
|
- No install/postinstall hooks, runtime package dependencies, native binaries or background services. Installing the package does not register MCP servers or modify agent instructions.
|
|
28
27
|
- Configuration happens only when you run a command. `skill install` writes the selected project's skill files and instruction pointers; existing customizations are preserved and conflicts reported.
|
|
29
|
-
- `login` uses browser consent and stores the CLI credential in `~/.config/
|
|
28
|
+
- `login` uses browser consent and stores the CLI credential in `~/.config/editorbrain/config.json` (owner-only permissions on POSIX systems). This is a local credential file, not an OS keychain. Never share it or commit it.
|
|
30
29
|
- Remote commands contact the selected API endpoint. Use the official `https://api.editorbrain.com` endpoint. Commands such as push and media upload transfer the selected documents or assets; npm installation and help do not upload your projects.
|
|
31
|
-
- MCP
|
|
30
|
+
- MCP uses separate OAuth consent in the browser. Do not paste secrets into chats or project files.
|
|
32
31
|
- Updates are explicit. Pin a version for reproducibility; do not bypass agent permission prompts or use sudo/--force to resolve installation conflicts.
|
|
33
32
|
|
|
34
33
|
Distribution is proprietary (UNLICENSED); publication on npm does not make the private service or library open source.
|
|
@@ -1,11 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
// packages/cli/src/index.ts
|
|
4
|
-
import { chmod, lstat as lstat3, mkdir as mkdir2, open as open4 } from "node:fs/promises";
|
|
5
|
-
import { constants as constants4 } from "node:fs";
|
|
6
|
-
import { homedir } from "node:os";
|
|
7
|
-
import { basename, dirname as dirname2, extname, join as join2, resolve as resolve3 } from "node:path";
|
|
8
|
-
|
|
9
3
|
// packages/core/src/index.ts
|
|
10
4
|
var SCHEMA_VERSION = 1;
|
|
11
5
|
var MAX_DOCUMENT_BYTES = 128 * 1024;
|
|
@@ -30,9 +24,9 @@ function assertIdentifier(value) {
|
|
|
30
24
|
}
|
|
31
25
|
function safeDocumentPath(path) {
|
|
32
26
|
if (typeof path !== "string" || path.length > 400 || /[\\\x00-\x1f\x7f%:]/.test(path) || path.startsWith("/") || path.endsWith("/")) throw new Error("Unsafe document path");
|
|
33
|
-
const parts = path.split("/");
|
|
27
|
+
const parts = canonicalDocumentPath(path).split("/");
|
|
34
28
|
if (parts.some((part) => !part || part === "." || part === ".." || part.length > 100 || !/^[a-zA-Z0-9_.-]+$/.test(part) || ["__proto__", "prototype", "constructor"].includes(part) || /^(con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\.|$)/i.test(part) || /[. ]$/.test(part))) throw new Error("Unsafe or nonportable path segment");
|
|
35
|
-
if (![".editorharness", "videos"].includes(parts[0]) || parts.length < 3 || !/\.(json|md)$/.test(parts.at(-1))) throw new Error("Documents must be .md/.json beneath .
|
|
29
|
+
if (![".editorbrain", ".editorharness", "videos"].includes(parts[0]) || parts.length < 3 || !/\.(json|md)$/.test(parts.at(-1))) throw new Error("Documents must be .md/.json beneath .editorbrain/ or videos/");
|
|
36
30
|
if (parts.some((part) => /^\.env(?:\.|$)/i.test(part) || [".sync.json", ".credentials.json", "credentials.json"].includes(part.toLowerCase()))) throw new Error("Credential and local state files cannot be synced");
|
|
37
31
|
return path;
|
|
38
32
|
}
|
|
@@ -86,10 +80,10 @@ function validateDocument(path, content) {
|
|
|
86
80
|
return { valid: false, issues };
|
|
87
81
|
}
|
|
88
82
|
if (doc.schemaVersion !== SCHEMA_VERSION) add("schemaVersion must be 1");
|
|
89
|
-
const parts = path.split("/");
|
|
90
|
-
const isBrand = parts[0] === ".
|
|
91
|
-
const isStyle = parts[0] === ".
|
|
92
|
-
const isOutput = parts[0] === ".
|
|
83
|
+
const parts = canonicalDocumentPath(path).split("/");
|
|
84
|
+
const isBrand = parts[0] === ".editorbrain" && parts[1] === "brands" && parts.length === 4;
|
|
85
|
+
const isStyle = parts[0] === ".editorbrain" && parts[1] === "styles" && parts.length === 4;
|
|
86
|
+
const isOutput = parts[0] === ".editorbrain" && parts[1] === "output-profiles" && parts.length === 3;
|
|
93
87
|
const isProject = parts[0] === "videos" && parts.length === 3 && parts[2] === "project.json";
|
|
94
88
|
const isMedia = parts[0] === "videos" && parts.length === 3 && parts[2] === "media.json";
|
|
95
89
|
if (isBrand || isStyle || isProject || isOutput) {
|
|
@@ -145,23 +139,24 @@ function validateDocument(path, content) {
|
|
|
145
139
|
}
|
|
146
140
|
function projectSources(project) {
|
|
147
141
|
return [
|
|
148
|
-
`.
|
|
149
|
-
`.
|
|
150
|
-
`.
|
|
151
|
-
`.
|
|
152
|
-
`.
|
|
153
|
-
`.
|
|
154
|
-
`.
|
|
155
|
-
`.
|
|
142
|
+
`.editorbrain/brands/${project.brand}/brand.md`,
|
|
143
|
+
`.editorbrain/brands/${project.brand}/tokens.json`,
|
|
144
|
+
`.editorbrain/brands/${project.brand}/language.json`,
|
|
145
|
+
`.editorbrain/brands/${project.brand}/assets.json`,
|
|
146
|
+
`.editorbrain/styles/${project.style}/style.md`,
|
|
147
|
+
`.editorbrain/styles/${project.style}/style.json`,
|
|
148
|
+
`.editorbrain/styles/${project.style}/examples.json`,
|
|
149
|
+
`.editorbrain/output-profiles/${project.outputProfile}.json`,
|
|
156
150
|
`videos/${project.id}/project.json`,
|
|
157
151
|
`videos/${project.id}/brief.md`,
|
|
158
152
|
`videos/${project.id}/direction.md`
|
|
159
153
|
];
|
|
160
154
|
}
|
|
161
155
|
function optionalProjectSources(project) {
|
|
162
|
-
return [`.
|
|
156
|
+
return [`.editorbrain/brands/${project.brand}/design.md`, `.editorbrain/styles/${project.style}/effects.md`, `.editorbrain/styles/${project.style}/music.md`, `videos/${project.id}/media.json`];
|
|
163
157
|
}
|
|
164
158
|
function validateWorkspace(files) {
|
|
159
|
+
files = canonicalFiles(files);
|
|
165
160
|
const issues = [];
|
|
166
161
|
for (const [path, content] of Object.entries(files)) issues.push(...validateDocument(path, content).issues);
|
|
167
162
|
for (const [path, content] of Object.entries(files)) {
|
|
@@ -181,6 +176,7 @@ async function sha256(content) {
|
|
|
181
176
|
return Array.from(new Uint8Array(digest), (b) => b.toString(16).padStart(2, "0")).join("");
|
|
182
177
|
}
|
|
183
178
|
async function resolveProject(files, projectId) {
|
|
179
|
+
files = canonicalFiles(files);
|
|
184
180
|
assertIdentifier(projectId);
|
|
185
181
|
const path = `videos/${projectId}/project.json`;
|
|
186
182
|
if (!own(files, path)) throw new ValidationError([{ path, message: "Project does not exist" }]);
|
|
@@ -312,31 +308,39 @@ function createWorkspace(options = {}) {
|
|
|
312
308
|
const project = assertIdentifier(options.project ?? "first-video");
|
|
313
309
|
const json = (value) => stableJson({ schemaVersion: 1, ...value });
|
|
314
310
|
return {
|
|
315
|
-
[`.
|
|
311
|
+
[`.editorbrain/brands/${brand}/brand.md`]: `# ${options.name ?? brand}
|
|
316
312
|
|
|
317
313
|
Describe identity, audience, non-negotiables and preferred visual/audio treatment. Exact colors and fonts belong in tokens.json. Do not inherit reference colors automatically.
|
|
318
314
|
`,
|
|
319
|
-
[`.
|
|
320
|
-
[`.
|
|
321
|
-
[`.
|
|
322
|
-
[`.
|
|
323
|
-
[`.
|
|
324
|
-
[`.
|
|
325
|
-
[`.
|
|
326
|
-
[`.
|
|
327
|
-
[`.
|
|
328
|
-
".
|
|
329
|
-
".
|
|
330
|
-
".
|
|
315
|
+
[`.editorbrain/brands/${brand}/design.md`]: "# Visual direction\n\nDescribe how the brand should look in context: hierarchy, readable typography, composition, photographic or illustration treatment, and use of negative space. Exact colors and fonts stay in tokens.json; refer to semantic roles instead of duplicating values here. Adapt positions and scale to the subject, material and output format. Separate graphic brand accents from natural skin tones and photographic grading. Prefer coherence and legibility over copying a reference frame literally.\n",
|
|
316
|
+
[`.editorbrain/brands/${brand}/tokens.json`]: json({ id: brand, tokens: {} }),
|
|
317
|
+
[`.editorbrain/brands/${brand}/language.json`]: json({ id: brand, defaultLanguage: "es", glossary: [], pronunciations: [], doNotTranslate: [] }),
|
|
318
|
+
[`.editorbrain/brands/${brand}/assets.json`]: json({ id: brand, assets: [] }),
|
|
319
|
+
[`.editorbrain/styles/${style}/style.md`]: "# Adaptive editorial style\n\nBuild a coherent edit around the script, audience and available material. Use B-roll to explain this topic, with a consistent photographic direction. Timing, typography, imagery and sound should support each narrative beat. Omit a technique if it weakens clarity. SFX require listening; do not infer their existence from images.\n",
|
|
320
|
+
[`.editorbrain/styles/${style}/effects.md`]: "# Effects and motion\n\nChoose each effect for an editorial function: reveal information, direct attention, explain a relationship or support a transition. Time motion to the actual speech, action and reading time. Adapt intensity, easing, scale and placement to the scene; omit effects when a hold or cut works better. Do not require a zoom, transition, animation or SFX in every beat. Keep observed reference effects separate from proposed effects and verified implementations.\n",
|
|
321
|
+
[`.editorbrain/styles/${style}/music.md`]: "# Music and sound\n\nChoose energy and instrumentation from the story and brand. Preserve voice intelligibility, use silence when it supports a thought, and adjust the mix after listening to the actual recording. Record provenance and available licenses for every music/SFX asset; a reference recording is not an automatically reusable track. Treat unidentified reference sounds as candidates. Avoid fixed track choices, sound quotas or universal loudness settings unless the output brief requires them.\n",
|
|
322
|
+
[`.editorbrain/styles/${style}/style.json`]: json({ id: style, name: "Adaptive", tokens: {}, references: [], components: [] }),
|
|
323
|
+
[`.editorbrain/styles/${style}/examples.json`]: json({ id: style, examples: [] }),
|
|
324
|
+
".editorbrain/output-profiles/vertical.json": json({ id: "vertical", width: 1080, height: 1920, fps: 30, captions: { enabled: true, language: "es" } }),
|
|
325
|
+
".editorbrain/library/index.json": json({ references: [], note: "Fetch references and examples on demand. Private media uses authenticated access." }),
|
|
326
|
+
".editorbrain/memory/preferences.json": json({ confirmed: [], note: "Only explicitly confirmed feedback becomes a reusable preference. Keep scope and evidence." }),
|
|
331
327
|
[`videos/${project}/project.json`]: json({ id: project, brand, style, outputProfile: "vertical", intent: { topic: options.topic ?? "Define the subject of this video", goal: options.goal ?? "Explain the subject clearly" }, script: "", overrides: {} }),
|
|
332
328
|
[`videos/${project}/brief.md`]: "# Brief\n\nDescribe the objective, audience, script or source video, constraints and available assets.\n",
|
|
333
|
-
[`videos/${project}/direction.md`]: "# Direction\n\nDIRECTION_PENDING\n\nRead the installed
|
|
329
|
+
[`videos/${project}/direction.md`]: "# Direction\n\nDIRECTION_PENDING\n\nRead the installed EditorBrain skill and its AGENTS.md. Inspect the actual script, media, brand and selected evidence. State the intended transfer: faithful preset, editorial family or inspiration. Connect each beat to its meaning, resource/action, composition, motion, real timing and verification. Distinguish observed evidence, inference and new design. Define layer treatments, justify source reuse, and review movement and SFX in the final render. Preserve reference hashes and project-scoped feedback. A plan is not an executed or verified edit.\n"
|
|
334
330
|
};
|
|
335
331
|
}
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
332
|
+
function canonicalDocumentPath(path) {
|
|
333
|
+
return path.replace(/^\.editorharness\//, ".editorbrain/");
|
|
334
|
+
}
|
|
335
|
+
function canonicalFiles(files) {
|
|
336
|
+
const result = {};
|
|
337
|
+
for (const [path, content] of Object.entries(files)) {
|
|
338
|
+
const canonical = canonicalDocumentPath(path);
|
|
339
|
+
if (Object.prototype.hasOwnProperty.call(result, canonical) && result[canonical] !== content) throw new Error("Conflicting workspace layouts. Keep both copies and resolve their differences before continuing.");
|
|
340
|
+
result[canonical] = content;
|
|
341
|
+
}
|
|
342
|
+
return result;
|
|
343
|
+
}
|
|
340
344
|
|
|
341
345
|
// packages/cli/src/io.ts
|
|
342
346
|
import { constants } from "node:fs";
|
|
@@ -432,8 +436,9 @@ async function readWorkspace(root) {
|
|
|
432
436
|
}
|
|
433
437
|
}
|
|
434
438
|
await walk(".editorharness");
|
|
439
|
+
await walk(".editorbrain");
|
|
435
440
|
await walk("videos");
|
|
436
|
-
return files;
|
|
441
|
+
return canonicalFiles(files);
|
|
437
442
|
}
|
|
438
443
|
|
|
439
444
|
// packages/cli/src/client.ts
|
|
@@ -566,8 +571,8 @@ var Client = class {
|
|
|
566
571
|
async me() {
|
|
567
572
|
return this.request("/v1/me");
|
|
568
573
|
}
|
|
569
|
-
async list() {
|
|
570
|
-
const data = await this.request("/v1/documents");
|
|
574
|
+
async list(prefix = "") {
|
|
575
|
+
const data = await this.request("/v1/documents" + (prefix ? "?" + new URLSearchParams({ prefix }) : ""));
|
|
571
576
|
return data.documents;
|
|
572
577
|
}
|
|
573
578
|
async get(path) {
|
|
@@ -577,7 +582,7 @@ var Client = class {
|
|
|
577
582
|
return this.request(`/v1/documents/${safeDocumentPath(path).split("/").map(encodeURIComponent).join("/")}`, { method: "PUT", body: { content, expectedRevision } });
|
|
578
583
|
}
|
|
579
584
|
async media(path, options = {}) {
|
|
580
|
-
if (!/^\/v1\/assets\/[a-f0-9]{64}(?:\?|$)/.test(path)) throw new Error("Invalid asset endpoint");
|
|
585
|
+
if (!/^\/v1\/assets\/[a-f0-9]{64}(?:\?|$)/.test(path) && !(!options.content && /^\/v1\/library\/[^/?#]+\/media\/[a-f0-9]{64}$/.test(path))) throw new Error("Invalid asset endpoint");
|
|
581
586
|
const headers = { Authorization: `Bearer ${this.config.apiKey}` };
|
|
582
587
|
if (options.content) {
|
|
583
588
|
headers["Content-Type"] = options.mimeType ?? "application/octet-stream";
|
|
@@ -590,21 +595,32 @@ var Client = class {
|
|
|
590
595
|
};
|
|
591
596
|
async function loadSyncState(root, endpoint, workspaceId) {
|
|
592
597
|
try {
|
|
593
|
-
const
|
|
598
|
+
const raw = await readText(root, ".editorbrain/.sync.json", false).catch((error) => {
|
|
599
|
+
if (error.code !== "ENOENT") throw error;
|
|
600
|
+
return readText(root, ".editorharness/.sync.json", false);
|
|
601
|
+
});
|
|
602
|
+
const state = JSON.parse(raw);
|
|
603
|
+
if (state.projectId !== void 0) assertIdentifier(state.projectId);
|
|
594
604
|
if (state.schemaVersion !== 1 || typeof state.documents !== "object") throw new Error("Invalid local sync state");
|
|
595
605
|
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.");
|
|
596
606
|
for (const [path, item] of Object.entries(state.documents)) {
|
|
597
607
|
safeDocumentPath(path);
|
|
598
608
|
if (!Number.isInteger(item.revision) || item.revision < 1 || !/^[a-f0-9]{64}$/.test(item.sha256)) throw new Error("Invalid local revision");
|
|
599
609
|
}
|
|
600
|
-
|
|
610
|
+
const documents = {};
|
|
611
|
+
for (const [path, item] of Object.entries(state.documents)) {
|
|
612
|
+
const canonical = canonicalDocumentPath(path);
|
|
613
|
+
if (documents[canonical] && (documents[canonical].revision !== item.revision || documents[canonical].sha256 !== item.sha256)) throw new Error("Conflicting sync state aliases");
|
|
614
|
+
documents[canonical] = item;
|
|
615
|
+
}
|
|
616
|
+
return { ...state, documents };
|
|
601
617
|
} catch (error) {
|
|
602
618
|
if (error.code !== "ENOENT") throw error;
|
|
603
619
|
return { schemaVersion: 1, endpoint, workspaceId, documents: {} };
|
|
604
620
|
}
|
|
605
621
|
}
|
|
606
622
|
async function saveSyncState(root, state) {
|
|
607
|
-
await atomicWrite(root, ".
|
|
623
|
+
await atomicWrite(root, ".editorbrain/.sync.json", stableJson(state), { document: false, mode: 384 });
|
|
608
624
|
}
|
|
609
625
|
function checkRemote(summary) {
|
|
610
626
|
safeDocumentPath(summary.path);
|
|
@@ -660,7 +676,7 @@ ${conflicts.join("\n")}`, "Run push --dry-run and reconcile the reported paths."
|
|
|
660
676
|
async function pullWorkspace(client, root, local, options = {}) {
|
|
661
677
|
const me = await client.me();
|
|
662
678
|
const state = await loadSyncState(root, client.url, me.workspaceId);
|
|
663
|
-
const summaries = await client.list();
|
|
679
|
+
const summaries = options.summaries ?? await client.list();
|
|
664
680
|
const pending = [];
|
|
665
681
|
const conflicts = [];
|
|
666
682
|
let unchanged = 0;
|
|
@@ -685,12 +701,13 @@ async function pullWorkspace(client, root, local, options = {}) {
|
|
|
685
701
|
${conflicts.join("\n")}`, "Run pull --dry-run and reconcile the reported paths.", false, { conflicts });
|
|
686
702
|
const documents = [];
|
|
687
703
|
for (const summary of pending) {
|
|
688
|
-
const doc = await client.get(summary.path);
|
|
704
|
+
const doc = options.prefetched?.get(summary.path) ?? await client.get(summary.path);
|
|
689
705
|
checkRemote(doc);
|
|
690
706
|
const validation = validateDocument(doc.path, doc.content);
|
|
691
707
|
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.`);
|
|
692
708
|
documents.push(doc);
|
|
693
709
|
}
|
|
710
|
+
await options.beforeWrite?.();
|
|
694
711
|
const downloaded = [];
|
|
695
712
|
for (const doc of documents) {
|
|
696
713
|
let current;
|
|
@@ -709,20 +726,67 @@ ${conflicts.join("\n")}`, "Run pull --dry-run and reconcile the reported paths."
|
|
|
709
726
|
return { downloaded, unchanged };
|
|
710
727
|
}
|
|
711
728
|
|
|
729
|
+
// packages/cli/src/project-selection.ts
|
|
730
|
+
async function listProjects(client) {
|
|
731
|
+
const documents = await client.list("videos/");
|
|
732
|
+
return documents.filter((row) => /^videos\/[^/]+\/project\.json$/.test(row.path)).map((row) => ({ id: assertIdentifier(row.path.split("/")[1]), revision: row.revision, updatedAt: row.updatedAt }));
|
|
733
|
+
}
|
|
734
|
+
async function pullProject(client, root, local, options = {}) {
|
|
735
|
+
const me = await client.me(), state = await loadSyncState(root, client.url, me.workspaceId);
|
|
736
|
+
let projectId = options.projectId ?? state.projectId;
|
|
737
|
+
if (!projectId) {
|
|
738
|
+
const localIds = Object.keys(local).filter((path) => /^videos\/[^/]+\/project\.json$/.test(path)).map((path) => path.split("/")[1]);
|
|
739
|
+
if (localIds.length === 1) projectId = localIds[0];
|
|
740
|
+
}
|
|
741
|
+
if (!projectId) {
|
|
742
|
+
const projects = await listProjects(client);
|
|
743
|
+
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 });
|
|
744
|
+
projectId = projects[0].id;
|
|
745
|
+
}
|
|
746
|
+
assertIdentifier(projectId);
|
|
747
|
+
const projectPath = `videos/${projectId}/project.json`, projectDoc = await client.get(projectPath);
|
|
748
|
+
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.");
|
|
749
|
+
const project = JSON.parse(projectDoc.content);
|
|
750
|
+
if (project.id !== projectId) throw new CliError("INVALID_PROJECT", "Project ID does not match its path.", "Correct the project in the app.");
|
|
751
|
+
const wanted = /* @__PURE__ */ new Set([...projectSources(project), ...optionalProjectSources(project)]);
|
|
752
|
+
const prefixes = [`.editorbrain/brands/${project.brand}/`, `.editorbrain/styles/${project.style}/`, `.editorbrain/output-profiles/${project.outputProfile}.json`, `videos/${projectId}/`];
|
|
753
|
+
const documents = [];
|
|
754
|
+
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);
|
|
755
|
+
const initial = documents.find((row) => row.path === projectPath);
|
|
756
|
+
if (!initial || initial.revision !== projectDoc.revision || initial.sha256 !== projectDoc.sha256) throw new CliError("SYNC_CONFLICT", "Project selection changed during preparation.", "Retry the current project.");
|
|
757
|
+
const missing = projectSources(project).filter((path) => !documents.some((row) => row.path === path));
|
|
758
|
+
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 });
|
|
759
|
+
const result = await pullWorkspace(client, root, local, { dryRun: options.dryRun, summaries: documents, prefetched: /* @__PURE__ */ new Map([[projectPath, projectDoc]]), beforeWrite: async () => {
|
|
760
|
+
const fresh = await client.get(projectPath);
|
|
761
|
+
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.");
|
|
762
|
+
} });
|
|
763
|
+
if (!options.dryRun) {
|
|
764
|
+
const current = await loadSyncState(root, client.url, me.workspaceId);
|
|
765
|
+
await saveSyncState(root, { ...current, projectId });
|
|
766
|
+
}
|
|
767
|
+
return { ...result, projectId, scope: "project_context", libraryDownloaded: false };
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
// packages/cli/src/setup.ts
|
|
771
|
+
import { spawn } from "node:child_process";
|
|
772
|
+
import { resolve as resolve3 } from "node:path";
|
|
773
|
+
|
|
712
774
|
// packages/cli/src/agent-guidance.ts
|
|
775
|
+
import { createHash } from "node:crypto";
|
|
713
776
|
import { constants as constants2 } from "node:fs";
|
|
714
|
-
import { lstat as lstat2, open as open2, readdir as readdir2 } from "node:fs/promises";
|
|
777
|
+
import { lstat as lstat2, open as open2, readdir as readdir2, rename as rename2 } from "node:fs/promises";
|
|
715
778
|
import { fileURLToPath } from "node:url";
|
|
779
|
+
var previousSkillHashes = { "SKILL.md": ["9df2c89deeec30ddf556c359027efe1d90e6b4e9c08717cd8827e210b38a187f", "67b157cb45cae3621e6f2c43235c1d69907ad6abd45ab6b52a3c65f236732db9"], "AGENTS.md": ["4f3ef860ade3a31aa7c6a6aab5cf6ec2a700850bf19ea2320324c12c2e0fbf5b"], "references/cli-mcp.md": ["5d3a79343fbd76cdae5bdbe12fb9c944d5dff4f489ac6534fd96e0cb8630b988", "8e011c31368cef2117a28ee972b39aab9535eeb0f601a4a0f9d6fc019b22c1c2"], "references/production-and-review.md": ["055c6df9e0f167be148b0a5c0bba5b43e108eb1f58f478d88b3536c6589ac7b3"], "references/workspace-files.md": ["e72af2871f42cd776be1bfff8009ed5562780e7f8a65b12c27887f3880631f81", "00780c64bd0244c6afb18d786b7512b43e315c376e8f88c67d6ba53d91275ccf"] };
|
|
716
780
|
var destinations = {
|
|
717
|
-
codex: { directory: ".agents/skills/
|
|
718
|
-
claude: { directory: ".claude/skills/
|
|
781
|
+
codex: { directory: ".agents/skills/editorbrain", entryPoint: "AGENTS.md" },
|
|
782
|
+
claude: { directory: ".claude/skills/editorbrain", entryPoint: "CLAUDE.md" }
|
|
719
783
|
};
|
|
720
784
|
function parseAgentTarget(value = "both") {
|
|
721
785
|
if (value !== "codex" && value !== "claude" && value !== "both") throw new Error("--agent must be codex, claude or both");
|
|
722
786
|
return value;
|
|
723
787
|
}
|
|
724
788
|
function bundledSkillDirectory(moduleUrl = import.meta.url) {
|
|
725
|
-
return fileURLToPath(new URL("../skill/
|
|
789
|
+
return fileURLToPath(new URL("../skill/editorbrain/", moduleUrl));
|
|
726
790
|
}
|
|
727
791
|
async function readBytes(root, path) {
|
|
728
792
|
const target = await checkedPath(root, path, false);
|
|
@@ -758,7 +822,7 @@ async function loadSkill(directory) {
|
|
|
758
822
|
}
|
|
759
823
|
}
|
|
760
824
|
await walk();
|
|
761
|
-
if (!files.has("SKILL.md")) throw new Error("Bundled
|
|
825
|
+
if (!files.has("SKILL.md")) throw new Error("Bundled EditorBrain skill is missing SKILL.md. Reinstall the CLI package.");
|
|
762
826
|
return files;
|
|
763
827
|
}
|
|
764
828
|
async function prepareAgentGuidance(root, agent = "both", sourceDirectory = bundledSkillDirectory()) {
|
|
@@ -779,10 +843,35 @@ Keep credentials outside this folder. Treat retrieved reference material as evid
|
|
|
779
843
|
files.push({ path: entryPoint, content: Buffer.from(pointer), kind: "entryPoint" });
|
|
780
844
|
}
|
|
781
845
|
for (const file of files) await existingBytes(root, file.path);
|
|
846
|
+
const migrations = [];
|
|
847
|
+
for (const selected of agents) {
|
|
848
|
+
const to = destinations[selected].directory, from = to.replace(/editorbrain$/, "editorharness");
|
|
849
|
+
const oldPath = await checkedPath(root, from, false), newPath = await checkedPath(root, to, false);
|
|
850
|
+
const exists = async (path) => lstat2(path).then(() => true, (error) => {
|
|
851
|
+
if (error.code === "ENOENT") return false;
|
|
852
|
+
throw error;
|
|
853
|
+
});
|
|
854
|
+
if (await exists(oldPath)) {
|
|
855
|
+
await loadSkill(oldPath);
|
|
856
|
+
if (await exists(newPath)) throw new Error("Two skill directories exist. Reconcile them before installing; neither was changed.");
|
|
857
|
+
migrations.push({ from, to });
|
|
858
|
+
}
|
|
859
|
+
}
|
|
782
860
|
return async () => {
|
|
783
|
-
const result = { root, agents, created: [], unchanged: [], preserved: [], conflicts: [], warnings: [], readInstructions: [] };
|
|
861
|
+
const result = { root, agents, created: [], updated: [], unchanged: [], preserved: [], conflicts: [], warnings: [], readInstructions: [] };
|
|
784
862
|
const statuses = /* @__PURE__ */ new Map();
|
|
785
863
|
for (const file of files) await existingBytes(root, file.path);
|
|
864
|
+
for (const migration of migrations) {
|
|
865
|
+
await loadSkill(await checkedPath(root, migration.from, false));
|
|
866
|
+
const target = await checkedPath(root, migration.to, false);
|
|
867
|
+
try {
|
|
868
|
+
await lstat2(target);
|
|
869
|
+
throw new Error("Skill destination appeared during migration; no overwrite allowed.");
|
|
870
|
+
} catch (error) {
|
|
871
|
+
if (error.code !== "ENOENT") throw error;
|
|
872
|
+
}
|
|
873
|
+
await rename2(await checkedPath(root, migration.from, false), target);
|
|
874
|
+
}
|
|
786
875
|
for (const file of files) {
|
|
787
876
|
let status;
|
|
788
877
|
try {
|
|
@@ -791,7 +880,13 @@ Keep credentials outside this folder. Treat retrieved reference material as evid
|
|
|
791
880
|
} catch (error) {
|
|
792
881
|
if (error.code !== "EEXIST") throw error;
|
|
793
882
|
const existing = await readBytes(root, file.path);
|
|
794
|
-
|
|
883
|
+
const relative2 = file.path.split("/skills/editorbrain/")[1];
|
|
884
|
+
const pristine = relative2 && previousSkillHashes[relative2]?.includes(createHash("sha256").update(existing).digest("hex"));
|
|
885
|
+
const oldPointer = Buffer.from(file.content.toString().replaceAll("/skills/editorbrain/", "/skills/editorharness/"));
|
|
886
|
+
if (!existing.equals(file.content) && (pristine || file.kind === "entryPoint" && existing.equals(oldPointer))) {
|
|
887
|
+
await atomicWrite(root, file.path, file.content, { document: false });
|
|
888
|
+
status = "updated";
|
|
889
|
+
} else status = existing.equals(file.content) ? "unchanged" : "preserved";
|
|
795
890
|
if (status === "preserved" && file.kind === "skill") result.conflicts.push(file.path);
|
|
796
891
|
}
|
|
797
892
|
result[status].push(file.path);
|
|
@@ -816,32 +911,57 @@ async function installAgentGuidance(directory, agent = "both") {
|
|
|
816
911
|
|
|
817
912
|
// packages/cli/src/workspace-lock.ts
|
|
818
913
|
import { constants as constants3 } from "node:fs";
|
|
819
|
-
import { open as open3, unlink as unlink2 } from "node:fs/promises";
|
|
914
|
+
import { lstat as lstat3, open as open3, rename as rename3, unlink as unlink2 } from "node:fs/promises";
|
|
820
915
|
import { hostname } from "node:os";
|
|
821
916
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
822
|
-
var LOCK_PATH = ".
|
|
917
|
+
var LOCK_PATH = ".editorbrain-operation.lock";
|
|
823
918
|
async function withWorkspaceLock(root, operation) {
|
|
824
|
-
const
|
|
825
|
-
let
|
|
919
|
+
const legacyLock = await checkedPath(root, ".editorharness-operation.lock", false);
|
|
920
|
+
let legacy;
|
|
826
921
|
try {
|
|
827
|
-
|
|
922
|
+
legacy = await open3(legacyLock, constants3.O_CREAT | constants3.O_EXCL | constants3.O_WRONLY | constants3.O_NOFOLLOW, 384);
|
|
828
923
|
} catch (error) {
|
|
829
|
-
if (error.code
|
|
830
|
-
throw
|
|
924
|
+
if (error.code === "EEXIST") throw new CliError("WORKSPACE_BUSY", "Another CLI operation holds this workspace lock.", "Wait for the other operation to finish.");
|
|
925
|
+
throw error;
|
|
831
926
|
}
|
|
832
|
-
const owner = JSON.stringify({ pid: process.pid, host: hostname(), nonce: randomUUID2(), startedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
833
927
|
try {
|
|
834
|
-
await
|
|
835
|
-
|
|
836
|
-
|
|
928
|
+
const path = await checkedPath(root, LOCK_PATH, false);
|
|
929
|
+
let handle;
|
|
930
|
+
try {
|
|
931
|
+
handle = await open3(path, constants3.O_CREAT | constants3.O_EXCL | constants3.O_WRONLY | constants3.O_NOFOLLOW, 384);
|
|
932
|
+
} catch (error) {
|
|
933
|
+
if (error.code !== "EEXIST") throw error;
|
|
934
|
+
throw new CliError("WORKSPACE_BUSY", "Another CLI operation holds this workspace lock.", "Wait for the other operation. If it crashed, verify no process is using the folder before removing .editorbrain-operation.lock. Locks are never removed automatically.");
|
|
935
|
+
}
|
|
936
|
+
const owner = JSON.stringify({ pid: process.pid, host: hostname(), nonce: randomUUID2(), startedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
937
|
+
try {
|
|
938
|
+
await handle.writeFile(owner);
|
|
939
|
+
await handle.close();
|
|
940
|
+
const oldRoot = await checkedPath(root, ".editorharness", false);
|
|
941
|
+
const newRoot = await checkedPath(root, ".editorbrain", false);
|
|
942
|
+
const exists = async (path2) => lstat3(path2).then(() => true, (error) => {
|
|
943
|
+
if (error.code === "ENOENT") return false;
|
|
944
|
+
throw error;
|
|
945
|
+
});
|
|
946
|
+
if (await exists(oldRoot)) {
|
|
947
|
+
if (await exists(newRoot)) throw new CliError("WORKSPACE_PATH_CONFLICT", "This folder contains two versions of the workspace directory.", "Reconcile the directories before continuing; neither was changed.");
|
|
948
|
+
await rename3(oldRoot, newRoot);
|
|
949
|
+
}
|
|
950
|
+
return await operation();
|
|
951
|
+
} finally {
|
|
952
|
+
await handle.close().catch(() => {
|
|
953
|
+
});
|
|
954
|
+
if (await readText(root, LOCK_PATH, false).catch(() => void 0) === owner) await unlink2(await checkedPath(root, LOCK_PATH, false));
|
|
955
|
+
}
|
|
837
956
|
} finally {
|
|
838
|
-
await
|
|
839
|
-
|
|
840
|
-
if (await readText(root, LOCK_PATH, false).catch(() => void 0) === owner) await unlink2(await checkedPath(root, LOCK_PATH, false));
|
|
957
|
+
await legacy.close();
|
|
958
|
+
await unlink2(legacyLock);
|
|
841
959
|
}
|
|
842
960
|
}
|
|
843
961
|
|
|
844
962
|
// packages/cli/src/doctor.ts
|
|
963
|
+
import { readFile } from "node:fs/promises";
|
|
964
|
+
import { resolve as resolve2 } from "node:path";
|
|
845
965
|
async function cliVersion() {
|
|
846
966
|
const metadata = JSON.parse(await readFile(new URL("../package.json", import.meta.url), "utf8"));
|
|
847
967
|
return metadata.version;
|
|
@@ -871,7 +991,7 @@ async function diagnose(options, fetcher = fetch) {
|
|
|
871
991
|
else throw error;
|
|
872
992
|
}
|
|
873
993
|
for (const agent of options.agent === "both" ? ["codex", "claude"] : [options.agent]) {
|
|
874
|
-
const directory = agent === "codex" ? ".agents/skills/
|
|
994
|
+
const directory = agent === "codex" ? ".agents/skills/editorbrain" : ".claude/skills/editorbrain";
|
|
875
995
|
let different = false;
|
|
876
996
|
try {
|
|
877
997
|
for (const file of guidanceFiles) {
|
|
@@ -909,8 +1029,120 @@ async function diagnose(options, fetcher = fetch) {
|
|
|
909
1029
|
return { schemaVersion: 1, cliVersion: await cliVersion(), ok: !checks.some((check) => check.status === "fail"), directory: root ?? resolve2(options.directory), workspaceId, checks, mcpClient: "not_verified", renderVerified: false };
|
|
910
1030
|
}
|
|
911
1031
|
|
|
1032
|
+
// packages/cli/src/setup.ts
|
|
1033
|
+
var runHost = (command, args, cwd, interactive = false) => new Promise((done, reject) => {
|
|
1034
|
+
const child = spawn(command, args, { cwd, shell: false, stdio: ["ignore", interactive ? 2 : "pipe", interactive ? 2 : "pipe"], windowsHide: !interactive });
|
|
1035
|
+
let stdout = "", stderr = "", size = 0;
|
|
1036
|
+
const collect = (kind, data) => {
|
|
1037
|
+
size += data.length;
|
|
1038
|
+
if (size > 1024 * 1024) {
|
|
1039
|
+
child.kill();
|
|
1040
|
+
reject(new CliError("HOST_OUTPUT_LIMIT", "Agent CLI returned too much output.", "Configure MCP using the documented manual command."));
|
|
1041
|
+
return;
|
|
1042
|
+
}
|
|
1043
|
+
if (kind === "stdout") stdout += data;
|
|
1044
|
+
else stderr += data;
|
|
1045
|
+
};
|
|
1046
|
+
child.stdout?.on("data", (chunk) => collect("stdout", chunk));
|
|
1047
|
+
child.stderr?.on("data", (chunk) => collect("stderr", chunk));
|
|
1048
|
+
const timer = setTimeout(() => {
|
|
1049
|
+
child.kill();
|
|
1050
|
+
reject(new CliError("HOST_TIMEOUT", "Agent configuration did not finish.", "Check the agent configuration before running setup again."));
|
|
1051
|
+
}, interactive ? 18e4 : 15e3);
|
|
1052
|
+
child.once("error", (error) => {
|
|
1053
|
+
clearTimeout(timer);
|
|
1054
|
+
reject(error);
|
|
1055
|
+
});
|
|
1056
|
+
child.once("close", (code) => {
|
|
1057
|
+
clearTimeout(timer);
|
|
1058
|
+
done({ code, stdout, stderr });
|
|
1059
|
+
});
|
|
1060
|
+
});
|
|
1061
|
+
async function configureMcp(agent, directory, url, noBrowser, runner = runHost) {
|
|
1062
|
+
const endpoint = url + "/mcp";
|
|
1063
|
+
const args = agent === "codex" ? ["mcp", "add", "editorbrain", "--url", endpoint] : ["mcp", "add", "--transport", "http", "--scope", "project", "editorbrain", endpoint];
|
|
1064
|
+
const next = agent === "codex" ? "Run codex mcp login editorbrain, then verify workspace_info inside Codex." : "Open Claude Code in this folder, approve the project MCP and use /mcp to authorize. Then verify workspace_info.";
|
|
1065
|
+
if (noBrowser) return { agent, status: "action_required", command: [agent, ...args], next, hostVerified: false };
|
|
1066
|
+
const version = await runner(agent, ["--version"], directory);
|
|
1067
|
+
if (version.code !== 0) throw new CliError("AGENT_UNAVAILABLE", `${agent} CLI is unavailable.`, "Install the official agent CLI, or use setup --skip-mcp.");
|
|
1068
|
+
const inspect = await runner(agent, agent === "codex" ? ["mcp", "list", "--json"] : ["mcp", "get", "editorbrain"], directory);
|
|
1069
|
+
let exists = false;
|
|
1070
|
+
if (agent === "codex") {
|
|
1071
|
+
if (inspect.code !== 0) throw new CliError("MCP_INSPECTION_FAILED", "Could not inspect Codex MCP configuration.", "Inspect it manually; setup did not overwrite it.");
|
|
1072
|
+
let list;
|
|
1073
|
+
try {
|
|
1074
|
+
list = JSON.parse(inspect.stdout);
|
|
1075
|
+
} catch {
|
|
1076
|
+
throw new CliError("MCP_INSPECTION_FAILED", "Codex returned an unsupported configuration format.", "Inspect MCP configuration manually.");
|
|
1077
|
+
}
|
|
1078
|
+
if (!Array.isArray(list)) throw new CliError("MCP_INSPECTION_FAILED", "Unsupported MCP configuration response.", "Inspect MCP configuration manually.");
|
|
1079
|
+
exists = list.some((item) => item?.name === "editorbrain");
|
|
1080
|
+
} else {
|
|
1081
|
+
exists = inspect.code === 0;
|
|
1082
|
+
if (!exists && !/^No MCP server named "editorbrain"[.\s]/.test(inspect.stderr + inspect.stdout)) throw new CliError("MCP_INSPECTION_FAILED", "Could not inspect Claude MCP configuration.", "Inspect it manually; setup did not overwrite it.");
|
|
1083
|
+
}
|
|
1084
|
+
if (exists) return { agent, status: "preserved", next: "An existing editorbrain server was preserved. Check its endpoint and authentication, then " + next, hostVerified: false };
|
|
1085
|
+
const added = await runner(agent, args, directory, true);
|
|
1086
|
+
return { agent, status: added.code === 0 ? "registered" : "action_required", next, hostVerified: false };
|
|
1087
|
+
}
|
|
1088
|
+
async function setup(options, runner = runHost) {
|
|
1089
|
+
const steps = {};
|
|
1090
|
+
let selected = await options.getConfig();
|
|
1091
|
+
let client = new Client(selected);
|
|
1092
|
+
let me;
|
|
1093
|
+
try {
|
|
1094
|
+
if (!selected.apiKey) throw new CliError("AUTH_REQUIRED", "Login required.", "Run login.");
|
|
1095
|
+
me = await client.me();
|
|
1096
|
+
} catch (error) {
|
|
1097
|
+
if (!(error instanceof CliError) || error.code !== "AUTH_REQUIRED") throw error;
|
|
1098
|
+
await options.login();
|
|
1099
|
+
selected = await options.getConfig();
|
|
1100
|
+
client = new Client(selected);
|
|
1101
|
+
me = await client.me();
|
|
1102
|
+
}
|
|
1103
|
+
if (options.workspaceId && me.workspaceId !== options.workspaceId) throw new CliError("WORKSPACE_MISMATCH", "The authorized account selected a different workspace.", "Run login for the intended workspace. No project files were changed.");
|
|
1104
|
+
steps.account = { status: "verified", workspaceId: me.workspaceId, endpoint: client.url };
|
|
1105
|
+
const root = await ensureRoot(resolve3(options.directory), true);
|
|
1106
|
+
let folderReady = false, skillReady = false;
|
|
1107
|
+
await withWorkspaceLock(root, async () => {
|
|
1108
|
+
try {
|
|
1109
|
+
steps.documents = { status: "ready", ...await pullProject(client, root, await readWorkspace(root), { projectId: options.projectId }) };
|
|
1110
|
+
folderReady = true;
|
|
1111
|
+
} catch (error) {
|
|
1112
|
+
steps.documents = { status: error instanceof CliError && error.code === "PROJECT_SELECTION_REQUIRED" ? "selection_required" : "failed", ...errorOutput(error) };
|
|
1113
|
+
}
|
|
1114
|
+
try {
|
|
1115
|
+
const result = await installAgentGuidance(root, options.agent);
|
|
1116
|
+
steps.skill = { status: result.conflicts.length ? "review_required" : "ready", ...result };
|
|
1117
|
+
skillReady = !result.conflicts.length;
|
|
1118
|
+
} catch (error) {
|
|
1119
|
+
steps.skill = { status: "failed", ...errorOutput(error) };
|
|
1120
|
+
}
|
|
1121
|
+
});
|
|
1122
|
+
const hosts = [];
|
|
1123
|
+
if (!options.skipMcp && skillReady) {
|
|
1124
|
+
for (const agent of options.agent === "both" ? ["codex", "claude"] : [options.agent]) {
|
|
1125
|
+
try {
|
|
1126
|
+
hosts.push(await configureMcp(agent, root, client.url, !!options.noBrowser, runner));
|
|
1127
|
+
} catch (error) {
|
|
1128
|
+
hosts.push({ agent, status: "action_required", ...errorOutput(error), hostVerified: false });
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
steps.mcp = { status: options.skipMcp ? "skipped" : "verification_required", hosts, expectedWorkspaceId: me.workspaceId, next: "Inside your agent, call workspace_info and confirm this workspace ID. Configuration alone does not verify a connection." };
|
|
1133
|
+
const diagnosis = await diagnose({ directory: root, agent: options.agent, config: selected });
|
|
1134
|
+
steps.diagnostics = diagnosis;
|
|
1135
|
+
return { schemaVersion: 1, directory: root, workspaceId: me.workspaceId, cliReady: folderReady && skillReady && diagnosis.ok, status: folderReady && skillReady && diagnosis.ok && options.skipMcp ? "ready" : "action_required", steps, renderVerified: false };
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
// packages/cli/src/index.ts
|
|
1139
|
+
import { chmod, lstat as lstat4, mkdir as mkdir2, open as open4 } from "node:fs/promises";
|
|
1140
|
+
import { constants as constants4 } from "node:fs";
|
|
1141
|
+
import { homedir } from "node:os";
|
|
1142
|
+
import { basename, dirname as dirname2, extname, join as join2, resolve as resolve4 } from "node:path";
|
|
1143
|
+
|
|
912
1144
|
// packages/cli/src/device-auth.ts
|
|
913
|
-
import { spawn } from "node:child_process";
|
|
1145
|
+
import { spawn as spawn2 } from "node:child_process";
|
|
914
1146
|
import { setTimeout as delay2 } from "node:timers/promises";
|
|
915
1147
|
var DEFAULT_API_URL = "https://api.editorbrain.com";
|
|
916
1148
|
var DeviceLoginError = class extends Error {
|
|
@@ -928,12 +1160,12 @@ function browserCommand(url, platform = process.platform) {
|
|
|
928
1160
|
}
|
|
929
1161
|
async function openSystemBrowser(url) {
|
|
930
1162
|
const { command, args } = browserCommand(url);
|
|
931
|
-
await new Promise((
|
|
932
|
-
const child =
|
|
1163
|
+
await new Promise((resolve5, reject) => {
|
|
1164
|
+
const child = spawn2(command, args, { shell: false, stdio: "ignore", detached: true, windowsHide: true });
|
|
933
1165
|
child.once("error", reject);
|
|
934
1166
|
child.once("spawn", () => {
|
|
935
1167
|
child.unref();
|
|
936
|
-
|
|
1168
|
+
resolve5();
|
|
937
1169
|
});
|
|
938
1170
|
});
|
|
939
1171
|
}
|
|
@@ -1113,30 +1345,38 @@ async function prepareDirectionContext(root, client, projectId) {
|
|
|
1113
1345
|
var HELP = `EditorBrain \u2014 portable editorial context for AI video agents
|
|
1114
1346
|
|
|
1115
1347
|
editorbrain version
|
|
1348
|
+
editorbrain setup --dir folder --agent codex|claude|both [--project id] [--workspace id] [--no-browser] [--skip-mcp]
|
|
1116
1349
|
editorbrain doctor [--dir folder] [--agent codex|claude|both] [--offline]
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1350
|
+
editorbrain init [--dir folder] [--brand id] [--style id] [--project id] [--topic text] [--goal text] [--agent codex|claude|both]
|
|
1351
|
+
editorbrain skill install [--dir folder] [--agent codex|claude|both]
|
|
1352
|
+
editorbrain validate [--dir folder]
|
|
1353
|
+
editorbrain resolve <project-id> [--dir folder]
|
|
1354
|
+
editorbrain export <project-id> --adapter neutral|hyperframes|remotion [--out folder] [--dir folder]
|
|
1355
|
+
editorbrain login [--url https://server] [--no-browser] [--timeout seconds]
|
|
1356
|
+
editorbrain login [--url https://server] --key-stdin
|
|
1357
|
+
editorbrain register --url https://server --name workspace --invite-stdin
|
|
1358
|
+
editorbrain status
|
|
1359
|
+
editorbrain pull [--project id | --all] [--dir folder] [--dry-run]
|
|
1360
|
+
editorbrain push [--dir folder] [--dry-run]
|
|
1361
|
+
editorbrain projects list
|
|
1362
|
+
editorbrain documents list [--prefix path]
|
|
1363
|
+
editorbrain documents get <path>
|
|
1364
|
+
editorbrain library search [query]
|
|
1365
|
+
editorbrain library get <video-id> [--document relative-source-path] [--sha256 source-hash]
|
|
1366
|
+
editorbrain library media <video-id> --sha256 media-hash --out file
|
|
1367
|
+
editorbrain narrative search [query] [--kind hook|cta|...] [--channel visual|spoken|...] [--limit 10]
|
|
1368
|
+
editorbrain narrative get <video-id> [--sha256 document-hash] [--event-offset 0] [--relation-offset 0] [--limit 50]
|
|
1369
|
+
editorbrain styles list
|
|
1370
|
+
editorbrain styles recommend <project-id> [--assets tag,tag] [--capabilities tag,tag] [--style pack-id] [--duration seconds]
|
|
1371
|
+
editorbrain direction draft <project-id> [--assets tag,tag] [--capabilities tag,tag] [--style pack-id] [--duration seconds] [--out new-folder]
|
|
1372
|
+
editorbrain assets list [--limit 200] [--cursor opaque-cursor]
|
|
1373
|
+
editorbrain assets upload <file> [--mime supported-media-type]
|
|
1374
|
+
editorbrain assets download <sha256> --out file
|
|
1375
|
+
editorbrain keys list
|
|
1376
|
+
editorbrain keys create --label name [--role reader|editor|owner]
|
|
1377
|
+
editorbrain keys revoke <key-id>
|
|
1138
1378
|
|
|
1139
|
-
Authentication:
|
|
1379
|
+
Authentication: EDITORBRAIN_URL and EDITORBRAIN_API_KEY, or login.
|
|
1140
1380
|
Secrets go to a user config with mode 0600, never the portable project.
|
|
1141
1381
|
Resolve writes the source-hash lock. Export produces a handoff, not a rendered video.
|
|
1142
1382
|
Push/pull preserve conflicting changes; media uploads use the separate assets command.
|
|
@@ -1147,8 +1387,8 @@ Output is JSON except help; login progress/code goes to stderr. --url overrides
|
|
|
1147
1387
|
function parseArgs(argv) {
|
|
1148
1388
|
const positional = [];
|
|
1149
1389
|
const flags = {};
|
|
1150
|
-
const booleans = /* @__PURE__ */ new Set(["help", "version", "offline", "dry-run", "key-stdin", "invite-stdin", "no-browser"]);
|
|
1151
|
-
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", ...booleans]);
|
|
1390
|
+
const booleans = /* @__PURE__ */ new Set(["help", "version", "offline", "dry-run", "key-stdin", "invite-stdin", "no-browser", "skip-mcp", "all"]);
|
|
1391
|
+
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", ...booleans]);
|
|
1152
1392
|
for (let i = 0; i < argv.length; i++) {
|
|
1153
1393
|
const arg = argv[i];
|
|
1154
1394
|
if (!arg.startsWith("--")) {
|
|
@@ -1174,7 +1414,15 @@ function output(value) {
|
|
|
1174
1414
|
process.stdout.write(stableJson(value));
|
|
1175
1415
|
}
|
|
1176
1416
|
async function configRoot(create = false) {
|
|
1177
|
-
|
|
1417
|
+
let path = process.env.EDITORBRAIN_CONFIG_DIR ?? process.env.EDITORHARNESS_CONFIG_DIR ?? join2(homedir(), ".config", "editorbrain");
|
|
1418
|
+
if (!create && !process.env.EDITORBRAIN_CONFIG_DIR && !process.env.EDITORHARNESS_CONFIG_DIR) {
|
|
1419
|
+
try {
|
|
1420
|
+
await lstat4(path);
|
|
1421
|
+
} catch (error) {
|
|
1422
|
+
if (error.code !== "ENOENT") throw error;
|
|
1423
|
+
path = join2(homedir(), ".config", "editorharness");
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1178
1426
|
if (create) await mkdir2(path, { recursive: true, mode: 448 });
|
|
1179
1427
|
return ensureRoot(path);
|
|
1180
1428
|
}
|
|
@@ -1196,10 +1444,10 @@ async function saveConfig(config2) {
|
|
|
1196
1444
|
}
|
|
1197
1445
|
async function config(args, requireKey = true) {
|
|
1198
1446
|
const stored = await loadConfig();
|
|
1199
|
-
const url = normalizeUrl(str(args, "url") ?? process.env.EDITORHARNESS_URL ?? stored.url ?? DEFAULT_API_URL);
|
|
1447
|
+
const url = normalizeUrl(str(args, "url") ?? process.env.EDITORBRAIN_URL ?? process.env.EDITORHARNESS_URL ?? stored.url ?? DEFAULT_API_URL);
|
|
1200
1448
|
const storedKey = stored.url && normalizeUrl(stored.url) === url ? stored.apiKey : void 0;
|
|
1201
|
-
const apiKey = process.env.EDITORHARNESS_API_KEY ?? storedKey ?? "";
|
|
1202
|
-
if (requireKey && !apiKey) throw new CliError("AUTH_REQUIRED", "Run editorbrain login or set
|
|
1449
|
+
const apiKey = process.env.EDITORBRAIN_API_KEY ?? process.env.EDITORHARNESS_API_KEY ?? storedKey ?? "";
|
|
1450
|
+
if (requireKey && !apiKey) throw new CliError("AUTH_REQUIRED", "Run editorbrain login or set EDITORBRAIN_API_KEY for this endpoint.", "Authorize the selected endpoint using browser consent.");
|
|
1203
1451
|
return { url, apiKey };
|
|
1204
1452
|
}
|
|
1205
1453
|
async function secretFromStdin(label) {
|
|
@@ -1220,7 +1468,7 @@ async function init(args) {
|
|
|
1220
1468
|
for (const path of Object.keys(files)) {
|
|
1221
1469
|
const target = await checkedPath(root, path);
|
|
1222
1470
|
try {
|
|
1223
|
-
await
|
|
1471
|
+
await lstat4(target);
|
|
1224
1472
|
throw new Error(`Refusing to replace existing file: ${path}`);
|
|
1225
1473
|
} catch (error) {
|
|
1226
1474
|
if (error.code !== "ENOENT") throw error;
|
|
@@ -1232,7 +1480,7 @@ async function init(args) {
|
|
|
1232
1480
|
for (const path of directories) {
|
|
1233
1481
|
const target = await checkedPath(root, path, false);
|
|
1234
1482
|
try {
|
|
1235
|
-
if (!(await
|
|
1483
|
+
if (!(await lstat4(target)).isDirectory()) throw new Error(`Not a directory: ${path}`);
|
|
1236
1484
|
} catch (error) {
|
|
1237
1485
|
if (error.code !== "ENOENT") throw error;
|
|
1238
1486
|
}
|
|
@@ -1241,11 +1489,53 @@ async function init(args) {
|
|
|
1241
1489
|
for (const path of directories) await mkdir2(await checkedPath(root, path, false), { recursive: true });
|
|
1242
1490
|
const guidance = await installGuidance();
|
|
1243
1491
|
const entryPoints = guidance.readInstructions.filter((item) => item.entryPointStatus === "created").map((item) => item.entryPoint);
|
|
1244
|
-
output({ root, created: Object.keys(files), entryPoints, guidance, next: `Edit brand tokens and brief, then run
|
|
1492
|
+
output({ root, created: Object.keys(files), entryPoints, guidance, next: `Edit brand tokens and brief, then run editorbrain resolve ${project}` });
|
|
1493
|
+
}
|
|
1494
|
+
async function login(args, quiet = false) {
|
|
1495
|
+
const selected = await config(args, false);
|
|
1496
|
+
if (args.flags["key-stdin"] && args.flags["no-browser"]) throw new Error("Choose --key-stdin or --no-browser, not both.");
|
|
1497
|
+
if (args.flags["key-stdin"] || process.env.EDITORBRAIN_API_KEY && !args.flags["no-browser"]) {
|
|
1498
|
+
const apiKey = args.flags["key-stdin"] ? await secretFromStdin("API key") : process.env.EDITORBRAIN_API_KEY;
|
|
1499
|
+
const saved = { url: selected.url, apiKey };
|
|
1500
|
+
const me = await new Client(saved).me();
|
|
1501
|
+
await saveConfig(saved);
|
|
1502
|
+
if (!quiet) output({ authenticated: true, endpoint: saved.url, workspaceId: me.workspaceId });
|
|
1503
|
+
return;
|
|
1504
|
+
}
|
|
1505
|
+
const controller = new AbortController();
|
|
1506
|
+
const cancel = () => controller.abort();
|
|
1507
|
+
const terminate = () => {
|
|
1508
|
+
process.exitCode = 143;
|
|
1509
|
+
controller.abort();
|
|
1510
|
+
};
|
|
1511
|
+
process.once("SIGINT", cancel);
|
|
1512
|
+
process.once("SIGTERM", terminate);
|
|
1513
|
+
try {
|
|
1514
|
+
const authorization = await loginWithDevice({
|
|
1515
|
+
url: selected.url,
|
|
1516
|
+
noBrowser: !!args.flags["no-browser"],
|
|
1517
|
+
timeoutSeconds: str(args, "timeout") === void 0 ? void 0 : Number(str(args, "timeout")),
|
|
1518
|
+
signal: controller.signal,
|
|
1519
|
+
onChallenge: (challenge) => process.stderr.write(`Authorize this device in your browser:
|
|
1520
|
+
${challenge.verificationUri}
|
|
1521
|
+
Code: ${challenge.userCode}
|
|
1522
|
+
Waiting for authorization. Press Ctrl+C to cancel.
|
|
1523
|
+
`),
|
|
1524
|
+
onBrowserUnavailable: () => process.stderr.write("The browser could not be opened. Use the URL and code above on any device.\n")
|
|
1525
|
+
});
|
|
1526
|
+
await saveConfig({ url: selected.url, apiKey: authorization.apiKey });
|
|
1527
|
+
if (!quiet) output({ authenticated: true, endpoint: selected.url, workspaceId: authorization.workspace.id, workspaceName: authorization.workspace.name, role: authorization.role, credentialStored: true });
|
|
1528
|
+
} finally {
|
|
1529
|
+
process.removeListener("SIGINT", cancel);
|
|
1530
|
+
process.removeListener("SIGTERM", terminate);
|
|
1531
|
+
}
|
|
1532
|
+
return;
|
|
1245
1533
|
}
|
|
1246
1534
|
async function main() {
|
|
1247
1535
|
const args = parseArgs(process.argv.slice(2));
|
|
1248
1536
|
const [command, subcommand, argument] = args.positional;
|
|
1537
|
+
if (args.flags.all && command !== "pull") throw new Error("--all is supported only by pull.");
|
|
1538
|
+
if (args.flags.all && str(args, "project")) throw new Error("Choose --project or --all, not both.");
|
|
1249
1539
|
if (command === "version" || args.flags.version) {
|
|
1250
1540
|
output({ name: "EditorBrain", version: await cliVersion() });
|
|
1251
1541
|
return;
|
|
@@ -1278,46 +1568,13 @@ async function main() {
|
|
|
1278
1568
|
output(await withWorkspaceLock(root2, () => installAgentGuidance(root2, agent)));
|
|
1279
1569
|
return;
|
|
1280
1570
|
}
|
|
1281
|
-
if (command === "
|
|
1282
|
-
const
|
|
1283
|
-
|
|
1284
|
-
if (
|
|
1285
|
-
const apiKey = args.flags["key-stdin"] ? await secretFromStdin("API key") : process.env.EDITORHARNESS_API_KEY;
|
|
1286
|
-
const saved = { url: selected.url, apiKey };
|
|
1287
|
-
const me = await new Client(saved).me();
|
|
1288
|
-
await saveConfig(saved);
|
|
1289
|
-
output({ authenticated: true, endpoint: saved.url, workspaceId: me.workspaceId });
|
|
1290
|
-
return;
|
|
1291
|
-
}
|
|
1292
|
-
const controller = new AbortController();
|
|
1293
|
-
const cancel = () => controller.abort();
|
|
1294
|
-
const terminate = () => {
|
|
1295
|
-
process.exitCode = 143;
|
|
1296
|
-
controller.abort();
|
|
1297
|
-
};
|
|
1298
|
-
process.once("SIGINT", cancel);
|
|
1299
|
-
process.once("SIGTERM", terminate);
|
|
1300
|
-
try {
|
|
1301
|
-
const authorization = await loginWithDevice({
|
|
1302
|
-
url: selected.url,
|
|
1303
|
-
noBrowser: !!args.flags["no-browser"],
|
|
1304
|
-
timeoutSeconds: str(args, "timeout") === void 0 ? void 0 : Number(str(args, "timeout")),
|
|
1305
|
-
signal: controller.signal,
|
|
1306
|
-
onChallenge: (challenge) => process.stderr.write(`Authorize this device in your browser:
|
|
1307
|
-
${challenge.verificationUri}
|
|
1308
|
-
Code: ${challenge.userCode}
|
|
1309
|
-
Waiting for authorization. Press Ctrl+C to cancel.
|
|
1310
|
-
`),
|
|
1311
|
-
onBrowserUnavailable: () => process.stderr.write("The browser could not be opened. Use the URL and code above on any device.\n")
|
|
1312
|
-
});
|
|
1313
|
-
await saveConfig({ url: selected.url, apiKey: authorization.apiKey });
|
|
1314
|
-
output({ authenticated: true, endpoint: selected.url, workspaceId: authorization.workspace.id, workspaceName: authorization.workspace.name, role: authorization.role, credentialStored: true });
|
|
1315
|
-
} finally {
|
|
1316
|
-
process.removeListener("SIGINT", cancel);
|
|
1317
|
-
process.removeListener("SIGTERM", terminate);
|
|
1318
|
-
}
|
|
1571
|
+
if (command === "setup") {
|
|
1572
|
+
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) });
|
|
1573
|
+
output(result);
|
|
1574
|
+
if (result.status !== "ready") process.exitCode = 2;
|
|
1319
1575
|
return;
|
|
1320
1576
|
}
|
|
1577
|
+
if (command === "login") return login(args);
|
|
1321
1578
|
if (command === "register") {
|
|
1322
1579
|
const selected = await config(args, false);
|
|
1323
1580
|
if (!args.flags["invite-stdin"]) throw new Error("Registration requires --invite-stdin (workspace invite provided by the service owner)");
|
|
@@ -1357,10 +1614,10 @@ Waiting for authorization. Press Ctrl+C to cancel.
|
|
|
1357
1614
|
const destination = str(args, "out");
|
|
1358
1615
|
if (destination) {
|
|
1359
1616
|
if (command !== "direction" || !result.files) throw new Error("--out is supported only for direction draft.");
|
|
1360
|
-
const outRoot = await ensureRoot(
|
|
1617
|
+
const outRoot = await ensureRoot(resolve4(str(args, "dir") ?? process.cwd(), destination), true);
|
|
1361
1618
|
for (const path of Object.keys(result.files)) {
|
|
1362
1619
|
try {
|
|
1363
|
-
await
|
|
1620
|
+
await lstat4(await checkedPath(outRoot, path, false));
|
|
1364
1621
|
throw new Error(`Refusing to replace existing proposal: ${path}`);
|
|
1365
1622
|
} catch (error) {
|
|
1366
1623
|
if (error.code !== "ENOENT") throw error;
|
|
@@ -1395,6 +1652,24 @@ Waiting for authorization. Press Ctrl+C to cancel.
|
|
|
1395
1652
|
}
|
|
1396
1653
|
throw new Error("Use keys list, keys create --label name, or keys revoke <key-id>");
|
|
1397
1654
|
}
|
|
1655
|
+
if (command === "projects" && subcommand === "list") {
|
|
1656
|
+
output({ projects: await listProjects(new Client(await config(args))) });
|
|
1657
|
+
return;
|
|
1658
|
+
}
|
|
1659
|
+
if (command === "documents") {
|
|
1660
|
+
const client = new Client(await config(args));
|
|
1661
|
+
if (subcommand === "list") {
|
|
1662
|
+
output({ documents: await client.list(str(args, "prefix") ?? "") });
|
|
1663
|
+
return;
|
|
1664
|
+
}
|
|
1665
|
+
if (subcommand === "get" && argument) {
|
|
1666
|
+
const doc = await client.get(argument);
|
|
1667
|
+
if (await sha256(doc.content) !== doc.sha256) throw new Error("Document integrity failed");
|
|
1668
|
+
output(doc);
|
|
1669
|
+
return;
|
|
1670
|
+
}
|
|
1671
|
+
throw new Error("Use documents list --prefix path or documents get <path>.");
|
|
1672
|
+
}
|
|
1398
1673
|
if (command === "assets") {
|
|
1399
1674
|
const client = new Client(await config(args));
|
|
1400
1675
|
if (subcommand === "list") {
|
|
@@ -1410,7 +1685,7 @@ Waiting for authorization. Press Ctrl+C to cancel.
|
|
|
1410
1685
|
return;
|
|
1411
1686
|
}
|
|
1412
1687
|
if (subcommand === "upload" && argument) {
|
|
1413
|
-
const target =
|
|
1688
|
+
const target = resolve4(str(args, "dir") ?? process.cwd(), argument);
|
|
1414
1689
|
const handle = await open4(target, constants4.O_RDONLY | constants4.O_NOFOLLOW);
|
|
1415
1690
|
let bytes;
|
|
1416
1691
|
try {
|
|
@@ -1432,7 +1707,7 @@ Waiting for authorization. Press Ctrl+C to cancel.
|
|
|
1432
1707
|
}
|
|
1433
1708
|
if (subcommand === "download" && argument && str(args, "out")) {
|
|
1434
1709
|
if (!/^[a-f0-9]{64}$/.test(argument)) throw new Error("Asset ID must be its SHA-256");
|
|
1435
|
-
const destination =
|
|
1710
|
+
const destination = resolve4(str(args, "dir") ?? process.cwd(), str(args, "out"));
|
|
1436
1711
|
const root2 = await ensureRoot(dirname2(destination), true);
|
|
1437
1712
|
await checkedPath(root2, basename(destination), false);
|
|
1438
1713
|
const response = await client.media(`/v1/assets/${argument}`);
|
|
@@ -1444,6 +1719,22 @@ Waiting for authorization. Press Ctrl+C to cancel.
|
|
|
1444
1719
|
}
|
|
1445
1720
|
throw new Error("Use assets list, assets upload <file>, or assets download <sha256> --out file");
|
|
1446
1721
|
}
|
|
1722
|
+
if (command === "narrative") {
|
|
1723
|
+
const client = new Client(await config(args));
|
|
1724
|
+
const query = new URLSearchParams();
|
|
1725
|
+
if (subcommand === "search") {
|
|
1726
|
+
query.set("query", args.positional.slice(2).join(" "));
|
|
1727
|
+
for (const [flag, key] of [["kind", "kind"], ["channel", "channel"], ["epistemic-status", "epistemicStatus"], ["limit", "limit"]]) if (str(args, flag) !== void 0) query.set(key, str(args, flag));
|
|
1728
|
+
output(await client.request("/v1/narrative?" + query));
|
|
1729
|
+
return;
|
|
1730
|
+
}
|
|
1731
|
+
if (subcommand === "get" && argument) {
|
|
1732
|
+
for (const [flag, key] of [["sha256", "sha256"], ["event-offset", "eventOffset"], ["relation-offset", "relationOffset"], ["limit", "limit"]]) if (str(args, flag) !== void 0) query.set(key, str(args, flag));
|
|
1733
|
+
output(await client.request(`/v1/library/${encodeURIComponent(argument)}/narrative?${query}`));
|
|
1734
|
+
return;
|
|
1735
|
+
}
|
|
1736
|
+
throw new Error("Use narrative search [query] or narrative get <video-id>.");
|
|
1737
|
+
}
|
|
1447
1738
|
if (command === "library") {
|
|
1448
1739
|
const client = new Client(await config(args));
|
|
1449
1740
|
if (subcommand === "search") {
|
|
@@ -1452,17 +1743,48 @@ Waiting for authorization. Press Ctrl+C to cancel.
|
|
|
1452
1743
|
}
|
|
1453
1744
|
if (subcommand === "get" && argument) {
|
|
1454
1745
|
const document = str(args, "document");
|
|
1455
|
-
|
|
1746
|
+
const hash = str(args, "sha256");
|
|
1747
|
+
if (hash && !document) throw new Error("--sha256 requires --document for library get.");
|
|
1748
|
+
const result = await client.request(`/v1/library/${encodeURIComponent(argument)}${document ? `/documents?${new URLSearchParams({ path: document, ...hash ? { sha256: hash } : {} })}` : ""}`);
|
|
1749
|
+
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.");
|
|
1750
|
+
output(result);
|
|
1751
|
+
return;
|
|
1752
|
+
}
|
|
1753
|
+
if (subcommand === "media" && argument && str(args, "out")) {
|
|
1754
|
+
const hash = str(args, "sha256");
|
|
1755
|
+
if (!hash || !/^[a-f0-9]{64}$/.test(hash)) throw new Error("--sha256 must be a media SHA-256 from library get.");
|
|
1756
|
+
const destination = resolve4(str(args, "dir") ?? process.cwd(), str(args, "out"));
|
|
1757
|
+
const root2 = await ensureRoot(dirname2(destination), true);
|
|
1758
|
+
await checkedPath(root2, basename(destination), false);
|
|
1759
|
+
const response = await client.media(`/v1/library/${encodeURIComponent(argument)}/media/${hash}`);
|
|
1760
|
+
const reader = response.body?.getReader();
|
|
1761
|
+
if (!reader) throw new Error("Media body missing.");
|
|
1762
|
+
const chunks = [];
|
|
1763
|
+
let size = 0;
|
|
1764
|
+
for (; ; ) {
|
|
1765
|
+
const { done, value } = await reader.read();
|
|
1766
|
+
if (done) break;
|
|
1767
|
+
size += value.length;
|
|
1768
|
+
if (size > 90 * 1024 * 1024) {
|
|
1769
|
+
await reader.cancel();
|
|
1770
|
+
throw new CliError("MEDIA_TOO_LARGE", "Reference media exceeds the 90 MiB CLI limit.", "Select a smaller derivative from the reference inventory.");
|
|
1771
|
+
}
|
|
1772
|
+
chunks.push(value);
|
|
1773
|
+
}
|
|
1774
|
+
const bytes = Buffer.concat(chunks);
|
|
1775
|
+
if (await sha256(bytes) !== hash) throw new CliError("HASH_MISMATCH", "Reference media verification failed.", "Do not use this file as evidence.");
|
|
1776
|
+
await atomicWrite(root2, basename(destination), bytes, { document: false, overwrite: false });
|
|
1777
|
+
output({ videoId: argument, path: destination, sha256: hash, bytes: size, mimeType: response.headers.get("Content-Type"), reviewed: false });
|
|
1456
1778
|
return;
|
|
1457
1779
|
}
|
|
1458
|
-
throw new Error("Use library search
|
|
1780
|
+
throw new Error("Use library search, library get, or library media <video-id> --sha256 hash --out file.");
|
|
1459
1781
|
}
|
|
1460
1782
|
if (!["validate", "resolve", "export", "push", "pull"].includes(command)) throw new Error(`Unknown command: ${command}`);
|
|
1461
1783
|
const root = await ensureRoot(str(args, "dir") ?? process.cwd(), command === "pull" && !args.flags["dry-run"]);
|
|
1462
1784
|
const run = async () => {
|
|
1463
1785
|
const files = await readWorkspace(root);
|
|
1464
1786
|
if (command === "pull") {
|
|
1465
|
-
output(await pullWorkspace(new Client(await config(args)), root, files, { dryRun: !!args.flags["dry-run"] }));
|
|
1787
|
+
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"] })));
|
|
1466
1788
|
return;
|
|
1467
1789
|
}
|
|
1468
1790
|
if (Object.keys(files).length === 0) throw new Error("No portable documents found. Run init or pull first.");
|
|
@@ -1487,7 +1809,7 @@ Waiting for authorization. Press Ctrl+C to cancel.
|
|
|
1487
1809
|
const adapter = str(args, "adapter") ?? "neutral";
|
|
1488
1810
|
if (!["neutral", "hyperframes", "remotion"].includes(adapter)) throw new Error("Adapter must be neutral, hyperframes or remotion");
|
|
1489
1811
|
const generated = exportProject(resolved, adapter);
|
|
1490
|
-
const destination =
|
|
1812
|
+
const destination = resolve4(root, str(args, "out") ?? `videos/${project}/plans/${adapter}`);
|
|
1491
1813
|
const outRoot = await ensureRoot(destination, true);
|
|
1492
1814
|
for (const [path, content] of Object.entries(generated)) await atomicWrite(outRoot, path, content, { document: false });
|
|
1493
1815
|
output({ adapter, kind: "editorial_handoff", rendered: false, directory: outRoot, files: Object.keys(generated), warnings: resolved.warnings });
|
package/package.json
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "editorbrain",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "EditorBrain editorial knowledge, portable workspaces and skills for AI video agents",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "UNLICENSED",
|
|
7
7
|
"bin": {
|
|
8
|
-
"editorbrain": "bin/
|
|
9
|
-
"editorharness": "bin/editorharness.mjs"
|
|
8
|
+
"editorbrain": "bin/editorbrain.mjs"
|
|
10
9
|
},
|
|
11
10
|
"files": [
|
|
12
11
|
"bin",
|
|
@@ -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,6 +10,16 @@ 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`.
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
---
|
|
2
|
-
name:
|
|
3
|
-
description: Use EditorBrain
|
|
2
|
+
name: editorbrain
|
|
3
|
+
description: Use EditorBrain to edit a user's video, adapt an editorial style, find reference evidence, or manage brand and video project context through its CLI or MCP. Connect the project's actual footage, script and brand to editing decisions, then implement and review the result with the available editing tools. Also use when the user names EditorBrain, requests its library or asks to follow a saved preset.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# EditorBrain
|
|
7
7
|
|
|
8
|
-
The public product is EditorBrain. The
|
|
8
|
+
The public product is EditorBrain. The npm package is `editorbrain`; `editorbrain` is the CLI command. Skill name and portable folder paths use `editorbrain`. Web: https://editorbrain.com. API/MCP: https://api.editorbrain.com/mcp.
|
|
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
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# CLI and MCP: read, retrieve, save
|
|
2
2
|
|
|
3
|
-
Use the connected MCP schema and `
|
|
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
|
|
|
@@ -9,24 +9,32 @@ API/MCP: `https://api.editorbrain.com/mcp`. The host must connect this server se
|
|
|
9
9
|
An existing account normally starts with:
|
|
10
10
|
|
|
11
11
|
```sh
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
editorbrain status
|
|
13
|
+
editorbrain projects list
|
|
14
|
+
editorbrain pull --project <id> --dir ./my-videos
|
|
15
|
+
editorbrain skill install --dir ./my-videos --agent both
|
|
15
16
|
```
|
|
16
17
|
|
|
17
|
-
If unauthenticated, `
|
|
18
|
+
If unauthenticated, `editorbrain login --url https://api.editorbrain.com` uses browser/device consent. Request the user's participation only for this required login step. Credentials stay outside project files. CLI login does not automatically authenticate a separate MCP host: register the MCP URL without a bearer header and authorize through the host’s OAuth login. Choose the same workspace in the consent page. Existing bearer-key connections remain compatible; do not overwrite one during setup.
|
|
18
19
|
|
|
19
|
-
Skill installation is offline and independent of login. It creates `.agents/skills/
|
|
20
|
+
Skill installation is offline and independent of login. It creates `.agents/skills/editorbrain/` for Codex and `.claude/skills/editorbrain/` for Claude. Open the target folder in the agent and explicitly ask it to read the installed `SKILL.md` if automatic discovery has not refreshed. The skill directs the agent to its bundled `AGENTS.md`; a bundled file alone is not assumed to load automatically.
|
|
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, `
|
|
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.
|
|
@@ -34,13 +42,13 @@ MCP sequence:
|
|
|
34
42
|
Use the local files after a successful `pull` when working through CLI. Commands:
|
|
35
43
|
|
|
36
44
|
```sh
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
45
|
+
editorbrain validate --dir ./my-videos
|
|
46
|
+
editorbrain resolve <project-id> --dir ./my-videos
|
|
47
|
+
editorbrain assets list --limit 200
|
|
48
|
+
editorbrain assets download <sha256> --out ./media/clip.mp4
|
|
41
49
|
```
|
|
42
50
|
|
|
43
|
-
`assets list` returns `nextCursor`; continue with `--cursor` when needed. MCP `list_assets
|
|
51
|
+
`assets list` returns `nextCursor`; continue with `--cursor` when needed. MCP `list_assets` also returns `nextCursor`; pass it as `cursor` to retrieve the next page (default 100, maximum 1000). Project `media.json` is the primary source for project-specific IDs. Asset tags passed to recommendations describe declared resources, not tool-verified contents.
|
|
44
52
|
|
|
45
53
|
## Selecting a method and examining examples
|
|
46
54
|
|
|
@@ -58,16 +66,16 @@ Discover current argument schemas before calling:
|
|
|
58
66
|
CLI counterparts for discovery/documents:
|
|
59
67
|
|
|
60
68
|
```sh
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
69
|
+
editorbrain styles list
|
|
70
|
+
editorbrain styles recommend <project-id> --dir ./my-videos
|
|
71
|
+
editorbrain library search "comparison"
|
|
72
|
+
editorbrain library get <video-id>
|
|
73
|
+
editorbrain library get <video-id> --document <returned-path>
|
|
66
74
|
```
|
|
67
75
|
|
|
68
76
|
Recommendations and direction drafts use the saved remote project, require a non-empty script and check that local source context matches it. After `init` or local edits, validate and successfully `push` the intended project context before those calls. Do not discard unsynced local changes to silence a context mismatch. An offline draft can be written by the agent from available evidence without claiming a remote recommendation ran.
|
|
69
77
|
|
|
70
|
-
|
|
78
|
+
Pin historical evidence with `editorbrain library get <video-id> --document <path> --sha256 <source-hash>`, or MCP `read_reference_document` with `sha256`. CLI checks the returned content hash. Without a pin, the latest version is retrieved; keep the returned hash.
|
|
71
79
|
|
|
72
80
|
### Complete enough evidence
|
|
73
81
|
|
|
@@ -88,8 +96,8 @@ Access is scoped to the current workspace's reference grant and credential. A kn
|
|
|
88
96
|
`draft_direction` proposes an initial method and techniques. Inspect its pending requirements and transfer status, complete the per-beat decisions using evidence and the actual material, then implement the user's requested output. Available CLI:
|
|
89
97
|
|
|
90
98
|
```sh
|
|
91
|
-
|
|
92
|
-
|
|
99
|
+
editorbrain direction draft <project-id> --dir ./my-videos --out ./direction-draft
|
|
100
|
+
editorbrain export <project-id> --dir ./my-videos --adapter neutral --out ./handoff
|
|
93
101
|
```
|
|
94
102
|
|
|
95
103
|
`export_project({projectId,adapter})` supports `neutral`, `hyperframes` and `remotion`. It returns plans for an external rendering agent/engine; it does not create a video. Inspect the engine's real capabilities before committing to an effect.
|
|
@@ -97,20 +105,30 @@ editorharness export <project-id> --dir ./my-videos --adapter neutral --out ./ha
|
|
|
97
105
|
Save through local portable documents plus `push`, or MCP `write_document` with the revision returned by the last read and the actual schema. To create a new document, use `expectedRevision: 0`; to update an existing one, use its current read revision. Do not guess a revision or force over a conflict; reread, reconcile and retry only if still authorized. Keep generated production notes small enough for the document limit (128 KiB per document); do not duplicate every raw catalog response into a customer workspace.
|
|
98
106
|
|
|
99
107
|
```sh
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
108
|
+
editorbrain validate --dir ./my-videos
|
|
109
|
+
editorbrain resolve <project-id> --dir ./my-videos
|
|
110
|
+
editorbrain push --dir ./my-videos
|
|
103
111
|
```
|
|
104
112
|
|
|
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
|
|
|
112
120
|
Before reconciling sync problems, use `push --dry-run` or `pull --dry-run`. Read `conflicts` and the planned paths. A preview neither writes nor reserves revisions. Never use a separate workspace to evade a binding mismatch.
|
|
113
121
|
|
|
114
|
-
Errors include `code`, `hint`, `retryable` and the legacy text `error`. `AUTH_REQUIRED` needs user authorization, `ACCESS_DENIED` needs the right permission, `SYNC_CONFLICT` needs reconciliation, and `WORKSPACE_MISMATCH` requires the correct separate folder. `WORKSPACE_BUSY` means another CLI operation owns `.
|
|
122
|
+
Errors include `code`, `hint`, `retryable` and the legacy text `error`. `AUTH_REQUIRED` needs user authorization, `ACCESS_DENIED` needs the right permission, `SYNC_CONFLICT` needs reconciliation, and `WORKSPACE_MISMATCH` requires the correct separate folder. `WORKSPACE_BUSY` means another CLI operation owns `.editorbrain-operation.lock`; do not auto-delete it. After a crash, verify no process is using the folder before removing a leftover lock.
|
|
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.
|
|
125
|
+
|
|
126
|
+
## Guided setup and OAuth (0.7.0)
|
|
127
|
+
|
|
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.
|
|
129
|
+
|
|
130
|
+
Codex can use `codex mcp login editorbrain`; Claude Code completes OAuth from `/mcp`. Native host configuration is not proof of an authenticated MCP. Call `workspace_info` inside the host and compare it with CLI `status`. A read-only consent cannot save documents: ask for editing scope only when needed. Revoke OAuth connections from the app’s connection settings. CLI device credentials and MCP OAuth connections are separate.
|
|
131
|
+
|
|
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.
|
|
133
|
+
|
|
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.
|
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
# EditorBrain workspace files
|
|
2
2
|
|
|
3
|
-
Use this reference when creating a project, changing branding or styles, resolving conflicting settings, or deciding where to save feedback. Technical paths and the CLI remain named `
|
|
3
|
+
Use this reference when creating a project, changing branding or styles, resolving conflicting settings, or deciding where to save feedback. Technical paths and the CLI remain named `editorbrain`; the public product is EditorBrain.
|
|
4
4
|
|
|
5
5
|
## Discover before editing
|
|
6
6
|
|
|
7
|
-
Read `videos/<project-id>/project.json` first: its `brand`, `style` and `outputProfile` fields select the context. Discover IDs using the returned documents or local files. Folder names below are placeholders, not literal IDs. For a new local workspace use `
|
|
7
|
+
Read `videos/<project-id>/project.json` first: its `brand`, `style` and `outputProfile` fields select the context. Discover IDs using the returned documents or local files. Folder names below are placeholders, not literal IDs. For a new local workspace use `editorbrain init`; for an existing account use `pull`. Read the generated documents before changing their schema.
|
|
8
8
|
|
|
9
9
|
```text
|
|
10
10
|
AGENTS.md Codex entry point (existing instructions preserved)
|
|
11
11
|
CLAUDE.md Claude Code entry point (existing instructions preserved)
|
|
12
|
-
.agents/skills/
|
|
13
|
-
.claude/skills/
|
|
14
|
-
.
|
|
12
|
+
.agents/skills/editorbrain/ Codex skill, contract and references
|
|
13
|
+
.claude/skills/editorbrain/ Claude Code skill, contract and references
|
|
14
|
+
.editorbrain/
|
|
15
15
|
brands/<brand-id>/
|
|
16
16
|
brand.md Identity, audience, constraints
|
|
17
17
|
design.md Composition and visual treatment (optional)
|
|
@@ -38,7 +38,7 @@ videos/<project-id>/
|
|
|
38
38
|
reviews/ Reviews tied to a particular delivered version
|
|
39
39
|
```
|
|
40
40
|
|
|
41
|
-
The native skill folders are local instructions, not portable documents synchronized by `push`. Local media/render folders are not media uploads. `.
|
|
41
|
+
The native skill folders are local instructions, not portable documents synchronized by `push`. Local media/render folders are not media uploads. `.editorbrain/.sync.json` is CLI-managed sync state; do not edit it to bypass conflicts. An older valid workspace may omit the optional files and local production directories.
|
|
42
42
|
|
|
43
43
|
The resolver requires the selected brand's `brand.md`, `tokens.json`, `language.json`, `assets.json`; the selected style's `style.md`, `style.json`, `examples.json`; the selected output profile; and the project's `project.json`, `brief.md`, `direction.md`. Preserve them even when a declaration is empty. Additional guidance such as `adaptation-rules.json`, library indexes or memory may be present: read it explicitly when relevant. It is not automatically included in `resolve` merely because it exists.
|
|
44
44
|
|
|
@@ -76,6 +76,6 @@ Save reusable feedback at its stated scope: project-only changes in the project,
|
|
|
76
76
|
|
|
77
77
|
## Updating the installed skill
|
|
78
78
|
|
|
79
|
-
Updating the global CLI does not replace skills already copied into a project. Run `
|
|
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.
|
|
File without changes
|