@neta-art/cohub-cli 3.7.0 → 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 +43 -0
- package/dist/commands/ui.d.ts +2 -0
- package/dist/commands/ui.js +140 -0
- package/dist/index.js +3 -0
- package/dist/work-ref.d.ts +4 -12
- package/dist/work-ref.js +2 -55
- package/package.json +2 -2
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,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);
|
package/dist/work-ref.d.ts
CHANGED
|
@@ -1,13 +1,5 @@
|
|
|
1
|
-
import type { CohubHttpClient, WorkGetResponse } from "@neta-art/cohub";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
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
|
-
|
|
2
|
-
|
|
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.
|
|
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",
|
|
@@ -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.
|
|
22
|
+
"@neta-art/cohub": "5.3.0"
|
|
23
23
|
},
|
|
24
24
|
"publishConfig": {
|
|
25
25
|
"access": "public"
|