@neta-art/cohub-cli 1.20.6 → 2.0.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 +17 -1
- package/dist/commands/generations.js +52 -6
- package/dist/commands/models.js +3 -0
- package/dist/commands/works.js +77 -17
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -149,6 +149,16 @@ cohub generate "restyle this image" \
|
|
|
149
149
|
--param size=1024x1024 \
|
|
150
150
|
--json
|
|
151
151
|
|
|
152
|
+
cohub generate "smoothly transition from the first frame to the last frame" \
|
|
153
|
+
--model seedance-2-0-fast \
|
|
154
|
+
--image first_frame=https://example.com/first.png \
|
|
155
|
+
--image last_frame=https://example.com/last.png
|
|
156
|
+
|
|
157
|
+
cohub generate "keep the character identity from all reference images" \
|
|
158
|
+
--model seedance-2-0-fast \
|
|
159
|
+
--image reference_image=https://example.com/reference-1.png \
|
|
160
|
+
--image reference_image=https://example.com/reference-2.png
|
|
161
|
+
|
|
152
162
|
cohub generate "a calm lake" \
|
|
153
163
|
--model <model> \
|
|
154
164
|
--async
|
|
@@ -160,10 +170,16 @@ Supported inputs:
|
|
|
160
170
|
|
|
161
171
|
```bash
|
|
162
172
|
--image <path-or-url>
|
|
173
|
+
--image first_frame=<path-or-url>
|
|
174
|
+
--image last_frame=<path-or-url>
|
|
175
|
+
--image reference_image=<path-or-url>
|
|
163
176
|
--video <path-or-url>
|
|
177
|
+
--video reference_video=<path-or-url>
|
|
164
178
|
--audio <path-or-url>
|
|
165
179
|
```
|
|
166
180
|
|
|
181
|
+
Role-qualified media values add `meta.role` to that content block. Repeat `--image reference_image=...` for multiple reference images. Seedance role-qualified media should use public URL inputs. Do not mix first/last frame roles with reference roles in one request.
|
|
182
|
+
|
|
167
183
|
Pass generation parameters with `--param key=value` or `--parameters '<json>'`.
|
|
168
184
|
|
|
169
185
|
## Files
|
|
@@ -191,7 +207,7 @@ cohub works get <workId> --json
|
|
|
191
207
|
cohub -s <spaceId> works publish demo --file dist/index.html
|
|
192
208
|
cohub -s <spaceId> works publish site --dir dist
|
|
193
209
|
cohub -s <spaceId> works publish app --port 3000
|
|
194
|
-
cohub works
|
|
210
|
+
cohub works publish-version <workId>
|
|
195
211
|
cohub works versions <workId> --json
|
|
196
212
|
cohub works rm <workId> --yes
|
|
197
213
|
```
|
|
@@ -4,6 +4,14 @@ import { GenerationPolicyError, assertGenerationRequestAllowedByPolicy, parseGen
|
|
|
4
4
|
import { createClient } from "../client.js";
|
|
5
5
|
import { resolveSpace } from "../space.js";
|
|
6
6
|
import { json as outJson, jsonRequested, ok, error, handleHttp, spinner } from "../output.js";
|
|
7
|
+
const frameMediaRoles = new Set(["first_frame", "last_frame"]);
|
|
8
|
+
const referenceMediaRoles = new Set(["reference_image", "reference_video"]);
|
|
9
|
+
const rolesByMediaType = {
|
|
10
|
+
image: new Set([...frameMediaRoles, "reference_image"]),
|
|
11
|
+
video: new Set(["reference_video"]),
|
|
12
|
+
audio: new Set(),
|
|
13
|
+
};
|
|
14
|
+
const mediaRoles = new Set([...frameMediaRoles, ...referenceMediaRoles]);
|
|
7
15
|
const mimeByExt = {
|
|
8
16
|
".png": "image/png",
|
|
9
17
|
".jpg": "image/jpeg",
|
|
@@ -45,12 +53,47 @@ function parseParams(param, parameters) {
|
|
|
45
53
|
}
|
|
46
54
|
return Object.keys(result).length > 0 ? result : undefined;
|
|
47
55
|
}
|
|
48
|
-
async function
|
|
49
|
-
|
|
50
|
-
|
|
56
|
+
async function pathExists(path) {
|
|
57
|
+
return Boolean(await stat(path).catch(() => null));
|
|
58
|
+
}
|
|
59
|
+
async function parseMediaInput(type, rawValue) {
|
|
60
|
+
const separator = rawValue.indexOf("=");
|
|
61
|
+
if (separator <= 0)
|
|
62
|
+
return { value: rawValue };
|
|
63
|
+
const role = rawValue.slice(0, separator).trim();
|
|
64
|
+
if (!mediaRoles.has(role))
|
|
65
|
+
return { value: rawValue };
|
|
66
|
+
if (await pathExists(rawValue))
|
|
67
|
+
return { value: rawValue };
|
|
68
|
+
if (!rolesByMediaType[type].has(role)) {
|
|
69
|
+
return error("Invalid media role", `${role} cannot be used with --${type}`);
|
|
70
|
+
}
|
|
71
|
+
const value = rawValue.slice(separator + 1).trim();
|
|
72
|
+
if (!value)
|
|
73
|
+
return error("Invalid media input", `--${type} ${role}= requires a path or URL`);
|
|
74
|
+
return { value, role };
|
|
75
|
+
}
|
|
76
|
+
async function contentFromPathOrUrl(type, rawValue) {
|
|
77
|
+
const { value, role } = await parseMediaInput(type, rawValue);
|
|
78
|
+
const meta = role ? { role } : undefined;
|
|
79
|
+
if (/^https?:\/\//.test(value)) {
|
|
80
|
+
return { type, source: { type: "url", url: value }, ...(meta ? { meta } : {}) };
|
|
81
|
+
}
|
|
51
82
|
const data = await readFile(value);
|
|
52
83
|
const mediaType = mimeByExt[extname(value).toLowerCase()] ?? "application/octet-stream";
|
|
53
|
-
return {
|
|
84
|
+
return {
|
|
85
|
+
type,
|
|
86
|
+
source: { type: "base64", mediaType, data: data.toString("base64") },
|
|
87
|
+
...(meta ? { meta } : {}),
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
function validateMediaRoleModes(content) {
|
|
91
|
+
const roles = content.map(metaRole).filter((role) => Boolean(role));
|
|
92
|
+
const hasFrameRole = roles.some((role) => frameMediaRoles.has(role));
|
|
93
|
+
const hasReferenceRole = roles.some((role) => referenceMediaRoles.has(role));
|
|
94
|
+
if (hasFrameRole && hasReferenceRole) {
|
|
95
|
+
error("Invalid media role mix", "Use first_frame/last_frame or reference_image/reference_video, not both.");
|
|
96
|
+
}
|
|
54
97
|
}
|
|
55
98
|
async function saveOutputs(output, outputPath) {
|
|
56
99
|
const outputs = output.filter((block) => block.type === "text" || block.type === "image" || block.type === "video" || block.type === "audio");
|
|
@@ -212,8 +255,8 @@ export function registerGenerations(program) {
|
|
|
212
255
|
.description("Generate multimodal outputs")
|
|
213
256
|
.argument("<prompt>", "Prompt text")
|
|
214
257
|
.requiredOption("-m, --model <model>", "Multimodal model ID from `cohub models ls --model-type multimodal`")
|
|
215
|
-
.option("--image <path-or-url>", "Image input file path or URL; repeatable", collect, [])
|
|
216
|
-
.option("--video <path-or-url>", "Video input file path or URL; repeatable", collect, [])
|
|
258
|
+
.option("--image <path-or-url>", "Image input file path or URL; prefix with first_frame=, last_frame=, or reference_image= when needed; repeatable", collect, [])
|
|
259
|
+
.option("--video <path-or-url>", "Video input file path or URL; prefix with reference_video= when needed; repeatable", collect, [])
|
|
217
260
|
.option("--audio <path-or-url>", "Audio input file path or URL; repeatable", collect, [])
|
|
218
261
|
.option("--param <key=value>", "Generation parameter; repeatable, values may be JSON/number/boolean", collect, [])
|
|
219
262
|
.option("--parameters <json>", "Generation parameters as a JSON object")
|
|
@@ -228,6 +271,8 @@ Examples:
|
|
|
228
271
|
cohub models ls --model-type multimodal
|
|
229
272
|
cohub -s <space-id> generate "A calm lake at sunrise" -m <model> -o lake.png
|
|
230
273
|
COHUB_SPACE_ID=<space-id> cohub generate "Restyle this image" -m <model> --image input.png
|
|
274
|
+
cohub -s <space-id> generate "Smooth transition" -m seedance-2-0-fast --image first_frame=https://example.com/first.png --image last_frame=https://example.com/last.png
|
|
275
|
+
cohub -s <space-id> generate "Use these references" -m seedance-2-0-fast --image reference_image=https://example.com/a.png --image reference_image=https://example.com/b.png
|
|
231
276
|
cohub -s <space-id> generate "A calm lake" -m <model> --async
|
|
232
277
|
`)
|
|
233
278
|
.action(async (prompt, opts) => {
|
|
@@ -237,6 +282,7 @@ Examples:
|
|
|
237
282
|
content.push(...await Promise.all(opts.image.map((value) => contentFromPathOrUrl("image", value))));
|
|
238
283
|
content.push(...await Promise.all(opts.video.map((value) => contentFromPathOrUrl("video", value))));
|
|
239
284
|
content.push(...await Promise.all(opts.audio.map((value) => contentFromPathOrUrl("audio", value))));
|
|
285
|
+
validateMediaRoleModes(content);
|
|
240
286
|
const parameters = parseParams(opts.param, opts.parameters);
|
|
241
287
|
try {
|
|
242
288
|
assertGenerationRequestAllowedByPolicy({
|
package/dist/commands/models.js
CHANGED
|
@@ -17,6 +17,7 @@ function printSection(title, lines) {
|
|
|
17
17
|
}
|
|
18
18
|
function formatContentSpec(spec) {
|
|
19
19
|
const details = [];
|
|
20
|
+
const roles = spec.roles;
|
|
20
21
|
details.push(spec.required === false ? "optional" : "required");
|
|
21
22
|
if (typeof spec.min === "number")
|
|
22
23
|
details.push(`min ${spec.min}`);
|
|
@@ -24,6 +25,8 @@ function formatContentSpec(spec) {
|
|
|
24
25
|
details.push(`max ${spec.max}`);
|
|
25
26
|
if (spec.sources?.length)
|
|
26
27
|
details.push(`sources: ${spec.sources.join(", ")}`);
|
|
28
|
+
if (roles?.length)
|
|
29
|
+
details.push(`roles: ${roles.join(", ")}`);
|
|
27
30
|
if (spec.merge)
|
|
28
31
|
details.push(`merge: ${spec.merge}`);
|
|
29
32
|
if (spec.description)
|
package/dist/commands/works.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import { HttpError } from "@neta-art/cohub";
|
|
1
2
|
import { createClient } from "../client.js";
|
|
2
3
|
import { error, handleHttp, json as outJson, jsonRequested, ok, table } from "../output.js";
|
|
3
4
|
import { resolveSpace } from "../space.js";
|
|
4
|
-
const WORK_STATUSES = ["
|
|
5
|
+
const WORK_STATUSES = ["published", "disabled"];
|
|
6
|
+
const WORK_VISIBILITIES = ["public", "space"];
|
|
5
7
|
const collectOption = (value, previous = []) => [...previous, value];
|
|
6
8
|
function parseChoice(value, name, choices) {
|
|
7
9
|
if (choices.includes(value))
|
|
@@ -54,16 +56,20 @@ function resolveTarget(opts) {
|
|
|
54
56
|
return targets[0] ?? null;
|
|
55
57
|
}
|
|
56
58
|
function resolveStatus(opts) {
|
|
57
|
-
const values = [opts.status, opts.
|
|
59
|
+
const values = [opts.status, opts.disabled ? "disabled" : undefined].filter(Boolean);
|
|
58
60
|
if (values.length > 1)
|
|
59
|
-
return error("Conflicting status", "Use only one of --status
|
|
61
|
+
return error("Conflicting status", "Use only one of --status or --disabled");
|
|
60
62
|
return values[0] ? parseChoice(values[0], "status", WORK_STATUSES) : "published";
|
|
61
63
|
}
|
|
64
|
+
function resolveVisibility(value) {
|
|
65
|
+
return value ? parseChoice(value, "visibility", WORK_VISIBILITIES) : undefined;
|
|
66
|
+
}
|
|
62
67
|
function printWork(work) {
|
|
63
68
|
table([work], [
|
|
64
69
|
{ key: "id", label: "ID" },
|
|
65
70
|
{ key: "slug", label: "Slug" },
|
|
66
71
|
{ key: "status", label: "Status" },
|
|
72
|
+
{ key: "visibility", label: "Visibility" },
|
|
67
73
|
{ key: "targetType", label: "Target" },
|
|
68
74
|
{ key: "targetRef", label: "Ref" },
|
|
69
75
|
{ key: "latestVersion", label: "Version" },
|
|
@@ -93,6 +99,19 @@ async function confirmDelete(opts) {
|
|
|
93
99
|
if (answer !== "y" && answer !== "yes")
|
|
94
100
|
return error("Cancelled");
|
|
95
101
|
}
|
|
102
|
+
async function publishWorkVersion(id, opts) {
|
|
103
|
+
const client = createClient();
|
|
104
|
+
try {
|
|
105
|
+
const result = await client.works.publishVersion(id);
|
|
106
|
+
if (jsonRequested(opts))
|
|
107
|
+
return outJson(result);
|
|
108
|
+
ok(`Work version updated: v${result.version.version}`);
|
|
109
|
+
printWork(result.work);
|
|
110
|
+
}
|
|
111
|
+
catch (e) {
|
|
112
|
+
handleHttp(e);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
96
115
|
export function registerWorks(program) {
|
|
97
116
|
const worksCmd = program.command("works").description("Work management");
|
|
98
117
|
worksCmd
|
|
@@ -111,6 +130,7 @@ export function registerWorks(program) {
|
|
|
111
130
|
{ key: "id", label: "ID" },
|
|
112
131
|
{ key: "slug", label: "Slug" },
|
|
113
132
|
{ key: "status", label: "Status" },
|
|
133
|
+
{ key: "visibility", label: "Visibility" },
|
|
114
134
|
{ key: "targetType", label: "Target" },
|
|
115
135
|
{ key: "targetRef", label: "Ref" },
|
|
116
136
|
{ key: "latestVersion", label: "Version" },
|
|
@@ -167,9 +187,9 @@ export function registerWorks(program) {
|
|
|
167
187
|
.option("--file <path>", "Publish a HTML file")
|
|
168
188
|
.option("--dir <path>", "Publish a directory site")
|
|
169
189
|
.option("--port <port>", "Publish a public sandbox port")
|
|
170
|
-
.option("--draft", "Create as draft")
|
|
171
190
|
.option("--disabled", "Create as disabled")
|
|
172
|
-
.option("--status <status>", "Work status:
|
|
191
|
+
.option("--status <status>", "Work status: published, disabled")
|
|
192
|
+
.option("--visibility <visibility>", "Work visibility: public, space")
|
|
173
193
|
.option("--work-scope <scope>", "Scope granted to the work runtime (space.view, session.view, file.view, taskrun.view)", collectOption, [])
|
|
174
194
|
.option("--viewer-scope <scope>", "Scope viewers may request (session.prompt.readonly, session.prompt.fullaccess, generation.create, user.space.list, user.session.list, user.usage.read)", collectOption, [])
|
|
175
195
|
.option("--meta <json>", "Work metadata as a JSON object")
|
|
@@ -194,6 +214,7 @@ export function registerWorks(program) {
|
|
|
194
214
|
spaceId,
|
|
195
215
|
slug,
|
|
196
216
|
status,
|
|
217
|
+
visibility: resolveVisibility(opts.visibility),
|
|
197
218
|
targetType: target.targetType,
|
|
198
219
|
targetRef: target.targetRef,
|
|
199
220
|
workScopes: opts.workScope,
|
|
@@ -208,20 +229,44 @@ export function registerWorks(program) {
|
|
|
208
229
|
printWork(result.work);
|
|
209
230
|
}
|
|
210
231
|
catch (e) {
|
|
211
|
-
|
|
232
|
+
if (!(e instanceof HttpError) || e.status !== 409)
|
|
233
|
+
handleHttp(e);
|
|
234
|
+
try {
|
|
235
|
+
const { works } = await client.works.listBySpace(spaceId);
|
|
236
|
+
const existingWork = works.find((work) => work.slug === slug);
|
|
237
|
+
if (!existingWork)
|
|
238
|
+
return handleHttp(e);
|
|
239
|
+
const { work } = await client.works.update(existingWork.id, {
|
|
240
|
+
status: status === "published" && existingWork.status !== "published" ? existingWork.status : status,
|
|
241
|
+
visibility: resolveVisibility(opts.visibility),
|
|
242
|
+
targetType: target.targetType,
|
|
243
|
+
targetRef: target.targetRef,
|
|
244
|
+
workScopes: opts.workScope,
|
|
245
|
+
allowedViewerScopes: opts.viewerScope,
|
|
246
|
+
meta,
|
|
247
|
+
});
|
|
248
|
+
const publishedVersion = status === "published" ? await client.works.publishVersion(work.id) : null;
|
|
249
|
+
const result = publishedVersion ?? { work };
|
|
250
|
+
if (jsonRequested(opts))
|
|
251
|
+
return outJson(result);
|
|
252
|
+
ok(status === "published" && publishedVersion ? `Work version updated: v${publishedVersion.version.version}` : `Work updated: ${work.id}`);
|
|
253
|
+
printWork(result.work);
|
|
254
|
+
}
|
|
255
|
+
catch (fallbackError) {
|
|
256
|
+
handleHttp(fallbackError);
|
|
257
|
+
}
|
|
212
258
|
}
|
|
213
259
|
});
|
|
214
260
|
worksCmd
|
|
215
261
|
.command("update <id>")
|
|
216
|
-
.description("Update work settings
|
|
262
|
+
.description("Update work settings")
|
|
217
263
|
.option("--slug <slug>", "New work slug")
|
|
218
264
|
.option("--file <path>", "Use a HTML file target")
|
|
219
265
|
.option("--dir <path>", "Use a directory site target")
|
|
220
266
|
.option("--port <port>", "Use a public sandbox port target")
|
|
221
|
-
.option("--draft", "Set status to draft")
|
|
222
267
|
.option("--disabled", "Set status to disabled")
|
|
223
|
-
.option("--status <status>", "Work status:
|
|
224
|
-
.option("--
|
|
268
|
+
.option("--status <status>", "Work status: published, disabled")
|
|
269
|
+
.option("--visibility <visibility>", "Work visibility: public, space")
|
|
225
270
|
.option("--work-scope <scope>", "Scope granted to the work runtime (space.view, session.view, file.view, taskrun.view)", collectOption, [])
|
|
226
271
|
.option("--viewer-scope <scope>", "Scope viewers may request (session.prompt.readonly, session.prompt.fullaccess, generation.create, user.space.list, user.session.list, user.usage.read)", collectOption, [])
|
|
227
272
|
.option("--clear-work-scopes", "Clear work runtime scopes")
|
|
@@ -257,29 +302,45 @@ export function registerWorks(program) {
|
|
|
257
302
|
showCohubBar: opts.showCohubBar,
|
|
258
303
|
});
|
|
259
304
|
}
|
|
305
|
+
const nextStatus = opts.status || opts.disabled ? resolveStatus(opts) : undefined;
|
|
306
|
+
const currentWork = nextStatus === "published" ? (await client.works.get(id)).work : null;
|
|
260
307
|
const input = compactObject({
|
|
261
308
|
slug: opts.slug,
|
|
262
|
-
status:
|
|
309
|
+
status: nextStatus === "published" && currentWork?.status !== "published" ? currentWork?.status : nextStatus,
|
|
310
|
+
visibility: resolveVisibility(opts.visibility),
|
|
263
311
|
targetType: target?.targetType,
|
|
264
312
|
targetRef: target?.targetRef,
|
|
265
|
-
publishVersion: opts.publishVersion || undefined,
|
|
266
313
|
workScopes: opts.clearWorkScopes ? [] : opts.workScope?.length ? opts.workScope : undefined,
|
|
267
314
|
allowedViewerScopes: opts.clearViewerScopes ? [] : opts.viewerScope?.length ? opts.viewerScope : undefined,
|
|
268
315
|
meta,
|
|
269
316
|
});
|
|
270
317
|
if (Object.keys(input).length === 0)
|
|
271
|
-
return error("Nothing to update", "Pass --slug, --file, --dir, --port, --status, --
|
|
318
|
+
return error("Nothing to update", "Pass --slug, --file, --dir, --port, --status, --visibility, --work-scope, --viewer-scope, --clear-work-scopes, --clear-viewer-scopes, --meta, --hide-cohub-bar, or --show-cohub-bar.");
|
|
272
319
|
try {
|
|
273
|
-
const
|
|
320
|
+
const updated = await client.works.update(id, input);
|
|
321
|
+
const publishedVersion = nextStatus === "published" && currentWork?.status !== "published"
|
|
322
|
+
? await client.works.publishVersion(updated.work.id)
|
|
323
|
+
: null;
|
|
324
|
+
const result = publishedVersion ?? updated;
|
|
274
325
|
if (jsonRequested(opts))
|
|
275
326
|
return outJson(result);
|
|
276
|
-
ok("Work updated");
|
|
327
|
+
ok(publishedVersion ? `Work published: v${publishedVersion.version.version}` : "Work updated");
|
|
277
328
|
printWork(result.work);
|
|
278
329
|
}
|
|
279
330
|
catch (e) {
|
|
280
331
|
handleHttp(e);
|
|
281
332
|
}
|
|
282
333
|
});
|
|
334
|
+
worksCmd
|
|
335
|
+
.command("publish-version <id>")
|
|
336
|
+
.description("Publish or update the current work version")
|
|
337
|
+
.option("--json", "Output as JSON")
|
|
338
|
+
.action(publishWorkVersion);
|
|
339
|
+
worksCmd
|
|
340
|
+
.command("release <id>", { hidden: true })
|
|
341
|
+
.description("Deprecated alias for publish-version")
|
|
342
|
+
.option("--json", "Output as JSON")
|
|
343
|
+
.action(publishWorkVersion);
|
|
283
344
|
worksCmd
|
|
284
345
|
.command("versions <id>")
|
|
285
346
|
.description("List work versions")
|
|
@@ -293,10 +354,9 @@ export function registerWorks(program) {
|
|
|
293
354
|
table(result.versions, [
|
|
294
355
|
{ key: "version", label: "Version" },
|
|
295
356
|
{ key: "id", label: "ID" },
|
|
296
|
-
{ key: "status", label: "Status" },
|
|
297
357
|
{ key: "targetType", label: "Target" },
|
|
298
358
|
{ key: "targetRef", label: "Ref" },
|
|
299
|
-
{ key: "
|
|
359
|
+
{ key: "createdAt", label: "Created" },
|
|
300
360
|
]);
|
|
301
361
|
}
|
|
302
362
|
catch (e) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@neta-art/cohub-cli",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "CLI for Cohub — spaces, sessions, and agent collaboration.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "UNLICENSED",
|
|
@@ -13,10 +13,10 @@
|
|
|
13
13
|
"README.md"
|
|
14
14
|
],
|
|
15
15
|
"dependencies": {
|
|
16
|
-
"@neta-art/generation": "^0.1.
|
|
16
|
+
"@neta-art/generation": "^0.1.10",
|
|
17
17
|
"commander": "^14.0.3",
|
|
18
18
|
"sharp": "^0.34.5",
|
|
19
|
-
"@neta-art/cohub": "
|
|
19
|
+
"@neta-art/cohub": "2.0.0"
|
|
20
20
|
},
|
|
21
21
|
"publishConfig": {
|
|
22
22
|
"access": "public"
|