@mindscraft/branch-video-agent-cli 0.4.7 → 0.5.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 +65 -3
- package/dist/commands/workbench.d.ts +2 -0
- package/dist/commands/workbench.js +305 -0
- package/dist/index.js +15 -1
- package/dist/lib/flags.d.ts +8 -0
- package/dist/lib/flags.js +42 -1
- package/dist/lib/http.d.ts +17 -0
- package/dist/lib/http.js +81 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -252,7 +252,61 @@ AI Gateway Seed Audio 音频生成并等待示例:
|
|
|
252
252
|
'@ | npm exec --yes --package=@mindscraft/branch-video-agent-cli branch-video-agent -- ai-gateway seed-audio-create -- --wait --raw
|
|
253
253
|
```
|
|
254
254
|
|
|
255
|
-
`tts-create` / `tts-get` 是 Seed Audio 的历史兼容别名;新脚本优先使用 `seed-audio-create` / `seed-audio-get`。
|
|
255
|
+
`tts-create` / `tts-get` 是 Seed Audio 的历史兼容别名;新脚本优先使用 `seed-audio-create` / `seed-audio-get`。
|
|
256
|
+
|
|
257
|
+
## RPG2 生产工作台
|
|
258
|
+
|
|
259
|
+
工作台 CLI 只提交调用方显式登记的项目、产物和归档清单;它不会扫描 RPG 阶段目录,也不会根据文件名推断版本关系。认证仍使用 `AIHUB_AGENT_TOKEN` 与 `BRANCH_VIDEO_AGENT_BASE_URL`,不要在 JSON、命令行或日志中保存 token。
|
|
260
|
+
|
|
261
|
+
项目、生产记录和外部 CDN 产物可使用 JSON 文件提交:
|
|
262
|
+
|
|
263
|
+
```powershell
|
|
264
|
+
branch-video-agent workbench project create --file .\project.json --raw
|
|
265
|
+
branch-video-agent workbench record put --project wb_project_id --file .\record.json --raw
|
|
266
|
+
branch-video-agent workbench submit --file .\artifact-external.json --raw
|
|
267
|
+
branch-video-agent workbench annotation put --project wb_project_id --file .\annotation.json --raw
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
同一产物用稳定的 `artifactKey` 和由流程侧持久维护的递增 `sourceRevision`。`idempotencyKey` 由流程侧保存并在重试时复用。已有持久媒体使用 `content.type: "external"`;本地文档、脱敏 session 或其他需要长期保存的文件使用 `content.type: "local"` 与 `sourcePath`。CLI 会先计算 SHA-256、声明 blob,再以原始二进制流上传;服务端只收到 blob 描述,绝不会收到本地路径。单文件上限为 64 MiB。
|
|
271
|
+
|
|
272
|
+
```json
|
|
273
|
+
{
|
|
274
|
+
"schemaVersion": "workbench.artifact/1",
|
|
275
|
+
"projectId": "wb_project_id",
|
|
276
|
+
"artifactKey": "document:director-program",
|
|
277
|
+
"sourceRevision": 2,
|
|
278
|
+
"idempotencyKey": "persisted-event-uuid",
|
|
279
|
+
"kind": "markdown",
|
|
280
|
+
"stageKey": "04",
|
|
281
|
+
"title": "导演稿",
|
|
282
|
+
"content": {
|
|
283
|
+
"type": "local",
|
|
284
|
+
"sourcePath": "C:/production/canonical/director.md",
|
|
285
|
+
"mime": "text/markdown"
|
|
286
|
+
},
|
|
287
|
+
"source": {
|
|
288
|
+
"productionRecordId": "record_id",
|
|
289
|
+
"archiveGeneration": 0,
|
|
290
|
+
"sourceSchema": "director-program/4"
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
上传响应在网络中丢失时,CLI 会先查询同一个已声明 blob;ready 状态不会重传,也不会重新登记业务版本。失败交给流程侧持久待传队列用相同事件重试。
|
|
296
|
+
|
|
297
|
+
`sync` 只读取 `--manifest` 中显式的 `items`。每个 item 为 `type: "artifact"`(默认)或 `type: "annotation"`,可直接携带提交字段或放进 `submission`;不会递归任何工作目录。
|
|
298
|
+
|
|
299
|
+
```powershell
|
|
300
|
+
branch-video-agent workbench sync --manifest .\.workbench\pending-items.json --raw
|
|
301
|
+
branch-video-agent workbench archive prepare --manifest .\.workbench\archive-wrapper.json --raw
|
|
302
|
+
branch-video-agent workbench archive finalize --project wb_project_id --record record_id --revision 1 --manifest-hash <sha256> --raw
|
|
303
|
+
branch-video-agent workbench archive status --project wb_project_id --record record_id --raw
|
|
304
|
+
branch-video-agent workbench archive download --project wb_project_id --record record_id --revision 1 --file .\entry.json --output .\session.jsonl.zst --raw
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
`archive prepare` 的文件是调用方维护的 wrapper:`{ manifest, manifestHash, expectedPreviousRevision }`;CLI 原样提交规范化清单和 hash。`archive finalize` 只校验已经提交的清单,不补扫文件。下载的 `entry.json` 至少包含 `{ "entryId": "..." }`;受权限保护的字节写入 `--output`,stdout 仍只输出 JSON 回执。
|
|
308
|
+
|
|
309
|
+
`record put` 的路径参数是稳定的流程 `recordKey`,返回对象的 `id` 才是后续 `end-intent`、`reopen` 和所有 `archive` 命令使用的 `productionRecordId`(也可传给 `--record`)。不要把 `recordKey` 当成归档路由 ID。
|
|
256
310
|
|
|
257
311
|
## 在仓库内构建发布包
|
|
258
312
|
|
|
@@ -260,8 +314,16 @@ AI Gateway Seed Audio 音频生成并等待示例:
|
|
|
260
314
|
|
|
261
315
|
```powershell
|
|
262
316
|
npm run agent:cli:package:build
|
|
263
|
-
npm run agent:cli:package:smoke
|
|
264
|
-
```
|
|
317
|
+
npm run agent:cli:package:smoke
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
需要运行会启动 standalone Mongo、真实 CLI HTTP 流、Python adapter 或 Chromium 的 Production Workbench 集成验收时,使用专用命令;常规 `npm test` 会排除这些外部运行时依赖。adapter E2E 需要相邻的 rpg2 adapter checkout,或显式设置 `RPG2_WORKBENCH_ADAPTER_ROOT`;缺失 checkout、`zstd` 或 Chromium 时命令应失败。
|
|
321
|
+
|
|
322
|
+
```powershell
|
|
323
|
+
npm run test:workbench-cli-integration
|
|
324
|
+
npm run test:workbench-adapter-e2e
|
|
325
|
+
npm run test:workbench-integration
|
|
326
|
+
```
|
|
265
327
|
|
|
266
328
|
产物输出到:
|
|
267
329
|
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { createReadStream } from 'node:fs';
|
|
3
|
+
import { stat, writeFile } from 'node:fs/promises';
|
|
4
|
+
import { Readable } from 'node:stream';
|
|
5
|
+
import { CliCommandError } from '../lib/errors.js';
|
|
6
|
+
import { ensureObject, getRequiredInt, getRequiredString, omitKeys, pickDefined } from '../lib/payload.js';
|
|
7
|
+
const workbenchPath = '/api/production-workbench';
|
|
8
|
+
const MAX_LOCAL_FILE_BYTES = 64 * 1024 * 1024;
|
|
9
|
+
const blobPurposes = new Set(['artifact', 'manifest', 'session', 'evidence']);
|
|
10
|
+
function payloadValue(payload, flagValue, key) {
|
|
11
|
+
return getRequiredString({ [key]: payload[key] || flagValue }, key);
|
|
12
|
+
}
|
|
13
|
+
function projectId(payload, flagValue) {
|
|
14
|
+
return payloadValue(payload, flagValue, 'projectId');
|
|
15
|
+
}
|
|
16
|
+
function productionRecordId(payload, flagValue) {
|
|
17
|
+
return getRequiredString({ productionRecordId: payload.productionRecordId || payload.recordId || flagValue }, 'productionRecordId');
|
|
18
|
+
}
|
|
19
|
+
function getRevision(payload, flagValue) {
|
|
20
|
+
return payload.revision === undefined && flagValue
|
|
21
|
+
? getRequiredInt({ revision: flagValue }, 'revision')
|
|
22
|
+
: getRequiredInt(payload, 'revision');
|
|
23
|
+
}
|
|
24
|
+
function localContent(payload) {
|
|
25
|
+
const content = ensureObject(payload.content, 'content');
|
|
26
|
+
const sourcePath = typeof content.sourcePath === 'string' && content.sourcePath.trim()
|
|
27
|
+
? content.sourcePath.trim()
|
|
28
|
+
: typeof payload.sourcePath === 'string' && payload.sourcePath.trim()
|
|
29
|
+
? payload.sourcePath.trim()
|
|
30
|
+
: '';
|
|
31
|
+
return sourcePath ? { content, sourcePath } : null;
|
|
32
|
+
}
|
|
33
|
+
async function sha256File(sourcePath) {
|
|
34
|
+
const digest = createHash('sha256');
|
|
35
|
+
for await (const chunk of createReadStream(sourcePath))
|
|
36
|
+
digest.update(chunk);
|
|
37
|
+
return digest.digest('hex');
|
|
38
|
+
}
|
|
39
|
+
async function uploadLocalContent(context, sourcePath, content) {
|
|
40
|
+
let fileStat;
|
|
41
|
+
try {
|
|
42
|
+
fileStat = await stat(sourcePath);
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
throw new CliCommandError(`Cannot read sourcePath: ${error?.message || error}`, 'VALIDATION_ERROR');
|
|
46
|
+
}
|
|
47
|
+
if (!fileStat.isFile())
|
|
48
|
+
throw new CliCommandError('sourcePath must be a regular file', 'VALIDATION_ERROR');
|
|
49
|
+
if (fileStat.size > MAX_LOCAL_FILE_BYTES)
|
|
50
|
+
throw new CliCommandError(`sourcePath exceeds ${MAX_LOCAL_FILE_BYTES} byte upload limit`, 'FILE_TOO_LARGE');
|
|
51
|
+
const sha256 = await sha256File(sourcePath);
|
|
52
|
+
if (typeof content.sha256 === 'string' && content.sha256 && content.sha256 !== sha256) {
|
|
53
|
+
throw new CliCommandError('sourcePath sha256 does not match content.sha256', 'VALIDATION_ERROR');
|
|
54
|
+
}
|
|
55
|
+
const mime = typeof content.mime === 'string' && content.mime ? content.mime : 'application/octet-stream';
|
|
56
|
+
const purpose = typeof content.purpose === 'string' && content.purpose ? content.purpose : 'artifact';
|
|
57
|
+
if (!blobPurposes.has(purpose))
|
|
58
|
+
throw new CliCommandError(`Invalid local content purpose: ${purpose}`, 'VALIDATION_ERROR');
|
|
59
|
+
const declared = await context.client.request(`${workbenchPath}/projects/${encodeURIComponent(projectId(context.payload, context.flags.project))}/blobs`, {
|
|
60
|
+
method: 'POST',
|
|
61
|
+
body: {
|
|
62
|
+
sha256,
|
|
63
|
+
size: fileStat.size,
|
|
64
|
+
mime,
|
|
65
|
+
purpose,
|
|
66
|
+
...(content.redaction === undefined ? {} : { redaction: content.redaction }),
|
|
67
|
+
},
|
|
68
|
+
timeoutMs: context.flags.timeoutMs,
|
|
69
|
+
});
|
|
70
|
+
const blobId = getRequiredString(declared.data, 'id');
|
|
71
|
+
if (declared.data.state !== 'ready') {
|
|
72
|
+
let uploadError;
|
|
73
|
+
try {
|
|
74
|
+
await context.client.requestStream(`${workbenchPath}/projects/${encodeURIComponent(projectId(context.payload, context.flags.project))}/blobs/${encodeURIComponent(blobId)}/content`, {
|
|
75
|
+
method: 'PUT',
|
|
76
|
+
body: Readable.toWeb(createReadStream(sourcePath)),
|
|
77
|
+
contentType: mime,
|
|
78
|
+
timeoutMs: context.flags.timeoutMs,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
uploadError = error;
|
|
83
|
+
}
|
|
84
|
+
if (uploadError) {
|
|
85
|
+
const status = await context.client.request(`${workbenchPath}/projects/${encodeURIComponent(projectId(context.payload, context.flags.project))}/blobs/${encodeURIComponent(blobId)}`, {
|
|
86
|
+
method: 'GET',
|
|
87
|
+
timeoutMs: context.flags.timeoutMs,
|
|
88
|
+
});
|
|
89
|
+
if (status.data.state !== 'ready')
|
|
90
|
+
throw uploadError;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return { type: 'blob', blobId, sha256, size: fileStat.size, mime };
|
|
94
|
+
}
|
|
95
|
+
function validateSubmission(payload) {
|
|
96
|
+
if (payload.schemaVersion !== 'workbench.artifact/1') {
|
|
97
|
+
throw new CliCommandError('schemaVersion must be workbench.artifact/1', 'VALIDATION_ERROR');
|
|
98
|
+
}
|
|
99
|
+
getRequiredString(payload, 'artifactKey');
|
|
100
|
+
const sourceRevision = getRequiredInt(payload, 'sourceRevision');
|
|
101
|
+
if (sourceRevision < 1)
|
|
102
|
+
throw new CliCommandError('Invalid sourceRevision', 'VALIDATION_ERROR');
|
|
103
|
+
getRequiredString(payload, 'idempotencyKey');
|
|
104
|
+
getRequiredString(payload, 'title');
|
|
105
|
+
getRequiredString(payload, 'kind');
|
|
106
|
+
getRequiredString(payload, 'stageKey');
|
|
107
|
+
const source = ensureObject(payload.source, 'source');
|
|
108
|
+
getRequiredString(source, 'productionRecordId');
|
|
109
|
+
const archiveGeneration = getRequiredInt(source, 'archiveGeneration');
|
|
110
|
+
if (archiveGeneration < 0)
|
|
111
|
+
throw new CliCommandError('Invalid source.archiveGeneration', 'VALIDATION_ERROR');
|
|
112
|
+
}
|
|
113
|
+
async function submitArtifact(context, payload) {
|
|
114
|
+
const id = projectId(payload, context.flags.project);
|
|
115
|
+
validateSubmission(payload);
|
|
116
|
+
const local = localContent(payload);
|
|
117
|
+
let body = omitKeys(payload, ['projectId', 'sourcePath']);
|
|
118
|
+
if (local) {
|
|
119
|
+
const blob = await uploadLocalContent({ ...context, payload }, local.sourcePath, local.content);
|
|
120
|
+
body = { ...body, content: blob };
|
|
121
|
+
}
|
|
122
|
+
const result = await context.client.request(`${workbenchPath}/projects/${encodeURIComponent(id)}/artifact-versions`, {
|
|
123
|
+
method: 'POST',
|
|
124
|
+
body,
|
|
125
|
+
timeoutMs: context.flags.timeoutMs,
|
|
126
|
+
});
|
|
127
|
+
return { data: result.data, requestId: result.requestId };
|
|
128
|
+
}
|
|
129
|
+
async function readManifest(context) {
|
|
130
|
+
if (!context.flags.manifest)
|
|
131
|
+
throw new CliCommandError('Missing --manifest', 'VALIDATION_ERROR');
|
|
132
|
+
try {
|
|
133
|
+
return ensureObject(JSON.parse(await context.runtime.readTextFile(context.flags.manifest)), '--manifest');
|
|
134
|
+
}
|
|
135
|
+
catch (error) {
|
|
136
|
+
if (error instanceof CliCommandError)
|
|
137
|
+
throw error;
|
|
138
|
+
throw new CliCommandError(`Invalid JSON in --manifest: ${error?.message || error}`, 'VALIDATION_ERROR');
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
export const workbenchCommands = {
|
|
142
|
+
'project.create': async ({ client, payload }) => {
|
|
143
|
+
const result = await client.request(`${workbenchPath}/projects`, {
|
|
144
|
+
method: 'POST',
|
|
145
|
+
body: payload,
|
|
146
|
+
});
|
|
147
|
+
return { data: result.data, requestId: result.requestId };
|
|
148
|
+
},
|
|
149
|
+
'project.get': async ({ client, payload, flags }) => {
|
|
150
|
+
const id = payloadValue(payload, flags.id, 'id');
|
|
151
|
+
const result = await client.request(`${workbenchPath}/projects/${encodeURIComponent(id)}`, { method: 'GET' });
|
|
152
|
+
return { data: result.data, requestId: result.requestId };
|
|
153
|
+
},
|
|
154
|
+
'project.list': async ({ client, payload }) => {
|
|
155
|
+
const result = await client.request(`${workbenchPath}/projects`, {
|
|
156
|
+
method: 'GET',
|
|
157
|
+
query: pickDefined(payload, ['cursor', 'limit', 'query']),
|
|
158
|
+
});
|
|
159
|
+
return { data: result.data, requestId: result.requestId };
|
|
160
|
+
},
|
|
161
|
+
'project.update': async ({ client, payload, flags }) => {
|
|
162
|
+
const id = projectId(payload, flags.id);
|
|
163
|
+
const result = await client.request(`${workbenchPath}/projects/${encodeURIComponent(id)}`, {
|
|
164
|
+
method: 'PATCH',
|
|
165
|
+
body: omitKeys(payload, ['projectId']),
|
|
166
|
+
});
|
|
167
|
+
return { data: result.data, requestId: result.requestId };
|
|
168
|
+
},
|
|
169
|
+
'record.put': async ({ client, payload, flags }) => {
|
|
170
|
+
const id = projectId(payload, flags.project);
|
|
171
|
+
const recordKey = payloadValue(payload, flags.record, 'recordKey');
|
|
172
|
+
const result = await client.request(`${workbenchPath}/projects/${encodeURIComponent(id)}/records/${encodeURIComponent(recordKey)}`, {
|
|
173
|
+
method: 'PUT',
|
|
174
|
+
body: omitKeys(payload, ['projectId', 'recordKey']),
|
|
175
|
+
});
|
|
176
|
+
return { data: result.data, requestId: result.requestId };
|
|
177
|
+
},
|
|
178
|
+
'record.end-intent': async ({ client, payload, flags }) => {
|
|
179
|
+
const id = projectId(payload, flags.project);
|
|
180
|
+
const recordId = productionRecordId(payload, flags.record);
|
|
181
|
+
const result = await client.request(`${workbenchPath}/projects/${encodeURIComponent(id)}/records/${encodeURIComponent(recordId)}/end-intent`, {
|
|
182
|
+
method: 'POST',
|
|
183
|
+
body: omitKeys(payload, ['projectId', 'productionRecordId', 'recordId']),
|
|
184
|
+
});
|
|
185
|
+
return { data: result.data, requestId: result.requestId };
|
|
186
|
+
},
|
|
187
|
+
'record.reopen': async ({ client, payload, flags }) => {
|
|
188
|
+
const id = projectId(payload, flags.project);
|
|
189
|
+
const recordId = productionRecordId(payload, flags.record);
|
|
190
|
+
const result = await client.request(`${workbenchPath}/projects/${encodeURIComponent(id)}/records/${encodeURIComponent(recordId)}/reopen`, {
|
|
191
|
+
method: 'POST',
|
|
192
|
+
body: omitKeys(payload, ['projectId', 'productionRecordId', 'recordId']),
|
|
193
|
+
});
|
|
194
|
+
return { data: result.data, requestId: result.requestId };
|
|
195
|
+
},
|
|
196
|
+
'annotation.put': async ({ client, payload, flags }) => {
|
|
197
|
+
const id = projectId(payload, flags.project);
|
|
198
|
+
const result = await client.request(`${workbenchPath}/projects/${encodeURIComponent(id)}/annotations`, {
|
|
199
|
+
method: 'POST',
|
|
200
|
+
body: omitKeys(payload, ['projectId']),
|
|
201
|
+
});
|
|
202
|
+
return { data: result.data, requestId: result.requestId };
|
|
203
|
+
},
|
|
204
|
+
'artifact.list': async ({ client, payload, flags }) => {
|
|
205
|
+
const id = projectId(payload, flags.project);
|
|
206
|
+
const result = await client.request(`${workbenchPath}/projects/${encodeURIComponent(id)}/artifacts`, {
|
|
207
|
+
method: 'GET', query: pickDefined(payload, ['kind', 'stageKey', 'query', 'cursor', 'limit']),
|
|
208
|
+
});
|
|
209
|
+
return { data: result.data, requestId: result.requestId };
|
|
210
|
+
},
|
|
211
|
+
'artifact.versions': async ({ client, payload, flags }) => {
|
|
212
|
+
const id = projectId(payload, flags.project);
|
|
213
|
+
const result = await client.request(`${workbenchPath}/projects/${encodeURIComponent(id)}/artifact-versions`, {
|
|
214
|
+
method: 'GET', query: pickDefined(payload, ['artifactKey', 'idempotencyKey', 'cursor', 'limit']),
|
|
215
|
+
});
|
|
216
|
+
return { data: result.data, requestId: result.requestId };
|
|
217
|
+
},
|
|
218
|
+
'blob.put': async (context) => {
|
|
219
|
+
projectId(context.payload, context.flags.project);
|
|
220
|
+
const local = localContent(context.payload);
|
|
221
|
+
if (!local)
|
|
222
|
+
throw new CliCommandError('blob put requires local content.sourcePath', 'VALIDATION_ERROR');
|
|
223
|
+
return { data: await uploadLocalContent(context, local.sourcePath, local.content) };
|
|
224
|
+
},
|
|
225
|
+
submit: async (context) => submitArtifact(context, context.payload),
|
|
226
|
+
sync: async (context) => {
|
|
227
|
+
const manifest = await readManifest(context);
|
|
228
|
+
const items = manifest.items;
|
|
229
|
+
if (!Array.isArray(items))
|
|
230
|
+
throw new CliCommandError('--manifest items must be an array', 'VALIDATION_ERROR');
|
|
231
|
+
const results = [];
|
|
232
|
+
for (const item of items) {
|
|
233
|
+
const entry = ensureObject(item, 'manifest item');
|
|
234
|
+
const type = typeof entry.type === 'string' ? entry.type : 'artifact';
|
|
235
|
+
const itemPayload = entry.submission === undefined
|
|
236
|
+
? omitKeys(entry, ['type'])
|
|
237
|
+
: ensureObject(entry.submission, 'manifest item submission');
|
|
238
|
+
const merged = { ...itemPayload, projectId: itemPayload.projectId || manifest.projectId || context.flags.project };
|
|
239
|
+
if (type === 'artifact')
|
|
240
|
+
results.push((await submitArtifact(context, merged)).data);
|
|
241
|
+
else if (type === 'annotation') {
|
|
242
|
+
const id = projectId(merged, context.flags.project);
|
|
243
|
+
results.push((await context.client.request(`${workbenchPath}/projects/${encodeURIComponent(id)}/annotations`, {
|
|
244
|
+
method: 'POST', body: omitKeys(merged, ['projectId']), timeoutMs: context.flags.timeoutMs,
|
|
245
|
+
})).data);
|
|
246
|
+
}
|
|
247
|
+
else
|
|
248
|
+
throw new CliCommandError(`Unsupported sync item type: ${type}`, 'VALIDATION_ERROR');
|
|
249
|
+
}
|
|
250
|
+
return { data: { items: results, nextCursor: null } };
|
|
251
|
+
},
|
|
252
|
+
'archive.prepare': async (context) => {
|
|
253
|
+
const wrapper = await readManifest(context);
|
|
254
|
+
const manifest = ensureObject(wrapper.manifest || wrapper, 'manifest');
|
|
255
|
+
const id = projectId(manifest, context.flags.project);
|
|
256
|
+
const recordKey = payloadValue(manifest, context.flags.record, 'productionRecordId');
|
|
257
|
+
const revision = getRequiredInt(manifest, 'revision');
|
|
258
|
+
const manifestHash = payloadValue(wrapper, context.flags.manifestHash, 'manifestHash');
|
|
259
|
+
const result = await context.client.request(`${workbenchPath}/projects/${encodeURIComponent(id)}/records/${encodeURIComponent(recordKey)}/manifests/${revision}`, {
|
|
260
|
+
method: 'PUT',
|
|
261
|
+
body: {
|
|
262
|
+
manifest,
|
|
263
|
+
manifestHash,
|
|
264
|
+
expectedPreviousRevision: wrapper.expectedPreviousRevision,
|
|
265
|
+
},
|
|
266
|
+
timeoutMs: context.flags.timeoutMs,
|
|
267
|
+
});
|
|
268
|
+
return { data: result.data, requestId: result.requestId };
|
|
269
|
+
},
|
|
270
|
+
'archive.finalize': async ({ client, payload, flags }) => {
|
|
271
|
+
const id = projectId(payload, flags.project);
|
|
272
|
+
const recordId = productionRecordId(payload, flags.record);
|
|
273
|
+
const revision = getRevision(payload, flags.revision);
|
|
274
|
+
const manifestHash = payloadValue(payload, flags.manifestHash, 'manifestHash');
|
|
275
|
+
const result = await client.request(`${workbenchPath}/projects/${encodeURIComponent(id)}/records/${encodeURIComponent(recordId)}/manifests/${revision}/finalize`, {
|
|
276
|
+
method: 'POST',
|
|
277
|
+
body: { manifestHash, ...pickDefined(payload, ['archiveGeneration']) },
|
|
278
|
+
});
|
|
279
|
+
return { data: result.data, requestId: result.requestId };
|
|
280
|
+
},
|
|
281
|
+
'archive.status': async ({ client, payload, flags }) => {
|
|
282
|
+
const id = projectId(payload, flags.project);
|
|
283
|
+
const recordId = productionRecordId(payload, flags.record);
|
|
284
|
+
const result = await client.request(`${workbenchPath}/projects/${encodeURIComponent(id)}/records/${encodeURIComponent(recordId)}/archive`, { method: 'GET' });
|
|
285
|
+
return { data: result.data, requestId: result.requestId };
|
|
286
|
+
},
|
|
287
|
+
'archive.download': async ({ client, payload, flags }) => {
|
|
288
|
+
const id = projectId(payload, flags.project);
|
|
289
|
+
const recordId = productionRecordId(payload, flags.record);
|
|
290
|
+
const revision = getRevision(payload, flags.revision);
|
|
291
|
+
const entryId = getRequiredString(payload, 'entryId');
|
|
292
|
+
if (!flags.output)
|
|
293
|
+
throw new CliCommandError('Missing --output', 'VALIDATION_ERROR');
|
|
294
|
+
const result = await client.requestBinary(`${workbenchPath}/projects/${encodeURIComponent(id)}/records/${encodeURIComponent(recordId)}/manifests/${revision}/files/${encodeURIComponent(entryId)}/content`, {
|
|
295
|
+
method: 'GET', timeoutMs: flags.timeoutMs,
|
|
296
|
+
});
|
|
297
|
+
try {
|
|
298
|
+
await writeFile(flags.output, result.data);
|
|
299
|
+
}
|
|
300
|
+
catch (error) {
|
|
301
|
+
throw new CliCommandError(`Cannot write --output: ${error?.message || error}`, 'VALIDATION_ERROR');
|
|
302
|
+
}
|
|
303
|
+
return { data: { output: flags.output, size: result.data.byteLength, contentType: result.contentType }, requestId: result.requestId };
|
|
304
|
+
},
|
|
305
|
+
};
|
package/dist/index.js
CHANGED
|
@@ -7,6 +7,7 @@ import { workflowCommands } from './commands/workflow.js';
|
|
|
7
7
|
import { characterCommands } from './commands/character.js';
|
|
8
8
|
import { aiGatewayCommands } from './commands/aiGateway.js';
|
|
9
9
|
import { playtestCommands } from './commands/playtest.js';
|
|
10
|
+
import { workbenchCommands } from './commands/workbench.js';
|
|
10
11
|
import { CliCommandError } from './lib/errors.js';
|
|
11
12
|
import { getCliHelpText, parseCliArgs, resolveCliConfig } from './lib/flags.js';
|
|
12
13
|
import { CliHttpClient } from './lib/http.js';
|
|
@@ -22,6 +23,7 @@ const commandRegistry = {
|
|
|
22
23
|
character: characterCommands,
|
|
23
24
|
'ai-gateway': aiGatewayCommands,
|
|
24
25
|
playtest: playtestCommands,
|
|
26
|
+
workbench: workbenchCommands,
|
|
25
27
|
};
|
|
26
28
|
function normalizeError(error) {
|
|
27
29
|
if (error instanceof CliCommandError) {
|
|
@@ -53,6 +55,16 @@ function writeError(runtime, command, raw, error) {
|
|
|
53
55
|
meta: { command },
|
|
54
56
|
});
|
|
55
57
|
}
|
|
58
|
+
async function readJsonFile(runtime, filePath, flag) {
|
|
59
|
+
try {
|
|
60
|
+
return ensureObject(JSON.parse(await runtime.readTextFile(filePath)));
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
if (error instanceof CliCommandError)
|
|
64
|
+
throw error;
|
|
65
|
+
throw new CliCommandError(`Invalid JSON in ${flag}: ${error?.message || error}`, 'VALIDATION_ERROR');
|
|
66
|
+
}
|
|
67
|
+
}
|
|
56
68
|
export async function main(argv, runtime = createProcessRuntime()) {
|
|
57
69
|
let commandName = '';
|
|
58
70
|
let rawOutput = false;
|
|
@@ -79,7 +91,9 @@ export async function main(argv, runtime = createProcessRuntime()) {
|
|
|
79
91
|
throw new CliCommandError(`Invalid stdin JSON: ${error?.message || error}`, 'VALIDATION_ERROR');
|
|
80
92
|
}
|
|
81
93
|
}
|
|
82
|
-
const
|
|
94
|
+
const filePayload = parsed.flags.file ? await readJsonFile(runtime, parsed.flags.file, '--file') : {};
|
|
95
|
+
const mergedPayload = { ...filePayload, ...stdinPayload };
|
|
96
|
+
const { baseUrl, token, commandPayload } = await resolveCliConfig(mergedPayload, parsed.flags, runtime);
|
|
83
97
|
const client = new CliHttpClient(baseUrl, token, runtime.fetchImpl);
|
|
84
98
|
const result = await command({
|
|
85
99
|
payload: commandPayload,
|
package/dist/lib/flags.d.ts
CHANGED
|
@@ -10,6 +10,14 @@ export type CliFlags = {
|
|
|
10
10
|
baseUrlArg: string;
|
|
11
11
|
tokenArg: string;
|
|
12
12
|
tokenFile: string;
|
|
13
|
+
file: string;
|
|
14
|
+
manifest: string;
|
|
15
|
+
id: string;
|
|
16
|
+
project: string;
|
|
17
|
+
record: string;
|
|
18
|
+
revision: string;
|
|
19
|
+
manifestHash: string;
|
|
20
|
+
output: string;
|
|
13
21
|
};
|
|
14
22
|
export type ParsedCliArgs = {
|
|
15
23
|
group: string;
|
package/dist/lib/flags.js
CHANGED
|
@@ -38,6 +38,14 @@ export function parseCliArgs(argv) {
|
|
|
38
38
|
baseUrlArg: '',
|
|
39
39
|
tokenArg: '',
|
|
40
40
|
tokenFile: '',
|
|
41
|
+
file: '',
|
|
42
|
+
manifest: '',
|
|
43
|
+
id: '',
|
|
44
|
+
project: '',
|
|
45
|
+
record: '',
|
|
46
|
+
revision: '',
|
|
47
|
+
manifestHash: '',
|
|
48
|
+
output: '',
|
|
41
49
|
};
|
|
42
50
|
for (let index = 0; index < argv.length; index += 1) {
|
|
43
51
|
const arg = argv[index];
|
|
@@ -81,6 +89,38 @@ export function parseCliArgs(argv) {
|
|
|
81
89
|
flags.tokenFile = readValue(argv, index, arg).trim();
|
|
82
90
|
index += 1;
|
|
83
91
|
break;
|
|
92
|
+
case '--file':
|
|
93
|
+
flags.file = readValue(argv, index, arg).trim();
|
|
94
|
+
index += 1;
|
|
95
|
+
break;
|
|
96
|
+
case '--manifest':
|
|
97
|
+
flags.manifest = readValue(argv, index, arg).trim();
|
|
98
|
+
index += 1;
|
|
99
|
+
break;
|
|
100
|
+
case '--id':
|
|
101
|
+
flags.id = readValue(argv, index, arg).trim();
|
|
102
|
+
index += 1;
|
|
103
|
+
break;
|
|
104
|
+
case '--project':
|
|
105
|
+
flags.project = readValue(argv, index, arg).trim();
|
|
106
|
+
index += 1;
|
|
107
|
+
break;
|
|
108
|
+
case '--record':
|
|
109
|
+
flags.record = readValue(argv, index, arg).trim();
|
|
110
|
+
index += 1;
|
|
111
|
+
break;
|
|
112
|
+
case '--revision':
|
|
113
|
+
flags.revision = readValue(argv, index, arg).trim();
|
|
114
|
+
index += 1;
|
|
115
|
+
break;
|
|
116
|
+
case '--manifest-hash':
|
|
117
|
+
flags.manifestHash = readValue(argv, index, arg).trim();
|
|
118
|
+
index += 1;
|
|
119
|
+
break;
|
|
120
|
+
case '--output':
|
|
121
|
+
flags.output = readValue(argv, index, arg).trim();
|
|
122
|
+
index += 1;
|
|
123
|
+
break;
|
|
84
124
|
default:
|
|
85
125
|
throw new CliCommandError(`Unknown flag: ${arg}`, 'VALIDATION_ERROR');
|
|
86
126
|
}
|
|
@@ -93,7 +133,7 @@ export function parseCliArgs(argv) {
|
|
|
93
133
|
}
|
|
94
134
|
export function getCliHelpText() {
|
|
95
135
|
return [
|
|
96
|
-
'Usage: branch-video-agent <group> <action> [--
|
|
136
|
+
'Usage: branch-video-agent <group> <action> [--file <json>] [--manifest <json>] [--raw] [--timeout <ms>] [--base-url <url>] [--token-file <path>]',
|
|
97
137
|
'',
|
|
98
138
|
'Groups:',
|
|
99
139
|
' preset list | upsert',
|
|
@@ -106,6 +146,7 @@ export function getCliHelpText() {
|
|
|
106
146
|
' ai-gateway capabilities | request | media-understand | image-generate | image-edit | image-get | video-create | video-get | seed-audio-create | seed-audio-get | music-submit | music-get | tts-create | tts-get',
|
|
107
147
|
' Images: --async (or --wait) requires stdin idempotencyKey; image-get accepts taskId or idempotencyKey. Recovery receipts go to stderr; stdout remains one JSON document.',
|
|
108
148
|
' playtest run',
|
|
149
|
+
' workbench project create|get|list|update | record put|end-intent|reopen | submit | sync | annotation put | archive prepare|finalize|status|download',
|
|
109
150
|
'',
|
|
110
151
|
'Config precedence:',
|
|
111
152
|
' AIHUB_AGENT_TOKEN / BRANCH_VIDEO_AGENT_BASE_URL > stdin JSON or --token-file > command flags',
|
package/dist/lib/http.d.ts
CHANGED
|
@@ -9,6 +9,18 @@ type RequestResult<T> = {
|
|
|
9
9
|
status: number;
|
|
10
10
|
requestId: string | null;
|
|
11
11
|
};
|
|
12
|
+
type StreamRequestOptions = {
|
|
13
|
+
method: string;
|
|
14
|
+
body: ReadableStream<Uint8Array>;
|
|
15
|
+
contentType: string;
|
|
16
|
+
timeoutMs?: number;
|
|
17
|
+
};
|
|
18
|
+
type BinaryRequestResult = {
|
|
19
|
+
data: Uint8Array;
|
|
20
|
+
status: number;
|
|
21
|
+
requestId: string | null;
|
|
22
|
+
contentType: string | null;
|
|
23
|
+
};
|
|
12
24
|
export declare class CliHttpClient {
|
|
13
25
|
private readonly baseUrl;
|
|
14
26
|
private readonly token;
|
|
@@ -16,5 +28,10 @@ export declare class CliHttpClient {
|
|
|
16
28
|
constructor(baseUrl: string, token: string, fetchImpl: typeof fetch);
|
|
17
29
|
resolveUrl(path: string): string;
|
|
18
30
|
request<T>(path: string, options: RequestOptions): Promise<RequestResult<T>>;
|
|
31
|
+
requestStream<T>(path: string, options: StreamRequestOptions): Promise<RequestResult<T>>;
|
|
32
|
+
requestBinary(path: string, options: {
|
|
33
|
+
method: string;
|
|
34
|
+
timeoutMs?: number;
|
|
35
|
+
}): Promise<BinaryRequestResult>;
|
|
19
36
|
}
|
|
20
37
|
export {};
|
package/dist/lib/http.js
CHANGED
|
@@ -17,6 +17,21 @@ function extractRequestId(response, parsed) {
|
|
|
17
17
|
|| response.headers.get('x-ai-gateway-request-id')
|
|
18
18
|
|| (typeof parsed?.requestId === 'string' ? parsed.requestId : null);
|
|
19
19
|
}
|
|
20
|
+
function parseResponseError(parsed, response) {
|
|
21
|
+
const error = typeof parsed === 'object' && parsed ? parsed?.error : undefined;
|
|
22
|
+
const nested = typeof error === 'object' && error ? error : undefined;
|
|
23
|
+
const message = typeof nested?.message === 'string'
|
|
24
|
+
? nested.message
|
|
25
|
+
: typeof error === 'string'
|
|
26
|
+
? error
|
|
27
|
+
: response.statusText || 'Request failed';
|
|
28
|
+
const code = typeof nested?.code === 'string'
|
|
29
|
+
? nested.code
|
|
30
|
+
: typeof parsed?.code === 'string'
|
|
31
|
+
? String(parsed.code)
|
|
32
|
+
: `HTTP_${response.status}`;
|
|
33
|
+
return { message, code };
|
|
34
|
+
}
|
|
20
35
|
export class CliHttpClient {
|
|
21
36
|
baseUrl;
|
|
22
37
|
token;
|
|
@@ -61,13 +76,8 @@ export class CliHttpClient {
|
|
|
61
76
|
: null;
|
|
62
77
|
const requestId = extractRequestId(response, parsed);
|
|
63
78
|
if (!response.ok) {
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
: response.statusText || 'Request failed';
|
|
67
|
-
const errorCode = typeof parsed === 'object' && parsed && typeof parsed.code === 'string'
|
|
68
|
-
? String(parsed.code)
|
|
69
|
-
: `HTTP_${response.status}`;
|
|
70
|
-
throw new CliCommandError(errorMessage, errorCode, parsed, requestId, response.status);
|
|
79
|
+
const error = parseResponseError(parsed, response);
|
|
80
|
+
throw new CliCommandError(error.message, error.code, parsed, requestId, response.status);
|
|
71
81
|
}
|
|
72
82
|
return {
|
|
73
83
|
data: parsed,
|
|
@@ -75,4 +85,68 @@ export class CliHttpClient {
|
|
|
75
85
|
requestId,
|
|
76
86
|
};
|
|
77
87
|
}
|
|
88
|
+
async requestStream(path, options) {
|
|
89
|
+
let response;
|
|
90
|
+
try {
|
|
91
|
+
response = await this.fetchImpl(buildUrl(this.baseUrl, path), {
|
|
92
|
+
method: options.method,
|
|
93
|
+
headers: {
|
|
94
|
+
Authorization: `Bearer ${this.token}`,
|
|
95
|
+
'content-type': options.contentType,
|
|
96
|
+
},
|
|
97
|
+
body: options.body,
|
|
98
|
+
duplex: 'half',
|
|
99
|
+
// A redirect to another origin must never receive the bearer token.
|
|
100
|
+
redirect: 'error',
|
|
101
|
+
signal: options.timeoutMs ? AbortSignal.timeout(options.timeoutMs) : undefined,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
throw new CliCommandError(error?.message || 'Network request failed', 'NETWORK_ERROR');
|
|
106
|
+
}
|
|
107
|
+
const text = await response.text();
|
|
108
|
+
const parsed = text
|
|
109
|
+
? (() => {
|
|
110
|
+
try {
|
|
111
|
+
return JSON.parse(text);
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
return text;
|
|
115
|
+
}
|
|
116
|
+
})()
|
|
117
|
+
: null;
|
|
118
|
+
const requestId = extractRequestId(response, parsed);
|
|
119
|
+
if (!response.ok) {
|
|
120
|
+
const error = parseResponseError(parsed, response);
|
|
121
|
+
throw new CliCommandError(error.message, error.code, parsed, requestId, response.status);
|
|
122
|
+
}
|
|
123
|
+
return { data: parsed, status: response.status, requestId };
|
|
124
|
+
}
|
|
125
|
+
async requestBinary(path, options) {
|
|
126
|
+
let response;
|
|
127
|
+
try {
|
|
128
|
+
response = await this.fetchImpl(buildUrl(this.baseUrl, path), {
|
|
129
|
+
method: options.method,
|
|
130
|
+
headers: { Authorization: `Bearer ${this.token}` },
|
|
131
|
+
redirect: 'error',
|
|
132
|
+
signal: options.timeoutMs ? AbortSignal.timeout(options.timeoutMs) : undefined,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
catch (error) {
|
|
136
|
+
throw new CliCommandError(error?.message || 'Network request failed', 'NETWORK_ERROR');
|
|
137
|
+
}
|
|
138
|
+
const bytes = new Uint8Array(await response.arrayBuffer());
|
|
139
|
+
const requestId = response.headers.get('x-request-id') || response.headers.get('x-correlation-id');
|
|
140
|
+
if (!response.ok) {
|
|
141
|
+
const text = new TextDecoder().decode(bytes);
|
|
142
|
+
let parsed = text;
|
|
143
|
+
try {
|
|
144
|
+
parsed = JSON.parse(text);
|
|
145
|
+
}
|
|
146
|
+
catch { /* preserve plain error body */ }
|
|
147
|
+
const error = parseResponseError(parsed, response);
|
|
148
|
+
throw new CliCommandError(error.message, error.code, parsed, requestId, response.status);
|
|
149
|
+
}
|
|
150
|
+
return { data: bytes, status: response.status, requestId, contentType: response.headers.get('content-type') };
|
|
151
|
+
}
|
|
78
152
|
}
|