@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 +2 -0
- package/dist/commands/desktop.d.ts +2 -7
- package/dist/commands/desktop.js +2 -10
- package/dist/commands/models.d.ts +26 -0
- package/dist/commands/models.js +103 -5
- package/dist/commands/space-webhooks.d.ts +7 -0
- package/dist/commands/space-webhooks.js +73 -0
- package/dist/commands/spaces.js +3 -0
- package/package.json +3 -3
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 {
|
|
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;
|
package/dist/commands/desktop.js
CHANGED
|
@@ -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 {};
|
package/dist/commands/models.js
CHANGED
|
@@ -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(
|
|
116
|
-
|
|
117
|
-
|
|
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
|
+
}
|
package/dist/commands/spaces.js
CHANGED
|
@@ -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.
|
|
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.
|
|
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.
|
|
22
|
+
"@neta-art/cohub": "8.14.0"
|
|
23
23
|
},
|
|
24
24
|
"publishConfig": {
|
|
25
25
|
"access": "public"
|