@neta-art/cohub-cli 1.7.1 → 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
@@ -129,6 +129,8 @@ cohub search "query" --limit 20 --json
129
129
  ```bash
130
130
  cohub models ls --json
131
131
  cohub models ls --model-type multimodal --json
132
+ cohub models show <model>
133
+ cohub models show <model> --json
132
134
 
133
135
  cohub generate "a calm lake at sunrise" \
134
136
  --model <model> \
@@ -140,6 +142,12 @@ cohub generate "restyle this image" \
140
142
  --image ./input.png \
141
143
  --param size=1024x1024 \
142
144
  --json
145
+
146
+ cohub generate "a calm lake" \
147
+ --model <model> \
148
+ --async
149
+
150
+ cohub tasks get <taskRunId> --json
143
151
  ```
144
152
 
145
153
  Supported inputs:
package/dist/auth.d.ts CHANGED
@@ -30,7 +30,9 @@ export declare class AuthRequiredError extends Error {
30
30
  export declare const readAuthSession: () => AuthSession | null;
31
31
  export declare const clearAuthSession: () => void;
32
32
  export declare const authSource: () => AuthSource;
33
- export declare function resolveAccessToken(): Promise<string | null>;
33
+ export declare function resolveAccessToken(options?: {
34
+ forceRefresh?: boolean;
35
+ }): Promise<string | null>;
34
36
  export declare function requireAccessToken(): Promise<string>;
35
37
  export declare function refreshAccessToken(session?: AuthSession | null): Promise<string | null>;
36
38
  export declare function requestDeviceCode(): Promise<DeviceCode>;
package/dist/auth.js CHANGED
@@ -125,14 +125,14 @@ export const authSource = () => {
125
125
  return "logto";
126
126
  return null;
127
127
  };
128
- export async function resolveAccessToken() {
128
+ export async function resolveAccessToken(options) {
129
129
  const executionToken = process.env.COHUB_EXECUTION_TOKEN?.trim();
130
130
  if (executionToken)
131
131
  return executionToken;
132
132
  const session = readAuthSession();
133
133
  if (!session)
134
134
  throw new AuthRequiredError();
135
- if (session.accessTokenExpiresAt - Date.now() > EXPIRY_SKEW_MS)
135
+ if (!options?.forceRefresh && session.accessTokenExpiresAt - Date.now() > EXPIRY_SKEW_MS)
136
136
  return session.accessToken;
137
137
  return refreshAccessToken(session);
138
138
  }
@@ -1,6 +1,6 @@
1
1
  import { authSource, loginWithDeviceFlow, readAuthSession, refreshAccessToken, requestDeviceCode, revokeAndClearAuthSession, verifyDeviceCode } from "../auth.js";
2
2
  import { createClient } from "../client.js";
3
- import { table, json as outJson, ok, error, spinner, handleHttp } from "../output.js";
3
+ import { table, json as outJson, jsonRequested, ok, error, spinner, handleHttp } from "../output.js";
4
4
  export function registerAuth(program) {
5
5
  const auth = program.command("auth").description("Authentication management");
6
6
  auth
@@ -9,8 +9,8 @@ export function registerAuth(program) {
9
9
  .option("--request-code", "Request a device code without polling")
10
10
  .option("--verify-code", "Exchange a previously requested device code")
11
11
  .option("--json", "Output as JSON")
12
- .action(async (opts, command) => {
13
- const asJson = Boolean(opts.json || command.parent?.optsWithGlobals().json);
12
+ .action(async (opts) => {
13
+ const asJson = jsonRequested(opts);
14
14
  if (opts.requestCode && opts.verifyCode) {
15
15
  return error("Conflicting options", "Use only one of --request-code or --verify-code");
16
16
  }
@@ -43,8 +43,8 @@ export function registerAuth(program) {
43
43
  .command("whoami")
44
44
  .description("Show current user info")
45
45
  .option("--json", "Output as JSON")
46
- .action(async (opts, command) => {
47
- const asJson = Boolean(opts.json || command.parent?.optsWithGlobals().json);
46
+ .action(async (opts) => {
47
+ const asJson = jsonRequested(opts);
48
48
  return showSignedIn(asJson);
49
49
  });
50
50
  auth
@@ -80,11 +80,11 @@ async function showSignedIn(asJson) {
80
80
  if (!source)
81
81
  return error("Not authenticated", "Run `cohub auth login`.");
82
82
  const client = createClient();
83
- const sp = spinner();
84
- sp.start("Fetching user info");
83
+ const sp = asJson ? null : spinner();
84
+ sp?.start("Fetching user info");
85
85
  try {
86
86
  const user = await client.user.getMe();
87
- sp.stop("Done");
87
+ sp?.stop("Done");
88
88
  const session = readAuthSession();
89
89
  const payload = {
90
90
  source,
@@ -93,22 +93,77 @@ async function showSignedIn(asJson) {
93
93
  };
94
94
  if (asJson)
95
95
  return outJson(payload);
96
- const u = user;
96
+ const u = flattenMeForTable(user);
97
97
  console.log(` Auth source: ${source}`);
98
98
  console.log(` Token: ${payload.refreshable ? "refreshable" : "ephemeral"}\n`);
99
99
  table([u], [
100
- { key: "id", label: "ID" },
100
+ { key: "uuid", label: "UUID" },
101
101
  { key: "username", label: "Username" },
102
- { key: "name", label: "Name" },
102
+ { key: "displayName", label: "Name" },
103
103
  { key: "email", label: "Email" },
104
- { key: "created_at", label: "Created" },
105
104
  ]);
106
105
  }
107
106
  catch (e) {
108
- sp.stop("Failed");
107
+ if (source === "execution-token") {
108
+ sp?.stop("Using local execution token");
109
+ return showExecutionTokenFallback(asJson);
110
+ }
111
+ sp?.stop("Failed");
109
112
  handleHttp(e);
110
113
  }
111
114
  }
115
+ function flattenMeForTable(user) {
116
+ const profile = user.profile && typeof user.profile === "object" ? user.profile : {};
117
+ return {
118
+ uuid: user.uuid,
119
+ username: profile.username,
120
+ displayName: profile.displayName,
121
+ email: user.email,
122
+ };
123
+ }
124
+ function decodeExecutionTokenPayload() {
125
+ const token = process.env.COHUB_EXECUTION_TOKEN?.trim();
126
+ const payload = token?.split(".")[1];
127
+ if (!payload)
128
+ return null;
129
+ try {
130
+ const decoded = Buffer.from(payload, "base64url").toString("utf-8");
131
+ const parsed = JSON.parse(decoded);
132
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
133
+ }
134
+ catch {
135
+ return null;
136
+ }
137
+ }
138
+ function showExecutionTokenFallback(asJson) {
139
+ const payload = decodeExecutionTokenPayload();
140
+ const execution = {
141
+ actorUserId: typeof payload?.actorUserId === "string" ? payload.actorUserId : null,
142
+ spaceId: typeof payload?.spaceId === "string" ? payload.spaceId : null,
143
+ sessionId: typeof payload?.sessionId === "string" ? payload.sessionId : null,
144
+ source: typeof payload?.source === "string" ? payload.source : null,
145
+ expiresAt: typeof payload?.exp === "number" ? new Date(payload.exp * 1000).toISOString() : null,
146
+ };
147
+ const result = {
148
+ source: "execution-token",
149
+ refreshable: false,
150
+ user: execution.actorUserId ? { uuid: execution.actorUserId } : null,
151
+ execution,
152
+ };
153
+ if (asJson) {
154
+ outJson(result);
155
+ return;
156
+ }
157
+ console.log(" Auth source: execution-token");
158
+ console.log(" Token: ephemeral\n");
159
+ table([execution], [
160
+ { key: "actorUserId", label: "Actor" },
161
+ { key: "spaceId", label: "Space" },
162
+ { key: "sessionId", label: "Session" },
163
+ { key: "source", label: "Source" },
164
+ { key: "expiresAt", label: "Expires" },
165
+ ]);
166
+ }
112
167
  function printDeviceCode(code) {
113
168
  console.log("\nOpen this URL to sign in:\n");
114
169
  console.log(` ${code.verificationUriComplete}\n`);
@@ -1,5 +1,5 @@
1
1
  import { createClient } from "../client.js";
2
- import { table, json as outJson, ok, error, handleHttp } from "../output.js";
2
+ import { table, json as outJson, jsonRequested, ok, error, handleHttp } from "../output.js";
3
3
  export function registerChannels(program) {
4
4
  const cmd = program.command("channels", { hidden: true }).description("Channel integrations");
5
5
  cmd
@@ -11,7 +11,7 @@ export function registerChannels(program) {
11
11
  const client = createClient();
12
12
  try {
13
13
  const items = await client.channels.list();
14
- if (opts.json)
14
+ if (jsonRequested(opts))
15
15
  return outJson(items);
16
16
  if (items.length === 0)
17
17
  return console.log(" (empty)");
@@ -50,7 +50,7 @@ export function registerChannels(program) {
50
50
  name: opts.name,
51
51
  credentials,
52
52
  });
53
- if (opts.json)
53
+ if (jsonRequested(opts))
54
54
  return outJson(result);
55
55
  ok("Channel created");
56
56
  }
@@ -1,5 +1,5 @@
1
1
  import { createClient } from "../client.js";
2
- import { table, json as outJson, ok, handleHttp } from "../output.js";
2
+ import { table, json as outJson, jsonRequested, ok, error, handleHttp } from "../output.js";
3
3
  export function registerCronJobs(program) {
4
4
  const cmd = program.command("cron-jobs", { hidden: true }).description("Scheduled prompt jobs");
5
5
  cmd
@@ -11,7 +11,7 @@ export function registerCronJobs(program) {
11
11
  const client = createClient();
12
12
  try {
13
13
  const result = await client.cronJobs.list(spaceId);
14
- if (opts.json)
14
+ if (jsonRequested(opts))
15
15
  return outJson(result);
16
16
  if (result.jobs.length === 0)
17
17
  return console.log(" (empty)");
@@ -44,6 +44,8 @@ export function registerCronJobs(program) {
44
44
  .command("toggle <id> <on|off>")
45
45
  .description("Enable or disable a cron job")
46
46
  .action(async (id, state) => {
47
+ if (state !== "on" && state !== "off")
48
+ return error("Invalid state", "Use on or off");
47
49
  const enabled = state === "on";
48
50
  const client = createClient();
49
51
  try {
@@ -62,7 +64,7 @@ export function registerCronJobs(program) {
62
64
  const client = createClient();
63
65
  try {
64
66
  const result = await client.cronJobs.runs(id);
65
- if (opts.json)
67
+ if (jsonRequested(opts))
66
68
  return outJson(result);
67
69
  if (result.runs.length === 0)
68
70
  return console.log(" (empty)");
@@ -1,7 +1,8 @@
1
1
  import { mkdir, readFile, stat, writeFile } from "node:fs/promises";
2
2
  import { basename, dirname, extname, join } from "node:path";
3
3
  import { createClient } from "../client.js";
4
- import { json as outJson, ok, handleHttp } from "../output.js";
4
+ import { resolveSpace } from "../space.js";
5
+ import { json as outJson, jsonRequested, ok, error, handleHttp, spinner } from "../output.js";
5
6
  const mimeByExt = {
6
7
  ".png": "image/png",
7
8
  ".jpg": "image/jpeg",
@@ -47,8 +48,8 @@ async function contentFromPathOrUrl(type, value) {
47
48
  if (/^https?:\/\//.test(value))
48
49
  return { type, source: { type: "url", url: value } };
49
50
  const data = await readFile(value);
50
- const media_type = mimeByExt[extname(value).toLowerCase()] ?? "application/octet-stream";
51
- 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") } };
52
53
  }
53
54
  async function saveOutputs(output, outputPath) {
54
55
  const outputs = output.filter((block) => block.type === "text" || block.type === "image" || block.type === "video" || block.type === "audio");
@@ -71,7 +72,7 @@ async function saveOutputs(output, outputPath) {
71
72
  continue;
72
73
  }
73
74
  const source = block.source;
74
- 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;
75
76
  if (source.type === "url") {
76
77
  const response = await fetch(source.url);
77
78
  if (!response.ok)
@@ -79,72 +80,144 @@ async function saveOutputs(output, outputPath) {
79
80
  await writeFile(target, Buffer.from(await response.arrayBuffer()));
80
81
  savedPaths.push(target);
81
82
  }
82
- else if (source.type === "base64") {
83
+ else {
83
84
  await writeFile(target, Buffer.from(source.data, "base64"));
84
85
  savedPaths.push(target);
85
86
  }
86
- else {
87
- throw new Error(`Cannot save space file output locally: ${source.space_id}:${source.path}`);
88
- }
89
87
  }
90
88
  return savedPaths;
91
89
  }
92
- function outputName(type, url, index) {
90
+ function outputName(block, url, index) {
93
91
  const fromUrl = url ? basename(new URL(url).pathname) : "";
92
+ const label = slugOutputLabel(block);
94
93
  if (fromUrl?.includes("."))
95
- return `generation-${index + 1}-${fromUrl}`;
96
- const ext = type === "video" ? "mp4" : type === "audio" ? "bin" : "png";
97
- 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);
98
119
  }
99
120
  function printGeneration(output) {
100
121
  for (const block of output) {
101
- if (block.type === "text")
122
+ if (block.type === "text") {
102
123
  console.log(block.text);
103
- else if (block.source.type === "url")
104
- console.log(`${block.type}: ${block.source.url}`);
105
- else if (block.source.type === "base64")
106
- console.log(`${block.type}: base64 ${block.source.media_type} (${block.source.data.length} chars)`);
107
- else
108
- 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
+ }
109
131
  }
110
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
+ }
146
+ function parseTimeoutMs(value) {
147
+ if (!value)
148
+ return undefined;
149
+ if (!/^\d+$/.test(value.trim()))
150
+ return error("Invalid timeout", "--timeout-ms must be a positive integer");
151
+ const timeoutMs = Number.parseInt(value, 10);
152
+ if (!Number.isSafeInteger(timeoutMs) || timeoutMs <= 0)
153
+ return error("Invalid timeout", "--timeout-ms must be a positive integer");
154
+ return timeoutMs;
155
+ }
111
156
  export function registerGenerations(program) {
112
157
  program
113
158
  .command("generate")
114
159
  .description("Generate multimodal outputs")
115
160
  .argument("<prompt>", "Prompt text")
116
- .requiredOption("--model <model>", "Multimodal model ID from `cohub models ls --model-type multimodal`")
161
+ .requiredOption("-m, --model <model>", "Multimodal model ID from `cohub models ls --model-type multimodal`")
117
162
  .option("--image <path-or-url>", "Image input file path or URL; repeatable", collect, [])
118
163
  .option("--video <path-or-url>", "Video input file path or URL; repeatable", collect, [])
119
164
  .option("--audio <path-or-url>", "Audio input file path or URL; repeatable", collect, [])
120
165
  .option("--param <key=value>", "Generation parameter; repeatable, values may be JSON/number/boolean", collect, [])
121
166
  .option("--parameters <json>", "Generation parameters as a JSON object")
122
167
  .option("--metadata <json>", "Metadata as a JSON object")
123
- .option("--output <path>", "Save generated output to a file or directory")
168
+ .option("-o, --output <path>", "Save generated output to a file or directory")
169
+ .option("--async", "Queue the generation task and return immediately")
170
+ .option("--timeout-ms <ms>", "Maximum time to wait in synchronous mode")
124
171
  .option("--json", "Output as JSON")
125
172
  .addHelpText("after", `
126
173
 
127
174
  Examples:
128
175
  cohub models ls --model-type multimodal
129
- cohub generate "A calm lake at sunrise" --model <model> --output lake.png
130
- cohub generate "Restyle this image" --model <model> --image input.png --param size=1024x1024
176
+ cohub -s <space-id> generate "A calm lake at sunrise" -m <model> -o lake.png
177
+ COHUB_SPACE_ID=<space-id> cohub generate "Restyle this image" -m <model> --image input.png
178
+ cohub -s <space-id> generate "A calm lake" -m <model> --async
131
179
  `)
132
180
  .action(async (prompt, opts) => {
133
181
  try {
182
+ const spaceId = resolveSpace(program);
134
183
  const content = [{ type: "text", text: prompt }];
135
184
  content.push(...await Promise.all(opts.image.map((value) => contentFromPathOrUrl("image", value))));
136
185
  content.push(...await Promise.all(opts.video.map((value) => contentFromPathOrUrl("video", value))));
137
186
  content.push(...await Promise.all(opts.audio.map((value) => contentFromPathOrUrl("audio", value))));
138
- const generation = await createClient().generations.create({
187
+ const client = createClient();
188
+ const created = await client.generations.create({
189
+ spaceId,
139
190
  model: opts.model,
140
191
  content,
141
192
  parameters: parseParams(opts.param, opts.parameters),
142
193
  metadata: opts.metadata ? JSON.parse(opts.metadata) : undefined,
143
194
  });
144
- const savedPaths = opts.output && generation.output ? await saveOutputs(generation.output, opts.output) : [];
145
- if (opts.json)
146
- return outJson(savedPaths.length > 0 ? { ...generation, savedPaths } : generation);
147
- printGeneration(generation.output ?? []);
195
+ if (opts.async) {
196
+ if (jsonRequested(opts))
197
+ return outJson(created);
198
+ return ok(`Generation queued — task ID: ${created.taskRunId}\n ${resumeHint(created.taskRunId)}`);
199
+ }
200
+ const spin = spinner();
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
+ }
208
+ const result = await client.generations.wait(created.taskRunId, {
209
+ timeoutMs: parseTimeoutMs(opts.timeoutMs),
210
+ onPoll: () => {
211
+ pollCount += 1;
212
+ spin.update(`Generating... ${formatElapsed(Date.now() - waitStartedAt)}, ${pollCount} polls`);
213
+ },
214
+ });
215
+ if (!jsonRequested(opts))
216
+ spin.stop(`Generation completed — task ID: ${created.taskRunId}, ${formatElapsed(Date.now() - waitStartedAt)}, ${pollCount} polls`);
217
+ const savedPaths = opts.output ? await saveOutputs(result.output, opts.output) : [];
218
+ if (jsonRequested(opts))
219
+ return outJson(savedPaths.length > 0 ? { ...result, taskRunId: created.taskRunId, savedPaths } : { ...result, taskRunId: created.taskRunId });
220
+ printGeneration(result.output);
148
221
  if (savedPaths.length > 0)
149
222
  ok(`Saved to ${savedPaths.join(", ")}`);
150
223
  }
@@ -1,5 +1,69 @@
1
1
  import { createClient } from "../client.js";
2
- import { table, json as outJson, error, handleHttp } from "../output.js";
2
+ import { table, json as outJson, jsonRequested, error, handleHttp } from "../output.js";
3
+ function toMultimodalModelSummary(model) {
4
+ return {
5
+ model: model.model,
6
+ ...(model.title ? { title: model.title } : {}),
7
+ ...(model.description ? { description: model.description } : {}),
8
+ };
9
+ }
10
+ function printSection(title, lines) {
11
+ if (lines.length === 0)
12
+ return;
13
+ console.log(`\n${title}`);
14
+ for (const line of lines)
15
+ console.log(` ${line}`);
16
+ }
17
+ function formatContentSpec(spec) {
18
+ const details = [];
19
+ details.push(spec.required === false ? "optional" : "required");
20
+ if (typeof spec.min === "number")
21
+ details.push(`min ${spec.min}`);
22
+ if (typeof spec.max === "number")
23
+ details.push(`max ${spec.max}`);
24
+ if (spec.sources?.length)
25
+ details.push(`sources: ${spec.sources.join(", ")}`);
26
+ if (spec.merge)
27
+ details.push(`merge: ${spec.merge}`);
28
+ if (spec.description)
29
+ details.push(spec.description);
30
+ return `${spec.type}${details.length > 0 ? ` — ${details.join("; ")}` : ""}`;
31
+ }
32
+ function formatParameter(name, spec) {
33
+ const lines = [`${name}`];
34
+ const details = [`type: ${spec.type}`];
35
+ if (spec.optional)
36
+ details.push("optional");
37
+ if ("default" in spec && spec.default !== undefined)
38
+ details.push(`default: ${String(spec.default)}`);
39
+ if ("min" in spec && typeof spec.min === "number")
40
+ details.push(`min: ${spec.min}`);
41
+ if ("max" in spec && typeof spec.max === "number")
42
+ details.push(`max: ${spec.max}`);
43
+ if ("enum" in spec && spec.enum?.length)
44
+ details.push(`values: ${spec.enum.join(", ")}`);
45
+ lines.push(` ${details.join("; ")}`);
46
+ if (spec.description)
47
+ lines.push(` ${spec.description}`);
48
+ if ("examples" in spec && spec.examples?.length)
49
+ lines.push(` examples: ${spec.examples.map(String).join(", ")}`);
50
+ return lines;
51
+ }
52
+ function printMultimodalModel(model) {
53
+ console.log(model.title ?? model.model);
54
+ printSection("Model", [model.model]);
55
+ if (model.description)
56
+ printSection("Description", [model.description]);
57
+ printSection("Input", model.content.input.map(formatContentSpec));
58
+ const parameterLines = Object.entries(model.parameters ?? {}).flatMap(([name, spec]) => formatParameter(name, spec));
59
+ printSection("Parameters", parameterLines);
60
+ const examples = model.examples ?? [];
61
+ printSection("Examples", examples.map((example, index) => {
62
+ const title = example.title ? `${example.title}: ` : "";
63
+ const prompt = example.request.content.find((block) => block.type === "text")?.text;
64
+ return `${index + 1}. ${title}${prompt ? `"${prompt}"` : example.request.model}`;
65
+ }));
66
+ }
3
67
  export function registerModels(program) {
4
68
  const cmd = program
5
69
  .command("models")
@@ -10,6 +74,8 @@ Examples:
10
74
  cohub models ls
11
75
  cohub models ls --model-type multimodal
12
76
  cohub models ls --model-type multimodal --json
77
+ cohub models show <model>
78
+ cohub models show <model> --json
13
79
  `);
14
80
  cmd
15
81
  .command("ls")
@@ -22,9 +88,10 @@ Examples:
22
88
  try {
23
89
  if (opts.modelType === "multimodal") {
24
90
  const response = await client.models.listMultimodal();
25
- if (opts.json)
26
- return outJson(response);
27
- table(response.models, [
91
+ const models = response.models.map(toMultimodalModelSummary);
92
+ if (jsonRequested(opts))
93
+ return outJson({ models });
94
+ table(models, [
28
95
  { key: "model", label: "Model" },
29
96
  { key: "title", label: "Title" },
30
97
  { key: "description", label: "Description" },
@@ -35,7 +102,7 @@ Examples:
35
102
  return error("Invalid model type", "Use --model-type llm or --model-type multimodal");
36
103
  }
37
104
  const catalog = await client.models.list();
38
- if (opts.json)
105
+ if (jsonRequested(opts))
39
106
  return outJson(catalog);
40
107
  // catalog is Record<provider, ModelCatalogEntry[]>
41
108
  for (const [provider, entries] of Object.entries(catalog)) {
@@ -51,4 +118,25 @@ Examples:
51
118
  handleHttp(e);
52
119
  }
53
120
  });
121
+ cmd
122
+ .command("show")
123
+ .description("Show full multimodal model details")
124
+ .argument("<model>", "Multimodal model ID")
125
+ .option("--json", "Output as JSON")
126
+ .action(async (modelId, opts) => {
127
+ const client = createClient();
128
+ try {
129
+ const response = await client.models.listMultimodal();
130
+ const model = response.models.find((item) => item.model === modelId);
131
+ if (!model) {
132
+ return error("Model not found", `No multimodal model named ${modelId}`);
133
+ }
134
+ if (jsonRequested(opts))
135
+ return outJson(model);
136
+ printMultimodalModel(model);
137
+ }
138
+ catch (e) {
139
+ handleHttp(e);
140
+ }
141
+ });
54
142
  }
@@ -1,6 +1,6 @@
1
1
  import { uploadAvatarAsset } from "../avatar.js";
2
2
  import { createClient } from "../client.js";
3
- import { json as outJson, ok, handleHttp } from "../output.js";
3
+ import { json as outJson, jsonRequested, ok, handleHttp } from "../output.js";
4
4
  export function registerProfile(program) {
5
5
  const profileCmd = program.command("profile").description("Manage your profile");
6
6
  profileCmd
@@ -12,7 +12,7 @@ export function registerProfile(program) {
12
12
  try {
13
13
  const asset = await uploadAvatarAsset({ client, purpose: "user_avatar", path });
14
14
  const result = await client.user.updateProfile({ avatarUrl: asset.publicUrl });
15
- if (opts.json)
15
+ if (jsonRequested(opts))
16
16
  return outJson({ ...result, asset });
17
17
  ok("Avatar updated");
18
18
  }
@@ -1,5 +1,5 @@
1
1
  import { createClient } from "../client.js";
2
- import { table, json as outJson, handleHttp } from "../output.js";
2
+ import { table, json as outJson, jsonRequested, handleHttp } from "../output.js";
3
3
  export function registerPrompts(program) {
4
4
  const cmd = program.command("prompts").description("Prompt template management");
5
5
  cmd
@@ -12,7 +12,7 @@ export function registerPrompts(program) {
12
12
  const client = createClient();
13
13
  try {
14
14
  const result = await client.prompts.list({ spaceId: opts.space });
15
- if (opts.json)
15
+ if (jsonRequested(opts))
16
16
  return outJson(result);
17
17
  if (result.prompts.length === 0)
18
18
  return console.log(" (empty)");
@@ -1,5 +1,5 @@
1
1
  import { createClient } from "../client.js";
2
- import { table, json as outJson, error, handleHttp } from "../output.js";
2
+ import { table, json as outJson, jsonRequested, error, handleHttp } from "../output.js";
3
3
  const DEFAULT_LIMIT = 20;
4
4
  const MAX_TITLE_LENGTH = 72;
5
5
  const MAX_CONTEXT_LENGTH = 42;
@@ -81,7 +81,7 @@ Examples:
81
81
  types: input.types,
82
82
  spaceId: input.spaceId,
83
83
  });
84
- if (opts.json)
84
+ if (jsonRequested(opts))
85
85
  return outJson(result);
86
86
  if (result.degraded) {
87
87
  process.stderr.write(" Search is temporarily degraded; results may be incomplete.\n");
@@ -1,5 +1,13 @@
1
1
  import { createClient } from "../client.js";
2
- import { table, json as outJson, handleHttp } from "../output.js";
2
+ import { table, json as outJson, jsonRequested, error, handleHttp } from "../output.js";
3
+ const SPACE_ROLES = ["host", "builder", "guest"];
4
+ function parseAnonymousRole(value) {
5
+ if (value === undefined || value === "null")
6
+ return null;
7
+ if (SPACE_ROLES.includes(value))
8
+ return value;
9
+ return error("Invalid anonymous role", "Use one of: host, builder, guest, null");
10
+ }
3
11
  export function registerSessionAccess(program) {
4
12
  const cmd = program
5
13
  .command("session-access")
@@ -12,7 +20,7 @@ export function registerSessionAccess(program) {
12
20
  const client = createClient();
13
21
  try {
14
22
  const policy = await client.sessionAccess.get(id);
15
- if (opts.json)
23
+ if (jsonRequested(opts))
16
24
  return outJson(policy);
17
25
  table([policy], [
18
26
  { key: "signed_in_user", label: "Signed-in" },
@@ -32,9 +40,9 @@ export function registerSessionAccess(program) {
32
40
  const client = createClient();
33
41
  try {
34
42
  const policy = await client.sessionAccess.set(id, {
35
- anonymous_user: (opts.anonymous ?? null),
43
+ anonymous_user: parseAnonymousRole(opts.anonymous),
36
44
  });
37
- if (opts.json)
45
+ if (jsonRequested(opts))
38
46
  return outJson(policy);
39
47
  console.log("Session access updated");
40
48
  table([policy], [