@neta-art/cohub-cli 3.6.4 → 3.8.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
@@ -276,6 +276,7 @@ cohub profile update --username <username>
276
276
  cohub spaces update <spaceId> --slug <space-slug>
277
277
  cohub -s <spaceId> works ls --json
278
278
  cohub works get <workId|url|username/space/work> --json
279
+ cohub works stats <workId|url|username/space/work>
279
280
  cohub works download <workId|url|username/space/work> --output <path>
280
281
  cohub -s <spaceId> works publish demo --file dist/index.html
281
282
  cohub -s <spaceId> works publish site --dir dist
@@ -291,11 +292,54 @@ Resolve a published Work by public identity:
291
292
  cohub works resolve <workSlug> --owner <username> --space-slug <spaceSlug>
292
293
  ```
293
294
 
294
- Use `--json` for machine-readable output. `works get` and `works download` also accept `cohub://works/<username>/<space>/<work>` mention URIs. Download restores newly published file and directory artifacts directly from the CDN with checksum verification. HTML files with companion assets are restored as directory bundles; Board and port Works are not downloadable. The resolve command remains available for explicit slug-based lookup.
295
+ Use `--json` for machine-readable output. `works get`, `works stats`, and `works download` also accept `cohub://works/<username>/<space>/<work>` mention URIs. `works stats` reports total, 24-hour, 7-day, and 30-day views with a source breakdown. Download restores newly published file and directory artifacts directly from the CDN with checksum verification. HTML files with companion assets are restored as directory bundles; Board and port Works are not downloadable. The resolve command remains available for explicit slug-based lookup.
295
296
 
296
297
  Realtime rooms use a published Work's runtime identity, so they are available
297
298
  through `client.work.realtime` in the SDK rather than as CLI commands.
298
299
 
300
+ ## Drive the Cohub UI
301
+
302
+ Show a Work preview in the Cohub tab that started the current work, and call
303
+ methods the Work exposes.
304
+
305
+ ```bash
306
+ cohub ui preview <workId|url|cohub://works/...|username/space/work>
307
+ cohub ui preview <work> --call selection.get
308
+ cohub ui preview <work> --call board.focus --data '{"nodeId":"n1"}'
309
+ cohub ui preview <work> --call report.build --input payload.json --json
310
+ ```
311
+
312
+ `ui preview` accepts the same Work references as `works get`. Showing a preview is
313
+ idempotent: repeating it re-activates the same tab and refreshes any launch state
314
+ carried by the reference. With `--call`, the command waits for the Work to announce readiness, invokes the method,
315
+ and waits for the Work to complete the same UI command with `client.ui.reportResult()`.
316
+
317
+ Work authors decide what is callable by registering handlers inside the Work:
318
+
319
+ ```ts
320
+ client.work.surface.handle("image.open", async (input, { commandId }) => {
321
+ openImageStudio(input, commandId);
322
+ });
323
+ ```
324
+
325
+ A Work answers only a Cohub app origin, so a third-party site that embeds it
326
+ cannot invoke these methods.
327
+
328
+ Retrying with the same `--command-id` re-delivers the command rather than
329
+ returning a stale pending record, which recovers a dispatch that never reached the
330
+ browser. The frontend dedupes by command id, so ordinary redelivery does not run
331
+ twice.
332
+
333
+ Delivery is at-least-once. Deduplication lives in the receiving tab's memory, so a
334
+ retry that spans a page reload can run a `--call` method a second time. Prefer
335
+ methods that are safe to repeat.
336
+
337
+ Commands reach only the frontend instance that originated the current work,
338
+ resolved from request provenance (`X-Cohub-Source-Client`, propagated into the
339
+ Sandbox as `COHUB_SOURCE_CLIENT_ID`). They cannot target another user's browser,
340
+ and offer no DOM access or script evaluation. Pass `--client` to address a
341
+ specific instance of your own account, and `--command-id` to make retries safe.
342
+
299
343
  ## Saves
300
344
 
301
345
  ```bash
@@ -0,0 +1,2 @@
1
+ import type { Command } from "commander";
2
+ export declare function registerUi(program: Command): void;
@@ -0,0 +1,140 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { parseWorkRef, UI_COMMAND_DEFAULT_TIMEOUT_MS, UI_COMMAND_MAX_TIMEOUT_MS, } from "@neta-art/cohub";
3
+ import { createClient } from "../client.js";
4
+ import { error, handleHttp, json as outJson, jsonRequested, ok } from "../output.js";
5
+ import { getWorkByRef } from "../work-ref.js";
6
+ function readCallInput(opts) {
7
+ if (opts.data !== undefined && opts.input !== undefined) {
8
+ return error("Conflicting input", "Use either --data or --input, not both.");
9
+ }
10
+ const raw = opts.data !== undefined
11
+ ? opts.data
12
+ : opts.input !== undefined
13
+ ? opts.input === "-"
14
+ ? readFileSync(0, "utf-8")
15
+ : readFileSync(opts.input, "utf-8")
16
+ : undefined;
17
+ if (raw === undefined)
18
+ return undefined;
19
+ try {
20
+ return JSON.parse(raw);
21
+ }
22
+ catch {
23
+ return error("Invalid input", "Call input must be valid JSON.");
24
+ }
25
+ }
26
+ function parseTimeout(value) {
27
+ if (!value)
28
+ return UI_COMMAND_DEFAULT_TIMEOUT_MS;
29
+ const parsed = Math.floor(Number(value));
30
+ if (!Number.isFinite(parsed) || parsed <= 0 || parsed > UI_COMMAND_MAX_TIMEOUT_MS) {
31
+ return error("Invalid timeout", `--timeout-ms must be between 1 and ${UI_COMMAND_MAX_TIMEOUT_MS} milliseconds.`);
32
+ }
33
+ return parsed;
34
+ }
35
+ async function resolveWorkTarget(client, ref) {
36
+ const parsed = parseWorkRef(ref);
37
+ const detail = await getWorkByRef(client, ref);
38
+ return {
39
+ workId: detail.work.id,
40
+ label: detail.work.slug,
41
+ launch: {
42
+ ...(parsed.search ? { search: parsed.search } : {}),
43
+ ...(parsed.hash ? { hash: parsed.hash } : {}),
44
+ },
45
+ };
46
+ }
47
+ function reportDispatch(record) {
48
+ if (record.status !== "pending") {
49
+ reportOutcome(record, Boolean(record.command.request));
50
+ return;
51
+ }
52
+ ok(`UI command dispatched (${record.commandId})`);
53
+ }
54
+ function reportOutcome(record, called) {
55
+ if (record.status === "applied") {
56
+ ok(called ? "Work preview shown and method called" : "Work preview shown");
57
+ if (record.result !== undefined) {
58
+ console.log(typeof record.result === "string" ? record.result : JSON.stringify(record.result, null, 2));
59
+ }
60
+ return;
61
+ }
62
+ error(`UI command ${record.status}`, record.error?.message ?? "The Cohub frontend did not apply this command.");
63
+ }
64
+ export function registerUi(program) {
65
+ const ui = program
66
+ .command("ui")
67
+ .description("Drive the Cohub frontend that started this work")
68
+ .addHelpText("after", `
69
+ Commands reach only the frontend instance that originated the current chat,
70
+ resolved from request provenance. Nothing else can be targeted.
71
+
72
+ Examples:
73
+ cohub ui preview <work-id>
74
+ cohub ui preview alice/studio/launch
75
+ cohub ui preview https://cohub.run/alice/studio/w/launch?view=timeline
76
+ cohub ui preview <work-id> --call selection.get
77
+ cohub ui preview <work-id> --call board.focus --data '{"nodeId":"n1"}'
78
+ `);
79
+ const preview = ui
80
+ .command("preview <work>")
81
+ .description("Show a Work preview tab, optionally calling a method it exposes")
82
+ .option("--call <method>", "Method the Work registered via client.work.surface.handle()")
83
+ .option("--data <json>", "Inline JSON input for --call")
84
+ .option("-i, --input <file>", "JSON input file for --call; use - for stdin")
85
+ .option("--client <clientId>", "Target a specific frontend instance of your account")
86
+ .option("--command-id <id>", "Stable id so retries never dispatch twice")
87
+ .option("--no-wait", "Dispatch the command and exit without waiting for a result")
88
+ .option("--timeout-ms <ms>", `How long to wait for the frontend (default: ${UI_COMMAND_DEFAULT_TIMEOUT_MS}; max: ${UI_COMMAND_MAX_TIMEOUT_MS})`)
89
+ .option("--json", "Output as JSON")
90
+ .action(async (work, opts) => {
91
+ const callInput = opts.call ? readCallInput(opts) : undefined;
92
+ if (!opts.call && (opts.data !== undefined || opts.input !== undefined)) {
93
+ return error("Missing --call", "--data and --input only apply together with --call.");
94
+ }
95
+ if (opts.noWait && opts.timeoutMs !== undefined) {
96
+ return error("Conflicting wait options", "Use either --no-wait or --timeout-ms, not both.");
97
+ }
98
+ const timeoutMs = parseTimeout(opts.timeoutMs);
99
+ const client = createClient();
100
+ try {
101
+ const target = await resolveWorkTarget(client, work);
102
+ const request = opts.call
103
+ ? { method: opts.call, ...(callInput === undefined ? {} : { input: callInput }) }
104
+ : undefined;
105
+ const input = {
106
+ command: {
107
+ type: "preview.show",
108
+ preview: {
109
+ kind: "work",
110
+ workId: target.workId,
111
+ label: target.label,
112
+ ...(target.launch.search || target.launch.hash ? { launch: target.launch } : {}),
113
+ },
114
+ ...(request ? { request } : {}),
115
+ },
116
+ ...(opts.client ? { targetClientId: opts.client } : {}),
117
+ ...(opts.commandId ? { commandId: opts.commandId } : {}),
118
+ };
119
+ const record = opts.noWait
120
+ ? (await client.ui.create(input)).command
121
+ : await client.ui.run(input, { timeoutMs });
122
+ if (jsonRequested(opts))
123
+ return outJson(record);
124
+ if (opts.noWait)
125
+ return reportDispatch(record);
126
+ reportOutcome(record, Boolean(request));
127
+ }
128
+ catch (e) {
129
+ if (e instanceof Error && e.name === "WorkRefParseError")
130
+ return error(e.message);
131
+ handleHttp(e);
132
+ }
133
+ });
134
+ preview.addHelpText("after", `
135
+ Notes:
136
+ - Showing a preview is idempotent; repeating it re-activates the same tab.
137
+ - --call waits for the Work to announce readiness, then invokes the method.
138
+ - Which methods exist is up to the Work author.
139
+ `);
140
+ }
@@ -1,2 +1,4 @@
1
+ import { type CohubHttpClient, type WorkViewStatsResponse } from "@neta-art/cohub";
1
2
  import type { Command } from "commander";
3
+ export declare function getWorkStatsByRef(client: CohubHttpClient, work: string): Promise<WorkViewStatsResponse>;
2
4
  export declare function registerWorks(program: Command): void;
@@ -3,7 +3,7 @@ import { createClient } from "../client.js";
3
3
  import { error, handleHttp, json as outJson, jsonRequested, ok, table } from "../output.js";
4
4
  import { resolveSpace } from "../space.js";
5
5
  import { downloadWork } from "../work-download.js";
6
- import { getWorkByRef } from "../work-ref.js";
6
+ import { getWorkByRef, parseWorkRef } from "../work-ref.js";
7
7
  import { registerWorkCommerce } from "./work-commerce.js";
8
8
  const WORK_STATUSES = ["published", "disabled"];
9
9
  const WORK_VISIBILITIES = ["public", "space"];
@@ -87,6 +87,28 @@ function printWorkUrls(result) {
87
87
  if (lines.length)
88
88
  console.log(`\n${lines.join("\n")}`);
89
89
  }
90
+ function printWorkStats(stats) {
91
+ table([stats.summary], [
92
+ { key: "totalViews", label: "Total" },
93
+ { key: "views24h", label: "24h" },
94
+ { key: "views7d", label: "7d" },
95
+ { key: "views30d", label: "30d" },
96
+ ]);
97
+ if (stats.sources.length) {
98
+ console.log("");
99
+ table(stats.sources, [
100
+ { key: "source", label: "Source" },
101
+ { key: "views", label: "Views" },
102
+ ]);
103
+ }
104
+ }
105
+ export async function getWorkStatsByRef(client, work) {
106
+ const ref = parseWorkRef(work);
107
+ if ("id" in ref)
108
+ return client.works.getStats(ref.id);
109
+ const detail = await getWorkByRef(client, work);
110
+ return client.works.getStats(detail.work.id);
111
+ }
90
112
  async function confirmDelete(opts) {
91
113
  if (opts.yes)
92
114
  return;
@@ -161,6 +183,22 @@ export function registerWorks(program) {
161
183
  handleHttp(e);
162
184
  }
163
185
  });
186
+ worksCmd
187
+ .command("stats <work>")
188
+ .description("Show view statistics by id, URL, mention URI, or username/space/work")
189
+ .option("--json", "Output as JSON")
190
+ .action(async (work, opts) => {
191
+ const client = createClient();
192
+ try {
193
+ const result = await getWorkStatsByRef(client, work);
194
+ if (jsonRequested(opts))
195
+ return outJson(result);
196
+ printWorkStats(result);
197
+ }
198
+ catch (e) {
199
+ handleHttp(e);
200
+ }
201
+ });
164
202
  worksCmd
165
203
  .command("download <work>")
166
204
  .description("Download a published file or directory Work")
package/dist/index.js CHANGED
@@ -18,6 +18,7 @@ import { registerPrompt, registerSpaces } from "./commands/spaces.js";
18
18
  import { maybeHandleRunCommand } from "./commands/run.js";
19
19
  import { registerSandbox } from "./commands/sandbox.js";
20
20
  import { registerTasks } from "./commands/tasks.js";
21
+ import { registerUi } from "./commands/ui.js";
21
22
  import { registerWorks } from "./commands/works.js";
22
23
  import { ensureCliSelfUpdated } from "./self-update.js";
23
24
  const VERSION = (() => {
@@ -54,6 +55,7 @@ Common commands:
54
55
  cohub -s <space-id> spaces sessions turns ls <session-id>
55
56
  cohub -s <space-id> spaces files ls
56
57
  cohub -s <space-id> works publish demo --file dist/index.html
58
+ cohub ui preview <work-id> --call selection.get
57
59
  cohub -s <space-id> spaces commerce products list
58
60
  cohub models ls
59
61
  cohub models ls --model-type multimodal
@@ -81,6 +83,7 @@ registerReferrals(program);
81
83
  registerTasks(program);
82
84
  registerCronJobs(program);
83
85
  registerWorks(program);
86
+ registerUi(program);
84
87
  const isVersionRequest = (argv) => argv.some((arg) => arg === "-v" || arg === "--version");
85
88
  try {
86
89
  const argv = process.argv.slice(2);
@@ -1,13 +1,5 @@
1
- import type { CohubHttpClient, WorkGetResponse } from "@neta-art/cohub";
2
- type WorkPublicRef = {
3
- username: string;
4
- spaceSlug: string;
5
- workSlug: string;
6
- };
7
- export type ParsedWorkRef = {
8
- id: string;
9
- } | WorkPublicRef;
10
- export declare function parseWorkRef(input: string): ParsedWorkRef;
11
- export declare function formatWorkRef(ref: ParsedWorkRef): string;
1
+ import type { CohubHttpClient, ParsedWorkRef, WorkGetResponse } from "@neta-art/cohub";
2
+ import { formatWorkRef, parseWorkRef } from "@neta-art/cohub";
3
+ export type { ParsedWorkRef };
4
+ export { formatWorkRef, parseWorkRef };
12
5
  export declare function getWorkByRef(client: CohubHttpClient, input: string): Promise<WorkGetResponse>;
13
- export {};
package/dist/work-ref.js CHANGED
@@ -1,58 +1,5 @@
1
- const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
2
- const USERNAME_PATTERN = /^(?!-)(?!.*--)[a-z0-9-]{1,39}(?<!-)$/;
3
- const SLUG_PATTERN = /^[a-z0-9](?:[a-z0-9_-]{0,78}[a-z0-9])?$/;
4
- function decodePart(value) {
5
- try {
6
- return decodeURIComponent(value).trim();
7
- }
8
- catch {
9
- return "";
10
- }
11
- }
12
- function publicRef(parts) {
13
- if (parts.length !== 3)
14
- return null;
15
- const [username = "", spaceSlug = "", workSlug = ""] = parts.map(decodePart);
16
- return USERNAME_PATTERN.test(username) && SLUG_PATTERN.test(spaceSlug) && SLUG_PATTERN.test(workSlug)
17
- ? { username, spaceSlug, workSlug }
18
- : null;
19
- }
20
- function parseUrlRef(value) {
21
- let url;
22
- try {
23
- url = new URL(value);
24
- }
25
- catch {
26
- return null;
27
- }
28
- const parts = url.pathname.split("/").filter(Boolean);
29
- if (url.protocol === "cohub:" && url.hostname === "works")
30
- return publicRef(parts) ?? null;
31
- if (url.protocol !== "http:" && url.protocol !== "https:")
32
- return null;
33
- if (parts.length === 4 && parts[0] === "spaces" && UUID_PATTERN.test(parts[1] ?? "") && parts[2] === "works" && UUID_PATTERN.test(parts[3] ?? "")) {
34
- return { id: parts[3] };
35
- }
36
- if (parts.length === 4 && parts[2] === "w")
37
- return publicRef([parts[0], parts[1], parts[3]]);
38
- return null;
39
- }
40
- export function parseWorkRef(input) {
41
- const value = input.trim();
42
- if (UUID_PATTERN.test(value))
43
- return { id: value };
44
- const parsedUrl = parseUrlRef(value.includes("://") ? value : value.startsWith("/") ? `https://cohub.invalid${value}` : value);
45
- if (parsedUrl)
46
- return parsedUrl;
47
- const parts = value.split("/").filter(Boolean);
48
- const parsedPublic = parts.length === 3 ? publicRef(parts) : null;
49
- if (parsedPublic)
50
- return parsedPublic;
51
- throw new Error("Work must be an id, public URL, cohub://works URI, or username/space/work reference");
52
- }
53
- export function formatWorkRef(ref) {
54
- return "id" in ref ? ref.id : `${ref.username}/${ref.spaceSlug}/${ref.workSlug}`;
55
- }
1
+ import { formatWorkRef, parseWorkRef } from "@neta-art/cohub";
2
+ export { formatWorkRef, parseWorkRef };
56
3
  export function getWorkByRef(client, input) {
57
4
  const ref = parseWorkRef(input);
58
5
  return "id" in ref
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@neta-art/cohub-cli",
3
- "version": "3.6.4",
3
+ "version": "3.8.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",
18
+ "@neta-art/generation": "^0.1.19",
19
19
  "commander": "^15.0.0",
20
20
  "pixi.js": "^8.19.0",
21
21
  "sharp": "^0.35.3",
22
- "@neta-art/cohub": "5.2.0"
22
+ "@neta-art/cohub": "5.3.0"
23
23
  },
24
24
  "publishConfig": {
25
25
  "access": "public"