@neta-art/cohub-cli 5.0.0 → 6.0.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.
@@ -0,0 +1,219 @@
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";
4
+ import { createClient } from "../client.js";
5
+ import { error, handleHttp, json as outJson, jsonRequested, ok } from "../output.js";
6
+ import { getAppByRef } from "../app-ref.js";
7
+ const FILE_SCHEME = "file://";
8
+ const APP_SCHEME = "app://";
9
+ const LEGACY_WORK_SCHEME = "work://";
10
+ function optionalSpaceId(command) {
11
+ let current = command;
12
+ while (current) {
13
+ const opts = current.opts();
14
+ if (typeof opts.space === "string" && opts.space.trim())
15
+ return opts.space.trim();
16
+ current = current.parent ?? null;
17
+ }
18
+ return process.env.COHUB_SPACE_ID?.trim() || undefined;
19
+ }
20
+ function parseFilePath(value) {
21
+ const path = value.slice(FILE_SCHEME.length).trim();
22
+ if (!path ||
23
+ path.startsWith("/") ||
24
+ path.includes("\0") ||
25
+ path.split("/").some((segment) => segment === "..") ||
26
+ path.includes("\\")) {
27
+ return error("Invalid file path", "Use a relative Space path after file://.");
28
+ }
29
+ return path;
30
+ }
31
+ function hasFileScheme(value) {
32
+ return value.toLowerCase().startsWith(FILE_SCHEME);
33
+ }
34
+ function hasAppScheme(value) {
35
+ const lowered = value.toLowerCase();
36
+ return lowered.startsWith(APP_SCHEME) || lowered.startsWith(LEGACY_WORK_SCHEME);
37
+ }
38
+ function readCallInput(opts) {
39
+ if (opts.data !== undefined && opts.input !== undefined) {
40
+ return error("Conflicting input", "Use either --data or --input, not both.");
41
+ }
42
+ const raw = opts.data !== undefined
43
+ ? opts.data
44
+ : opts.input !== undefined
45
+ ? opts.input === "-"
46
+ ? readFileSync(0, "utf-8")
47
+ : readFileSync(opts.input, "utf-8")
48
+ : undefined;
49
+ if (raw === undefined)
50
+ return undefined;
51
+ try {
52
+ return JSON.parse(raw);
53
+ }
54
+ catch {
55
+ return error("Invalid input", "Call input must be valid JSON.");
56
+ }
57
+ }
58
+ function parseTimeout(value) {
59
+ if (!value)
60
+ return DESKTOP_COMMAND_DEFAULT_TIMEOUT_MS;
61
+ const parsed = Math.floor(Number(value));
62
+ if (!Number.isFinite(parsed) || parsed <= 0 || parsed > DESKTOP_COMMAND_MAX_TIMEOUT_MS) {
63
+ return error("Invalid timeout", `--timeout-ms must be between 1 and ${DESKTOP_COMMAND_MAX_TIMEOUT_MS} milliseconds.`);
64
+ }
65
+ return parsed;
66
+ }
67
+ async function resolveAppTarget(client, ref) {
68
+ const normalized = hasAppScheme(ref) ? ref.replace(/^[a-zA-Z]+:\/\//, "") : ref;
69
+ const parsed = parseAppRef(normalized);
70
+ const detail = await getAppByRef(client, normalized);
71
+ return {
72
+ kind: "app",
73
+ appId: detail.app.id,
74
+ label: detail.app.slug,
75
+ launch: {
76
+ ...(parsed.search ? { search: parsed.search } : {}),
77
+ ...(parsed.hash ? { hash: parsed.hash } : {}),
78
+ },
79
+ };
80
+ }
81
+ async function resolveOpenTarget(client, command, value) {
82
+ if (hasFileScheme(value))
83
+ return { kind: "file", path: parseFilePath(value) };
84
+ if (hasAppScheme(value))
85
+ return resolveAppTarget(client, value);
86
+ const spaceId = optionalSpaceId(command);
87
+ if (spaceId) {
88
+ try {
89
+ await client.space(spaceId).files.read(value);
90
+ return { kind: "file", path: value };
91
+ }
92
+ catch (cause) {
93
+ if (!(cause instanceof HttpError) || cause.status !== 404)
94
+ throw cause;
95
+ }
96
+ }
97
+ return resolveAppTarget(client, value);
98
+ }
99
+ function reportDispatch(record) {
100
+ if (record.status !== "pending") {
101
+ reportOutcome(record, Boolean(record.command.call));
102
+ return;
103
+ }
104
+ ok(`Desktop command dispatched (${record.commandId})`);
105
+ }
106
+ function reportOutcome(record, called) {
107
+ if (record.status === "applied") {
108
+ ok(called ? "App window shown and method called" : "Window shown");
109
+ if (record.result !== undefined) {
110
+ console.log(typeof record.result === "string" ? record.result : JSON.stringify(record.result, null, 2));
111
+ }
112
+ return;
113
+ }
114
+ error(`Desktop command ${record.status}`, record.error?.message ?? "The Cohub desktop did not apply this command.");
115
+ }
116
+ async function openWindow(target, opts, command) {
117
+ const callInput = opts.call ? readCallInput(opts) : undefined;
118
+ if (!opts.call && (opts.data !== undefined || opts.input !== undefined)) {
119
+ return error("Missing --call", "--data and --input only apply together with --call.");
120
+ }
121
+ if (opts.noWait && opts.timeoutMs !== undefined) {
122
+ return error("Conflicting wait options", "Use either --no-wait or --timeout-ms, not both.");
123
+ }
124
+ const timeoutMs = parseTimeout(opts.timeoutMs);
125
+ const client = createClient();
126
+ try {
127
+ const resolved = await resolveOpenTarget(client, command, target);
128
+ if (resolved.kind === "file" && opts.call) {
129
+ return error("Unsupported option", "--call only applies to app targets.");
130
+ }
131
+ const call = opts.call
132
+ ? { method: opts.call, ...(callInput === undefined ? {} : { input: callInput }) }
133
+ : undefined;
134
+ const input = {
135
+ command: {
136
+ type: "desktop.open",
137
+ target: resolved.kind === "file"
138
+ ? resolved
139
+ : {
140
+ kind: "app",
141
+ appId: resolved.appId,
142
+ label: resolved.label,
143
+ ...(resolved.launch.search || resolved.launch.hash ? { launch: resolved.launch } : {}),
144
+ },
145
+ ...(call ? { call } : {}),
146
+ },
147
+ ...(opts.client ? { targetClientId: opts.client } : {}),
148
+ ...(opts.commandId ? { commandId: opts.commandId } : {}),
149
+ };
150
+ const record = opts.noWait
151
+ ? (await client.desktop.create(input)).command
152
+ : await client.desktop.run(input, { timeoutMs });
153
+ if (jsonRequested(opts))
154
+ return outJson(record);
155
+ if (opts.noWait)
156
+ return reportDispatch(record);
157
+ reportOutcome(record, Boolean(call));
158
+ }
159
+ catch (e) {
160
+ if (e instanceof Error && e.name === "AppRefParseError")
161
+ return error(e.message);
162
+ handleHttp(e);
163
+ }
164
+ }
165
+ const OPEN_HELP = `
166
+ Commands reach only the desktop that originated the current chat, resolved
167
+ from request provenance. Nothing else can be targeted.
168
+
169
+ Examples:
170
+ cohub desktop open <app-or-file>
171
+ cohub desktop open file://src/main.ts
172
+ cohub desktop open app://alice/studio/launch
173
+ cohub desktop open alice/studio/launch
174
+ cohub desktop open https://cohub.live/alice/studio/w/launch?view=timeline
175
+ cohub desktop open <app-id> --call selection.get
176
+ cohub desktop open <app-id> --call board.focus --data '{"nodeId":"n1"}'
177
+ `;
178
+ const OPEN_NOTES = `
179
+ Notes:
180
+ - Use file:// and app:// to make the target explicit; the legacy work://
181
+ scheme is still accepted.
182
+ - A plain target checks the current Space for a file before resolving an app.
183
+ - Opening a window is idempotent; repeating it re-activates the same tab.
184
+ - --call waits for the app to announce readiness, then invokes the method.
185
+ - Which methods exist is up to the app author.
186
+ `;
187
+ function registerOpen(parent, deprecated) {
188
+ const open = parent
189
+ .command(deprecated ? "preview <app-or-file>" : "open <app-or-file>")
190
+ .description(deprecated ? "Deprecated: use `cohub desktop open`" : "Open a file or app window on the Cohub desktop, optionally calling an app method")
191
+ .option("--call <method>", "Method the app registered via client.app.surface.handle()")
192
+ .option("--data <json>", "Inline JSON input for --call")
193
+ .option("-i, --input <file>", "JSON input file for --call; use - for stdin")
194
+ .option("--client <clientId>", "Target a specific desktop instance of your account")
195
+ .option("--command-id <id>", "Stable id so retries never dispatch twice")
196
+ .option("--no-wait", "Dispatch the command and exit without waiting for a result")
197
+ .option("--timeout-ms <ms>", `How long to wait for the desktop (default: ${DESKTOP_COMMAND_DEFAULT_TIMEOUT_MS}; max: ${DESKTOP_COMMAND_MAX_TIMEOUT_MS})`)
198
+ .option("--json", "Output as JSON")
199
+ .action(async (target, opts, thisCommand) => {
200
+ if (deprecated) {
201
+ ok("Deprecated: `cohub ui preview` is now `cohub desktop open`.");
202
+ }
203
+ await openWindow(target, opts, thisCommand);
204
+ });
205
+ open.addHelpText("after", deprecated ? OPEN_NOTES : OPEN_HELP + OPEN_NOTES);
206
+ }
207
+ export function registerDesktop(program) {
208
+ const desktop = program
209
+ .command("desktop")
210
+ .description("Drive the Cohub desktop that started this chat");
211
+ registerOpen(desktop, false);
212
+ }
213
+ export function registerLegacyUi(program) {
214
+ const ui = program
215
+ .command("ui", { hidden: true })
216
+ .description("Deprecated: use `cohub desktop open`")
217
+ .addHelpText("after", OPEN_HELP);
218
+ registerOpen(ui, true);
219
+ }
@@ -16,7 +16,7 @@ export function registerProfile(program) {
16
16
  const handle = result.profile.username ? `@${result.profile.username}` : result.profile.userUuid;
17
17
  console.log(`\n ${result.profile.displayName} (${handle})`);
18
18
  console.log(` Spaces: ${result.spaces.length}`);
19
- console.log(` Works: ${result.works.length}\n`);
19
+ console.log(` Apps: ${result.apps.length}\n`);
20
20
  if (result.spaces.length > 0) {
21
21
  console.log(" Spaces:");
22
22
  for (const space of result.spaces) {
@@ -24,10 +24,10 @@ export function registerProfile(program) {
24
24
  }
25
25
  console.log("");
26
26
  }
27
- if (result.works.length > 0) {
28
- console.log(" Works:");
29
- for (const work of result.works) {
30
- console.log(` - ${work.title} ${work.publicUrl}`);
27
+ if (result.apps.length > 0) {
28
+ console.log(" Apps:");
29
+ for (const app of result.apps) {
30
+ console.log(` - ${app.title} ${app.publicUrl}`);
31
31
  }
32
32
  console.log("");
33
33
  }
package/dist/index.js CHANGED
@@ -19,8 +19,8 @@ import { registerPrompt, registerSpaces } from "./commands/spaces.js";
19
19
  import { maybeHandleRunCommand } from "./commands/run.js";
20
20
  import { registerSandbox } from "./commands/sandbox.js";
21
21
  import { registerTasks } from "./commands/tasks.js";
22
- import { registerUi } from "./commands/ui.js";
23
- import { registerWorks } from "./commands/works.js";
22
+ import { registerDesktop, registerLegacyUi } from "./commands/desktop.js";
23
+ import { registerApps } from "./commands/apps.js";
24
24
  const VERSION = (() => {
25
25
  try {
26
26
  const pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf-8"));
@@ -55,8 +55,8 @@ Common commands:
55
55
  cohub -s <space-id> spaces sessions turns ls <session-id>
56
56
  cohub -s <space-id> spaces files ls
57
57
  cohub -s <space-id> public upload ./dist demo
58
- cohub -s <space-id> works publish demo --file dist/index.html
59
- cohub ui preview <work-id> --call selection.get
58
+ cohub -s <space-id> apps publish demo --file dist/index.html
59
+ cohub desktop open <app-id> --call selection.get
60
60
  cohub -s <space-id> spaces commerce products list
61
61
  cohub models ls
62
62
  cohub models ls --model-type multimodal
@@ -84,8 +84,9 @@ registerReferences(program);
84
84
  registerReferrals(program);
85
85
  registerTasks(program);
86
86
  registerCronJobs(program);
87
- registerWorks(program);
88
- registerUi(program);
87
+ registerApps(program);
88
+ registerDesktop(program);
89
+ registerLegacyUi(program);
89
90
  const argv = process.argv.slice(2);
90
91
  if (await maybeHandleRunCommand(argv)) {
91
92
  process.exit();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@neta-art/cohub-cli",
3
- "version": "5.0.0",
3
+ "version": "6.0.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": "7.0.0"
22
+ "@neta-art/cohub": "8.0.1"
23
23
  },
24
24
  "publishConfig": {
25
25
  "access": "public"
@@ -1,2 +0,0 @@
1
- import type { Command } from "commander";
2
- export declare function registerUi(program: Command): void;
@@ -1,199 +0,0 @@
1
- import { readFileSync } from "node:fs";
2
- import { HttpError, } from "@neta-art/cohub";
3
- import { parseWorkRef, UI_COMMAND_DEFAULT_TIMEOUT_MS, UI_COMMAND_MAX_TIMEOUT_MS, } from "@neta-art/cohub";
4
- import { createClient } from "../client.js";
5
- import { error, handleHttp, json as outJson, jsonRequested, ok } from "../output.js";
6
- import { getWorkByRef } from "../work-ref.js";
7
- const FILE_SCHEME = "file://";
8
- const WORK_SCHEME = "work://";
9
- function optionalSpaceId(command) {
10
- let current = command;
11
- while (current) {
12
- const opts = current.opts();
13
- if (typeof opts.space === "string" && opts.space.trim())
14
- return opts.space.trim();
15
- current = current.parent ?? null;
16
- }
17
- return process.env.COHUB_SPACE_ID?.trim() || undefined;
18
- }
19
- function parseFilePath(value) {
20
- const path = value.slice(FILE_SCHEME.length).trim();
21
- if (!path ||
22
- path.startsWith("/") ||
23
- path.includes("\0") ||
24
- path.split("/").some((segment) => segment === "..") ||
25
- path.includes("\\")) {
26
- return error("Invalid file path", "Use a relative Space path after file://.");
27
- }
28
- return path;
29
- }
30
- function hasFileScheme(value) {
31
- return value.toLowerCase().startsWith(FILE_SCHEME);
32
- }
33
- function hasWorkScheme(value) {
34
- return value.toLowerCase().startsWith(WORK_SCHEME);
35
- }
36
- function readCallInput(opts) {
37
- if (opts.data !== undefined && opts.input !== undefined) {
38
- return error("Conflicting input", "Use either --data or --input, not both.");
39
- }
40
- const raw = opts.data !== undefined
41
- ? opts.data
42
- : opts.input !== undefined
43
- ? opts.input === "-"
44
- ? readFileSync(0, "utf-8")
45
- : readFileSync(opts.input, "utf-8")
46
- : undefined;
47
- if (raw === undefined)
48
- return undefined;
49
- try {
50
- return JSON.parse(raw);
51
- }
52
- catch {
53
- return error("Invalid input", "Call input must be valid JSON.");
54
- }
55
- }
56
- function parseTimeout(value) {
57
- if (!value)
58
- return UI_COMMAND_DEFAULT_TIMEOUT_MS;
59
- const parsed = Math.floor(Number(value));
60
- if (!Number.isFinite(parsed) || parsed <= 0 || parsed > UI_COMMAND_MAX_TIMEOUT_MS) {
61
- return error("Invalid timeout", `--timeout-ms must be between 1 and ${UI_COMMAND_MAX_TIMEOUT_MS} milliseconds.`);
62
- }
63
- return parsed;
64
- }
65
- async function resolveWorkTarget(client, ref) {
66
- const normalized = hasWorkScheme(ref) ? ref.slice(WORK_SCHEME.length) : ref;
67
- const parsed = parseWorkRef(normalized);
68
- const detail = await getWorkByRef(client, normalized);
69
- return {
70
- kind: "work",
71
- workId: detail.work.id,
72
- label: detail.work.slug,
73
- launch: {
74
- ...(parsed.search ? { search: parsed.search } : {}),
75
- ...(parsed.hash ? { hash: parsed.hash } : {}),
76
- },
77
- };
78
- }
79
- async function resolvePreviewTarget(client, command, value) {
80
- if (hasFileScheme(value))
81
- return { kind: "file", path: parseFilePath(value) };
82
- if (hasWorkScheme(value))
83
- return resolveWorkTarget(client, value);
84
- const spaceId = optionalSpaceId(command);
85
- if (spaceId) {
86
- try {
87
- await client.space(spaceId).files.read(value);
88
- return { kind: "file", path: value };
89
- }
90
- catch (cause) {
91
- if (!(cause instanceof HttpError) || cause.status !== 404)
92
- throw cause;
93
- }
94
- }
95
- return resolveWorkTarget(client, value);
96
- }
97
- function reportDispatch(record) {
98
- if (record.status !== "pending") {
99
- reportOutcome(record, Boolean(record.command.request));
100
- return;
101
- }
102
- ok(`UI command dispatched (${record.commandId})`);
103
- }
104
- function reportOutcome(record, called) {
105
- if (record.status === "applied") {
106
- ok(called ? "Work preview shown and method called" : "Preview shown");
107
- if (record.result !== undefined) {
108
- console.log(typeof record.result === "string" ? record.result : JSON.stringify(record.result, null, 2));
109
- }
110
- return;
111
- }
112
- error(`UI command ${record.status}`, record.error?.message ?? "The Cohub frontend did not apply this command.");
113
- }
114
- export function registerUi(program) {
115
- const ui = program
116
- .command("ui")
117
- .description("Drive the Cohub frontend that started this work")
118
- .addHelpText("after", `
119
- Commands reach only the frontend instance that originated the current chat,
120
- resolved from request provenance. Nothing else can be targeted.
121
-
122
- Examples:
123
- cohub ui preview <work-or-file>
124
- cohub ui preview file://src/main.ts
125
- cohub ui preview work://alice/studio/launch
126
- cohub ui preview alice/studio/launch
127
- cohub ui preview https://cohub.live/alice/studio/w/launch?view=timeline
128
- cohub ui preview <work-id> --call selection.get
129
- cohub ui preview <work-id> --call board.focus --data '{"nodeId":"n1"}'
130
- `);
131
- const preview = ui
132
- .command("preview <work-or-file>")
133
- .description("Show a file or Work preview tab, optionally calling a Work method")
134
- .option("--call <method>", "Method the Work registered via client.work.surface.handle()")
135
- .option("--data <json>", "Inline JSON input for --call")
136
- .option("-i, --input <file>", "JSON input file for --call; use - for stdin")
137
- .option("--client <clientId>", "Target a specific frontend instance of your account")
138
- .option("--command-id <id>", "Stable id so retries never dispatch twice")
139
- .option("--no-wait", "Dispatch the command and exit without waiting for a result")
140
- .option("--timeout-ms <ms>", `How long to wait for the frontend (default: ${UI_COMMAND_DEFAULT_TIMEOUT_MS}; max: ${UI_COMMAND_MAX_TIMEOUT_MS})`)
141
- .option("--json", "Output as JSON")
142
- .action(async (work, opts) => {
143
- const callInput = opts.call ? readCallInput(opts) : undefined;
144
- if (!opts.call && (opts.data !== undefined || opts.input !== undefined)) {
145
- return error("Missing --call", "--data and --input only apply together with --call.");
146
- }
147
- if (opts.noWait && opts.timeoutMs !== undefined) {
148
- return error("Conflicting wait options", "Use either --no-wait or --timeout-ms, not both.");
149
- }
150
- const timeoutMs = parseTimeout(opts.timeoutMs);
151
- const client = createClient();
152
- try {
153
- const target = await resolvePreviewTarget(client, preview, work);
154
- if (target.kind === "file" && opts.call) {
155
- return error("Unsupported option", "--call only applies to Work previews.");
156
- }
157
- const request = opts.call
158
- ? { method: opts.call, ...(callInput === undefined ? {} : { input: callInput }) }
159
- : undefined;
160
- const input = {
161
- command: {
162
- type: "preview.show",
163
- preview: target.kind === "file"
164
- ? target
165
- : {
166
- kind: "work",
167
- workId: target.workId,
168
- label: target.label,
169
- ...(target.launch.search || target.launch.hash ? { launch: target.launch } : {}),
170
- },
171
- ...(request ? { request } : {}),
172
- },
173
- ...(opts.client ? { targetClientId: opts.client } : {}),
174
- ...(opts.commandId ? { commandId: opts.commandId } : {}),
175
- };
176
- const record = opts.noWait
177
- ? (await client.ui.create(input)).command
178
- : await client.ui.run(input, { timeoutMs });
179
- if (jsonRequested(opts))
180
- return outJson(record);
181
- if (opts.noWait)
182
- return reportDispatch(record);
183
- reportOutcome(record, Boolean(request));
184
- }
185
- catch (e) {
186
- if (e instanceof Error && e.name === "WorkRefParseError")
187
- return error(e.message);
188
- handleHttp(e);
189
- }
190
- });
191
- preview.addHelpText("after", `
192
- Notes:
193
- - Use file:// and work:// to make the target explicit.
194
- - A plain target checks the current Space for a file before resolving a Work.
195
- - Showing a preview is idempotent; repeating it re-activates the same tab.
196
- - --call waits for the Work to announce readiness, then invokes the method.
197
- - Which methods exist is up to the Work author.
198
- `);
199
- }
@@ -1,2 +0,0 @@
1
- import type { Command } from "commander";
2
- export declare function registerWorkCommerce(worksCmd: Command): void;
@@ -1,4 +0,0 @@
1
- import { type CohubHttpClient, type WorkViewStatsResponse } from "@neta-art/cohub";
2
- import type { Command } from "commander";
3
- export declare function getWorkStatsByRef(client: CohubHttpClient, work: string): Promise<WorkViewStatsResponse>;
4
- export declare function registerWorks(program: Command): void;
@@ -1,11 +0,0 @@
1
- import type { WorkGetResponse } from "@neta-art/cohub";
2
- type DownloadResult = {
3
- workId: string;
4
- version: number;
5
- kind: "file" | "directory";
6
- output: string;
7
- files: number;
8
- bytes: number;
9
- };
10
- export declare function downloadWork(detail: WorkGetResponse, outputOption?: string, fetcher?: typeof fetch): Promise<DownloadResult>;
11
- export {};
@@ -1,5 +0,0 @@
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 };
5
- export declare function getWorkByRef(client: CohubHttpClient, input: string): Promise<WorkGetResponse>;
package/dist/work-ref.js DELETED
@@ -1,8 +0,0 @@
1
- import { formatWorkRef, parseWorkRef } from "@neta-art/cohub";
2
- export { formatWorkRef, parseWorkRef };
3
- export function getWorkByRef(client, input) {
4
- const ref = parseWorkRef(input);
5
- return "id" in ref
6
- ? client.works.get(ref.id)
7
- : client.works.getBySlug(ref.username, ref.spaceSlug, ref.workSlug);
8
- }