@neta-art/cohub-cli 3.7.0 → 3.8.1

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
@@ -297,6 +297,49 @@ Use `--json` for machine-readable output. `works get`, `works stats`, and `works
297
297
  Realtime rooms use a published Work's runtime identity, so they are available
298
298
  through `client.work.realtime` in the SDK rather than as CLI commands.
299
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
+
300
343
  ## Saves
301
344
 
302
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
+ }
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.7.0",
3
+ "version": "3.8.1",
4
4
  "description": "CLI for Cohub — spaces, sessions, and agent collaboration.",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -19,7 +19,7 @@
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.1"
23
23
  },
24
24
  "publishConfig": {
25
25
  "access": "public"