@neta-art/cohub-cli 1.8.0 → 1.9.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
@@ -142,6 +142,12 @@ cohub generate "restyle this image" \
142
142
  --image ./input.png \
143
143
  --param size=1024x1024 \
144
144
  --json
145
+
146
+ cohub generate "a calm lake" \
147
+ --model <model> \
148
+ --async
149
+
150
+ cohub tasks get <taskRunId> --json
145
151
  ```
146
152
 
147
153
  Supported inputs:
@@ -48,8 +48,8 @@ async function contentFromPathOrUrl(type, value) {
48
48
  if (/^https?:\/\//.test(value))
49
49
  return { type, source: { type: "url", url: value } };
50
50
  const data = await readFile(value);
51
- const media_type = mimeByExt[extname(value).toLowerCase()] ?? "application/octet-stream";
52
- return { type, source: { type: "base64", media_type, data: data.toString("base64") } };
51
+ const mediaType = mimeByExt[extname(value).toLowerCase()] ?? "application/octet-stream";
52
+ return { type, source: { type: "base64", mediaType, data: data.toString("base64") } };
53
53
  }
54
54
  async function saveOutputs(output, outputPath) {
55
55
  const outputs = output.filter((block) => block.type === "text" || block.type === "image" || block.type === "video" || block.type === "audio");
@@ -72,7 +72,7 @@ async function saveOutputs(output, outputPath) {
72
72
  continue;
73
73
  }
74
74
  const source = block.source;
75
- const target = isDir ? join(outputPath, outputName(block.type, source.type === "url" ? source.url : undefined, i)) : outputPath;
75
+ const target = isDir ? join(outputPath, outputName(block, source.type === "url" ? source.url : undefined, i)) : outputPath;
76
76
  if (source.type === "url") {
77
77
  const response = await fetch(source.url);
78
78
  if (!response.ok)
@@ -80,35 +80,69 @@ async function saveOutputs(output, outputPath) {
80
80
  await writeFile(target, Buffer.from(await response.arrayBuffer()));
81
81
  savedPaths.push(target);
82
82
  }
83
- else if (source.type === "base64") {
83
+ else {
84
84
  await writeFile(target, Buffer.from(source.data, "base64"));
85
85
  savedPaths.push(target);
86
86
  }
87
- else {
88
- throw new Error(`Cannot save space file output locally: ${source.space_id}:${source.path}`);
89
- }
90
87
  }
91
88
  return savedPaths;
92
89
  }
93
- function outputName(type, url, index) {
90
+ function outputName(block, url, index) {
94
91
  const fromUrl = url ? basename(new URL(url).pathname) : "";
92
+ const label = slugOutputLabel(block);
95
93
  if (fromUrl?.includes("."))
96
- return `generation-${index + 1}-${fromUrl}`;
97
- const ext = type === "video" ? "mp4" : type === "audio" ? "bin" : "png";
98
- return `generation-${index + 1}.${ext}`;
94
+ return `generation-${index + 1}-${label}-${fromUrl}`;
95
+ const ext = block.type === "video" ? "mp4" : block.type === "audio" ? "bin" : block.type === "text" ? "txt" : "png";
96
+ return `generation-${index + 1}-${label}.${ext}`;
97
+ }
98
+ function metaRole(block) {
99
+ const role = block.meta?.role;
100
+ return typeof role === "string" && role.length > 0 ? role : undefined;
101
+ }
102
+ function humanizeRole(role) {
103
+ return role.replaceAll("_", " ").replaceAll("-", " ");
104
+ }
105
+ function slugify(value) {
106
+ return value.toLowerCase().replaceAll(/[^a-z0-9]+/g, "-").replaceAll(/^-|-$/g, "") || "output";
107
+ }
108
+ function formatOutputLabel(block) {
109
+ const role = metaRole(block);
110
+ if (!role)
111
+ return block.type;
112
+ const label = humanizeRole(role);
113
+ return block.type === "image" && ["first_frame", "last_frame", "reference_image"].includes(role)
114
+ ? label
115
+ : `${block.type} (role: ${role})`;
116
+ }
117
+ function slugOutputLabel(block) {
118
+ return slugify(metaRole(block) ?? block.type);
99
119
  }
100
120
  function printGeneration(output) {
101
121
  for (const block of output) {
102
- if (block.type === "text")
122
+ if (block.type === "text") {
103
123
  console.log(block.text);
104
- else if (block.source.type === "url")
105
- console.log(`${block.type}: ${block.source.url}`);
106
- else if (block.source.type === "base64")
107
- console.log(`${block.type}: base64 ${block.source.media_type} (${block.source.data.length} chars)`);
108
- else
109
- console.log(`${block.type}: ${block.source.space_id}:${block.source.path}`);
124
+ }
125
+ else if (block.source.type === "url") {
126
+ console.log(`${formatOutputLabel(block)}: ${block.source.url}`);
127
+ }
128
+ else {
129
+ console.log(`${formatOutputLabel(block)}: base64 ${block.source.mediaType} (${block.source.data.length} chars)`);
130
+ }
110
131
  }
111
132
  }
133
+ function resumeHint(taskRunId) {
134
+ return `Use \`cohub tasks get ${taskRunId} --json\` to inspect the task later.`;
135
+ }
136
+ function formatElapsed(ms) {
137
+ if (ms < 1000)
138
+ return `${ms}ms`;
139
+ const seconds = Math.floor(ms / 1000);
140
+ if (seconds < 60)
141
+ return `${seconds}s`;
142
+ const minutes = Math.floor(seconds / 60);
143
+ const restSeconds = seconds % 60;
144
+ return restSeconds > 0 ? `${minutes}m ${restSeconds}s` : `${minutes}m`;
145
+ }
112
146
  function parseTimeoutMs(value) {
113
147
  if (!value)
114
148
  return undefined;
@@ -161,16 +195,25 @@ Examples:
161
195
  if (opts.async) {
162
196
  if (jsonRequested(opts))
163
197
  return outJson(created);
164
- return ok(`Generation queued — taskRunId: ${created.taskRunId}`);
198
+ return ok(`Generation queued — task ID: ${created.taskRunId}\n ${resumeHint(created.taskRunId)}`);
165
199
  }
166
200
  const spin = spinner();
167
- if (!jsonRequested(opts))
168
- spin.start("Generating");
201
+ let pollCount = 0;
202
+ const waitStartedAt = Date.now();
203
+ if (!jsonRequested(opts)) {
204
+ process.stderr.write(` Generation queued — task ID: ${created.taskRunId}\n`);
205
+ process.stderr.write(` ${resumeHint(created.taskRunId)}\n`);
206
+ spin.start("Generating...");
207
+ }
169
208
  const result = await client.generations.wait(created.taskRunId, {
170
209
  timeoutMs: parseTimeoutMs(opts.timeoutMs),
210
+ onPoll: () => {
211
+ pollCount += 1;
212
+ spin.update(`Generating... ${formatElapsed(Date.now() - waitStartedAt)}, ${pollCount} polls`);
213
+ },
171
214
  });
172
215
  if (!jsonRequested(opts))
173
- spin.stop("Generation completed");
216
+ spin.stop(`Generation completed — task ID: ${created.taskRunId}, ${formatElapsed(Date.now() - waitStartedAt)}, ${pollCount} polls`);
174
217
  const savedPaths = opts.output ? await saveOutputs(result.output, opts.output) : [];
175
218
  if (jsonRequested(opts))
176
219
  return outJson(savedPaths.length > 0 ? { ...result, taskRunId: created.taskRunId, savedPaths } : { ...result, taskRunId: created.taskRunId });
package/dist/output.d.ts CHANGED
@@ -12,5 +12,6 @@ export declare function error(msg: string, detail?: string): never;
12
12
  export declare function handleHttp(e: unknown): never;
13
13
  export declare function spinner(): {
14
14
  start(msg: string): void;
15
+ update(msg: string): void;
15
16
  stop(msg: string): void;
16
17
  };
package/dist/output.js CHANGED
@@ -73,6 +73,18 @@ function debugErrorMetaFromBody(body) {
73
73
  items.push(`traceId: ${errorBody.traceId}`);
74
74
  return items;
75
75
  }
76
+ function fetchFailureDetail(e) {
77
+ if (!(e instanceof Error) || e.message !== "fetch failed")
78
+ return null;
79
+ const cause = e.cause;
80
+ const code = typeof cause?.code === "string" ? cause.code : null;
81
+ const hostname = typeof cause?.hostname === "string" ? cause.hostname : null;
82
+ const message = typeof cause?.message === "string" ? cause.message : null;
83
+ const parts = [code, hostname && `host: ${hostname}`, message].filter(Boolean);
84
+ return parts.length > 0
85
+ ? `Network request failed (${parts.join(" · ")}). Check DNS/proxy/firewall settings and try again.`
86
+ : "Network request failed. Check DNS/proxy/firewall settings and try again.";
87
+ }
76
88
  export function handleHttp(e) {
77
89
  if (e instanceof Error && e.name === "AuthRequiredError") {
78
90
  return error("not authenticated", "run `cohub auth login`");
@@ -80,32 +92,38 @@ export function handleHttp(e) {
80
92
  const status = e.status;
81
93
  const body = e.body;
82
94
  const message = errorMessageFromBody(body) ?? (e instanceof Error ? e.message : String(e));
95
+ const fetchDetail = fetchFailureDetail(e);
83
96
  const detailParts = [];
84
97
  if (process.env.COHUB_DEBUG_ERRORS) {
85
98
  if (status)
86
99
  detailParts.push(`HTTP ${status}`);
87
100
  detailParts.push(...debugErrorMetaFromBody(body));
88
101
  }
89
- error(message, detailParts.length > 0 ? detailParts.join(" · ") : undefined);
102
+ error(message, detailParts.length > 0 ? detailParts.join(" · ") : fetchDetail ?? undefined);
90
103
  }
91
104
  // -- Spinner -----------------------------------------------------------------
92
105
  export function spinner() {
93
106
  let interval = null;
94
107
  const frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
95
108
  let i = 0;
109
+ let currentMsg = "";
96
110
  return {
97
111
  start(msg) {
112
+ currentMsg = msg;
98
113
  if (process.env.CI || !process.stderr.isTTY) {
99
- process.stderr.write(` ${msg}...\n`);
114
+ process.stderr.write(` ${currentMsg}...\n`);
100
115
  return;
101
116
  }
102
- process.stderr.write(` ${msg} `);
117
+ process.stderr.write(` ${currentMsg} `);
103
118
  interval = setInterval(() => {
104
119
  process.stderr.clearLine?.(0);
105
120
  process.stderr.cursorTo?.(0);
106
- process.stderr.write(` ${frames[i++ % frames.length] ?? ""} ${msg} `);
121
+ process.stderr.write(` ${frames[i++ % frames.length] ?? ""} ${currentMsg} `);
107
122
  }, 80);
108
123
  },
124
+ update(msg) {
125
+ currentMsg = msg;
126
+ },
109
127
  stop(msg) {
110
128
  if (interval)
111
129
  clearInterval(interval);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@neta-art/cohub-cli",
3
- "version": "1.8.0",
3
+ "version": "1.9.0",
4
4
  "description": "CLI for Cohub — spaces, sessions, and agent collaboration.",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",
@@ -13,9 +13,10 @@
13
13
  "README.md"
14
14
  ],
15
15
  "dependencies": {
16
+ "@neta-art/generation": "^0.1.2",
16
17
  "commander": "^13.1.0",
17
18
  "sharp": "^0.34.5",
18
- "@neta-art/cohub": "1.16.0"
19
+ "@neta-art/cohub": "1.16.1"
19
20
  },
20
21
  "publishConfig": {
21
22
  "access": "public"