@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 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 update <workId> --publish-version
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 contentFromPathOrUrl(type, value) {
49
- if (/^https?:\/\//.test(value))
50
- return { type, source: { type: "url", url: value } };
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 { type, source: { type: "base64", mediaType, data: data.toString("base64") } };
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({
@@ -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)
@@ -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 = ["draft", "published", "disabled"];
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.draft ? "draft" : undefined, opts.disabled ? "disabled" : undefined].filter(Boolean);
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, --draft, or --disabled");
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: draft, published, disabled")
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
- handleHttp(e);
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 or publish a new version")
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: draft, published, disabled")
224
- .option("--publish-version", "Force publishing a new version")
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: opts.status || opts.draft || opts.disabled ? resolveStatus(opts) : undefined,
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, --publish-version, --work-scope, --viewer-scope, --clear-work-scopes, --clear-viewer-scopes, --meta, --hide-cohub-bar, or --show-cohub-bar.");
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 result = await client.works.update(id, input);
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: "publishedAt", label: "Published" },
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": "1.20.6",
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.9",
16
+ "@neta-art/generation": "^0.1.10",
17
17
  "commander": "^14.0.3",
18
18
  "sharp": "^0.34.5",
19
- "@neta-art/cohub": "1.34.0"
19
+ "@neta-art/cohub": "2.0.0"
20
20
  },
21
21
  "publishConfig": {
22
22
  "access": "public"