@neta-art/cohub-cli 6.9.1 → 6.11.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
@@ -227,6 +227,8 @@ cohub search "query" --limit 20 --json
227
227
 
228
228
  ## Models and multimodal generation
229
229
 
230
+ `models ls` prints each LLM's per-million-token cost (`$3 /M input · $15 /M output`, with cache rates when declared) and hides models marked `hidden`. `models ls --model-type multimodal` prints each model's unit price (`$0.04 / image`, `$0.10–$0.50 / second`); use `--json` for the raw `pricing` object (`unit`, `amount` or `min`/`max`, optional `note`).
231
+
230
232
  ```bash
231
233
  cohub models ls --json
232
234
  cohub models ls --model-type multimodal --json
@@ -1,10 +1,5 @@
1
- import { type DesktopSurface } from "@neta-art/cohub";
1
+ import { resolveOpenSurface } from "@neta-art/cohub";
2
2
  import type { Command } from "commander";
3
- /**
4
- * The surface a desktop.open should request: an explicit `--as` wins, then
5
- * whatever the App declared at publish time. `window` is the implicit default
6
- * and yields `undefined` so the command stays compact.
7
- */
8
- export declare function resolveOpenSurface(requested: string | undefined, declared: unknown): DesktopSurface | undefined;
3
+ export { resolveOpenSurface };
9
4
  export declare function registerDesktop(program: Command): void;
10
5
  export declare function registerLegacyUi(program: Command): void;
@@ -1,9 +1,9 @@
1
1
  import { readFileSync } from "node:fs";
2
- import { HttpError, } from "@neta-art/cohub";
3
- import { parseAppRef, DESKTOP_COMMAND_DEFAULT_TIMEOUT_MS, DESKTOP_COMMAND_MAX_TIMEOUT_MS, } from "@neta-art/cohub";
2
+ import { HttpError, parseAppRef, resolveOpenSurface, DESKTOP_COMMAND_DEFAULT_TIMEOUT_MS, DESKTOP_COMMAND_MAX_TIMEOUT_MS, } from "@neta-art/cohub";
4
3
  import { createClient } from "../client.js";
5
4
  import { error, handleHttp, json as outJson, jsonRequested, ok } from "../output.js";
6
5
  import { getAppByRef } from "../app-ref.js";
6
+ export { resolveOpenSurface };
7
7
  const FILE_SCHEME = "file://";
8
8
  const APP_SCHEME = "app://";
9
9
  const LEGACY_WORK_SCHEME = "work://";
@@ -65,14 +65,6 @@ function parseTimeout(value) {
65
65
  }
66
66
  return parsed;
67
67
  }
68
- /**
69
- * The surface a desktop.open should request: an explicit `--as` wins, then
70
- * whatever the App declared at publish time. `window` is the implicit default
71
- * and yields `undefined` so the command stays compact.
72
- */
73
- export function resolveOpenSurface(requested, declared) {
74
- return (requested ?? declared) === "overlay" ? "overlay" : undefined;
75
- }
76
68
  async function resolveAppTarget(client, ref) {
77
69
  const normalized = hasAppScheme(ref) ? ref.replace(/^[a-zA-Z]+:\/\//, "") : ref;
78
70
  const parsed = parseAppRef(normalized);
@@ -1,2 +1,28 @@
1
1
  import type { Command } from "commander";
2
+ import { type PublicGenerationDeclaration } from "@neta-art/cohub";
3
+ type MultimodalModelSummary = Pick<PublicGenerationDeclaration, "model" | "title" | "description" | "pricing">;
4
+ export declare function toMultimodalModelSummary(model: PublicGenerationDeclaration): MultimodalModelSummary;
5
+ export type GenerationModelPricing = NonNullable<PublicGenerationDeclaration["pricing"]>;
6
+ /**
7
+ * Render a display-only unit price, e.g. `$0.04 / image` or `$0.10–$0.50 / second`,
8
+ * optionally suffixed with a qualifier note.
9
+ *
10
+ * The raw `pricing` object is preserved in `--json` for machines; this string
11
+ * targets humans and agents reading the table output.
12
+ */
13
+ export declare function formatGenerationPrice(pricing: GenerationModelPricing): string;
14
+ /**
15
+ * Render per-million-token LLM cost, e.g. `$3 /M input · $15 /M output`,
16
+ * appending cache rates only when the model declares them.
17
+ */
18
+ export declare function formatLlmModelCost(model: Record<string, unknown>): string;
19
+ /**
20
+ * Drop hidden models from LLM discovery, mirroring generation models and the web
21
+ * picker. Providers left with no visible model are removed so the catalog shape
22
+ * matches what is actually shown. Explicit `true` is the only hidden signal.
23
+ */
24
+ export declare function filterHiddenLlmModels<T extends {
25
+ model: Record<string, unknown>;
26
+ }>(catalog: Record<string, T[]>): Record<string, T[]>;
2
27
  export declare function registerModels(program: Command): void;
28
+ export {};
@@ -1,11 +1,13 @@
1
1
  import { filterDiscoverableGenerationModels, filterGenerationDeclarationsByPolicy, getAllowedGenerationModelIds, parseGenerationPolicyFromEnv, } from "@neta-art/cohub";
2
2
  import { createClient } from "../client.js";
3
3
  import { table, json as outJson, jsonRequested, error, handleHttp } from "../output.js";
4
- function toMultimodalModelSummary(model) {
4
+ export function toMultimodalModelSummary(model) {
5
5
  return {
6
6
  model: model.model,
7
7
  ...(model.title ? { title: model.title } : {}),
8
8
  ...(model.description ? { description: model.description } : {}),
9
+ // Keep pricing structured for `--json`; the human table formats it separately.
10
+ ...(model.pricing ? { pricing: model.pricing } : {}),
9
11
  };
10
12
  }
11
13
  function printSection(title, lines) {
@@ -15,6 +17,88 @@ function printSection(title, lines) {
15
17
  for (const line of lines)
16
18
  console.log(` ${line}`);
17
19
  }
20
+ const PRICE_UNIT_LABELS = {
21
+ image: "image",
22
+ second: "second",
23
+ request: "request",
24
+ "1m_tokens": "1M tokens",
25
+ };
26
+ /** USD amounts stay compact while preserving the precision the value actually needs. */
27
+ function formatUsdAmount(value) {
28
+ const magnitude = Math.abs(value);
29
+ const isWholeDollar = magnitude >= 1 && Number.isInteger(value);
30
+ const fractionDigits = value === 0 || isWholeDollar ? 0 : magnitude < 0.01 ? 4 : 2;
31
+ return `$${value.toLocaleString("en-US", { minimumFractionDigits: fractionDigits, maximumFractionDigits: fractionDigits })}`;
32
+ }
33
+ /**
34
+ * Render a display-only unit price, e.g. `$0.04 / image` or `$0.10–$0.50 / second`,
35
+ * optionally suffixed with a qualifier note.
36
+ *
37
+ * The raw `pricing` object is preserved in `--json` for machines; this string
38
+ * targets humans and agents reading the table output.
39
+ */
40
+ export function formatGenerationPrice(pricing) {
41
+ const unit = PRICE_UNIT_LABELS[pricing.unit];
42
+ const value = typeof pricing.amount === "number"
43
+ ? formatUsdAmount(pricing.amount)
44
+ : pricing.min === pricing.max
45
+ ? formatUsdAmount(pricing.min)
46
+ : `${formatUsdAmount(pricing.min)}\u2013${formatUsdAmount(pricing.max)}`;
47
+ const base = `${value} / ${unit}`;
48
+ return pricing.note ? `${base} \u00b7 ${pricing.note}` : base;
49
+ }
50
+ /** LLM catalog entries carry cost as an untyped `model.cost` bag; validate it before use. */
51
+ function readLlmModelCost(model) {
52
+ const cost = model.cost;
53
+ if (!cost || typeof cost !== "object")
54
+ return null;
55
+ const { input, output, cacheRead, cacheWrite } = cost;
56
+ if (typeof input !== "number" || typeof output !== "number")
57
+ return null;
58
+ return {
59
+ input,
60
+ output,
61
+ ...(typeof cacheRead === "number" ? { cacheRead } : {}),
62
+ ...(typeof cacheWrite === "number" ? { cacheWrite } : {}),
63
+ };
64
+ }
65
+ /** Zero or absent per-token components carry no signal, so they are omitted. */
66
+ function formatLlmCostValue(value) {
67
+ return typeof value === "number" && Number.isFinite(value) && value !== 0 ? formatUsdAmount(value) : null;
68
+ }
69
+ /**
70
+ * Render per-million-token LLM cost, e.g. `$3 /M input · $15 /M output`,
71
+ * appending cache rates only when the model declares them.
72
+ */
73
+ export function formatLlmModelCost(model) {
74
+ const cost = readLlmModelCost(model);
75
+ if (!cost)
76
+ return "";
77
+ const parts = [
78
+ [cost.input, "input"],
79
+ [cost.output, "output"],
80
+ [cost.cacheRead, "cache read"],
81
+ [cost.cacheWrite, "cache write"],
82
+ ];
83
+ return parts.flatMap(([value, label]) => {
84
+ const formatted = formatLlmCostValue(value);
85
+ return formatted ? [`${formatted} /M ${label}`] : [];
86
+ }).join(" \u00b7 ");
87
+ }
88
+ /**
89
+ * Drop hidden models from LLM discovery, mirroring generation models and the web
90
+ * picker. Providers left with no visible model are removed so the catalog shape
91
+ * matches what is actually shown. Explicit `true` is the only hidden signal.
92
+ */
93
+ export function filterHiddenLlmModels(catalog) {
94
+ const visible = {};
95
+ for (const [provider, entries] of Object.entries(catalog)) {
96
+ const models = entries.filter((entry) => entry.model.hidden !== true);
97
+ if (models.length > 0)
98
+ visible[provider] = models;
99
+ }
100
+ return visible;
101
+ }
18
102
  function formatContentSpec(spec) {
19
103
  const details = [];
20
104
  const roles = spec.roles;
@@ -58,6 +142,8 @@ function printMultimodalModel(model) {
58
142
  printSection("Model", [model.model]);
59
143
  if (model.description)
60
144
  printSection("Description", [model.description]);
145
+ if (model.pricing)
146
+ printSection("Pricing", [formatGenerationPrice(model.pricing)]);
61
147
  printSection("Input", model.content.input.map(formatContentSpec));
62
148
  const parameterLines = Object.entries(model.parameters ?? {}).flatMap(([name, spec]) => formatParameter(name, spec));
63
149
  printSection("Parameters", parameterLines);
@@ -103,6 +189,11 @@ Examples:
103
189
  table(models, [
104
190
  { key: "model", label: "Model" },
105
191
  { key: "title", label: "Title" },
192
+ {
193
+ key: "pricing",
194
+ label: "Price",
195
+ format: (value) => (value ? formatGenerationPrice(value) : ""),
196
+ },
106
197
  { key: "description", label: "Description" },
107
198
  ]);
108
199
  return;
@@ -111,15 +202,22 @@ Examples:
111
202
  return error("Invalid model type", "Use --model-type llm or --model-type multimodal");
112
203
  }
113
204
  const catalog = await client.models.list();
205
+ const visibleCatalog = filterHiddenLlmModels(catalog);
114
206
  if (jsonRequested(opts))
115
- return outJson(catalog);
116
- // catalog is Record<provider, ModelCatalogEntry[]>
117
- for (const [provider, entries] of Object.entries(catalog)) {
207
+ return outJson(visibleCatalog);
208
+ if (Object.keys(visibleCatalog).length === 0)
209
+ return console.log(" (empty)");
210
+ for (const [provider, entries] of Object.entries(visibleCatalog)) {
118
211
  console.log(`\n ${provider}`);
119
212
  console.log(` ${"─".repeat(provider.length)}`);
120
- table(entries, [
213
+ table(entries.map((entry) => ({
214
+ id: entry.id,
215
+ provider: entry.provider,
216
+ cost: formatLlmModelCost(entry.model),
217
+ })), [
121
218
  { key: "id", label: "ID" },
122
219
  { key: "provider", label: "Provider" },
220
+ { key: "cost", label: "Cost" },
123
221
  ]);
124
222
  }
125
223
  }
@@ -0,0 +1,7 @@
1
+ import type { Command } from "commander";
2
+ import { createClient } from "../client.js";
3
+ export declare function parseBody(raw: string | undefined): unknown;
4
+ /** `cohub spaces webhooks` — inbound HTTP triggers declared by `.cohub/hooks/<name>` with `on.event: webhook`. */
5
+ export declare function registerSpaceWebhooks(spacesCmd: Command, deps?: {
6
+ createClient?: typeof createClient;
7
+ }): void;
@@ -0,0 +1,73 @@
1
+ import { joinApiUrl, resolveApiBaseUrl } from "@neta-art/cohub";
2
+ import { createClient } from "../client.js";
3
+ import { error, handleHttp, json as outJson, jsonRequested, ok, table } from "../output.js";
4
+ import { resolveSpace } from "../space.js";
5
+ export function parseBody(raw) {
6
+ if (raw === undefined)
7
+ return null;
8
+ try {
9
+ return JSON.parse(raw);
10
+ }
11
+ catch {
12
+ return error("Invalid body", "--body must be JSON");
13
+ }
14
+ }
15
+ /** `cohub spaces webhooks` — inbound HTTP triggers declared by `.cohub/hooks/<name>` with `on.event: webhook`. */
16
+ export function registerSpaceWebhooks(spacesCmd, deps = {}) {
17
+ const client = () => (deps.createClient ?? createClient)();
18
+ const webhooksCmd = spacesCmd
19
+ .command("webhooks")
20
+ .description("Inbound webhooks declared by .cohub/hooks/<name> (on.event: webhook)");
21
+ webhooksCmd
22
+ .command("ls")
23
+ .alias("list")
24
+ .description("List declared webhooks")
25
+ .option("--json", "Output as JSON")
26
+ .action(async (opts) => {
27
+ const spaceId = await resolveSpace(spacesCmd);
28
+ try {
29
+ const result = await client().space(spaceId).webhooks.list();
30
+ if (jsonRequested(opts))
31
+ return outJson(result.items);
32
+ table(result.items, [
33
+ { key: "name", label: "Name" },
34
+ { key: "action", label: "Action" },
35
+ { key: "hasSecret", label: "Secret" },
36
+ ]);
37
+ }
38
+ catch (e) {
39
+ handleHttp(e);
40
+ }
41
+ });
42
+ webhooksCmd
43
+ .command("url <name>")
44
+ .description("Print the URL external services should POST to")
45
+ .option("--json", "Output as JSON")
46
+ .action(async (name, opts) => {
47
+ const spaceId = await resolveSpace(spacesCmd);
48
+ const sdk = client();
49
+ const url = joinApiUrl(resolveApiBaseUrl({}), sdk.space(spaceId).webhooks.path(name));
50
+ if (jsonRequested(opts))
51
+ return outJson({ name, url });
52
+ process.stdout.write(`${url}\n`);
53
+ });
54
+ webhooksCmd
55
+ .command("trigger <name>")
56
+ .description("POST a JSON body to a webhook hook (local testing)")
57
+ .option("--body <json>", "JSON body", "null")
58
+ .option("--secret <secret>", "Value for on.secret")
59
+ .option("--json", "Output as JSON")
60
+ .action(async (name, opts) => {
61
+ const spaceId = await resolveSpace(spacesCmd);
62
+ const body = parseBody(opts.body);
63
+ try {
64
+ const result = await client().space(spaceId).webhooks.trigger(name, body, { secret: opts.secret });
65
+ if (jsonRequested(opts))
66
+ return outJson(result);
67
+ ok(`Triggered ${result.hook} — task ${result.taskRunId}`);
68
+ }
69
+ catch (e) {
70
+ handleHttp(e);
71
+ }
72
+ });
73
+ }
@@ -11,6 +11,7 @@ import { registerSpaceCommerce } from "./space-commerce.js";
11
11
  import { registerSpaceActivity } from "./space-activity.js";
12
12
  import { registerSpaceInvitations } from "./space-invitations.js";
13
13
  import { registerSpaceTurns } from "./space-turns.js";
14
+ import { registerSpaceWebhooks } from "./space-webhooks.js";
14
15
  const cliEnv = resolveCohubEnvironment();
15
16
  const defaultIdleTtlSeconds = cliEnv === "prod" ? 12 * 60 * 60 : 10 * 60;
16
17
  const SPACE_ROLES = ["host", "builder", "guest"];
@@ -659,6 +660,8 @@ export function registerSpaces(program) {
659
660
  registerMods(spacesCmd);
660
661
  // ── spaces labels ──
661
662
  registerLabels(spacesCmd);
663
+ // ── spaces webhooks ──
664
+ registerSpaceWebhooks(spacesCmd);
662
665
  // ── spaces pin / unpin (user-scope label convenience) ──
663
666
  spacesCmd
664
667
  .command("pin <id>")
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@neta-art/cohub-cli",
3
- "version": "6.9.1",
3
+ "version": "6.11.0",
4
4
  "description": "CLI for Cohub — spaces, sessions, and agent collaboration.",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -15,11 +15,11 @@
15
15
  "NOTICE"
16
16
  ],
17
17
  "dependencies": {
18
- "@neta-art/generation": "^0.1.26",
18
+ "@neta-art/generation": "^0.1.27",
19
19
  "commander": "^15.0.0",
20
20
  "pixi.js": "^8.20.1",
21
21
  "sharp": "^0.35.4",
22
- "@neta-art/cohub": "8.12.1"
22
+ "@neta-art/cohub": "8.14.0"
23
23
  },
24
24
  "publishConfig": {
25
25
  "access": "public"