@neta-art/cohub-cli 3.9.5 → 3.10.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
@@ -1,6 +1,6 @@
1
1
  # @neta-art/cohub-cli
2
2
 
3
- CLI for [Cohub](https://cohub.run) — work with Spaces, Chats, files, Saves, Tasks, scheduled prompts, search, and multimodal generation from your terminal.
3
+ CLI for [Cohub](https://cohub.live) — work with Spaces, Chats, files, Saves, Tasks, scheduled prompts, search, and multimodal generation from your terminal.
4
4
 
5
5
  ## Installation
6
6
 
@@ -158,7 +158,37 @@ cohub -s <spaceId> boards watch <boardId> --json
158
158
  ```
159
159
 
160
160
  Pass nodes, effects, and sequences as JSON when creating a Board. The path and
161
- title stay explicit in the command:
161
+ title stay explicit in the command. Inspect `boards capabilities --json` for the
162
+ supported node types, enums, and coordinate spaces.
163
+
164
+ ```json
165
+ {
166
+ "nodes": [
167
+ {
168
+ "nodeId": "goal",
169
+ "type": "geo",
170
+ "parentId": null,
171
+ "orderKey": null,
172
+ "x": 80,
173
+ "y": 80,
174
+ "width": 240,
175
+ "height": 120,
176
+ "rotation": 0,
177
+ "refKind": null,
178
+ "refPath": null,
179
+ "refUrl": null,
180
+ "view": {},
181
+ "style": {},
182
+ "data": {
183
+ "geo": "rectangle",
184
+ "text": "Ship",
185
+ "color": "green",
186
+ "fillOpacity": 0.12
187
+ }
188
+ }
189
+ ]
190
+ }
191
+ ```
162
192
 
163
193
  ```bash
164
194
  cohub -s <spaceId> boards create boards/plan.board \
@@ -347,6 +347,23 @@ export function registerBoards(program) {
347
347
  { key: "renderers", label: "Renderers" },
348
348
  { key: "digest", label: "Digest" },
349
349
  ]);
350
+ const nodes = result.nodes;
351
+ if (nodes) {
352
+ console.log();
353
+ table([{
354
+ types: nodes.types.join(", "),
355
+ colors: nodes.colors.join(", "),
356
+ geos: nodes.geos.join(", "),
357
+ drawPoints: nodes.coordinates.drawPoints,
358
+ arrowEndpoints: nodes.coordinates.arrowEndpoints,
359
+ }], [
360
+ { key: "types", label: "Node types" },
361
+ { key: "colors", label: "Colors" },
362
+ { key: "geos", label: "Geo kinds" },
363
+ { key: "drawPoints", label: "Draw points" },
364
+ { key: "arrowEndpoints", label: "Arrow endpoints" },
365
+ ]);
366
+ }
350
367
  }
351
368
  catch (cause) {
352
369
  handleHttp(cause);
@@ -43,17 +43,35 @@ export function registerMe(program) {
43
43
  }
44
44
  });
45
45
  meCmd
46
- .command("usage [days]")
47
- .description("Your aggregated usage across all spaces (default: 30 days)")
46
+ .command("activity")
47
+ .description("Show your activity across all spaces")
48
+ .option("--days <n>", "Show the last N days (default: 30)")
49
+ .option("--from <date>", "Start at this ISO 8601 date")
50
+ .option("--to <date>", "Stop before this ISO 8601 date")
48
51
  .option("--json", "Output as JSON")
49
- .action(async (days, opts) => {
52
+ .addHelpText("after", "\nExamples:\n $ cohub me activity --days 7\n $ cohub me activity --from 2026-01-01 --to 2026-02-01")
53
+ .action(async (opts) => {
54
+ if (opts.days && (opts.from || opts.to)) {
55
+ process.stderr.write("\n ✗ Invalid range\n --days cannot be combined with --from or --to\n\n");
56
+ process.exitCode = 1;
57
+ return;
58
+ }
59
+ if (opts.to && !opts.from) {
60
+ process.stderr.write("\n ✗ Invalid range\n --from is required when --to is provided\n\n");
61
+ process.exitCode = 1;
62
+ return;
63
+ }
50
64
  const client = createClient();
51
65
  try {
52
- const usage = await client.user.getUsage(days ? parseInteger(days, "days", 1) : 30);
66
+ const activity = await client.user.getActivity({
67
+ days: opts.days ? parseInteger(opts.days, "days", 1) : undefined,
68
+ from: opts.from,
69
+ to: opts.to,
70
+ });
53
71
  if (jsonRequested(opts))
54
- return outJson(usage);
55
- console.log("\n Summary:");
56
- table([usage.summary], [
72
+ return outJson(activity);
73
+ console.log(`\n ${activity.range.from} → ${activity.range.to}`);
74
+ table([activity.summary], [
57
75
  { key: "totalTokens", label: "Tokens" },
58
76
  { key: "costTotal", label: "Cost ($)" },
59
77
  { key: "requestCount", label: "Requests" },
@@ -0,0 +1,20 @@
1
+ import type { CohubHttpClient } from "@neta-art/cohub";
2
+ import type { Command } from "commander";
3
+ type LocalPublicFile = {
4
+ id: string;
5
+ localPath: string;
6
+ publicPath: string;
7
+ size: number;
8
+ mimeType: string;
9
+ };
10
+ type PublicCommandDeps = {
11
+ createClient?: () => CohubHttpClient;
12
+ fetch?: typeof fetch;
13
+ };
14
+ export declare function collectPublicUpload(source: string, destination?: string): Promise<{
15
+ files: LocalPublicFile[];
16
+ destination: string;
17
+ entryPath: string | null;
18
+ }>;
19
+ export declare function registerPublic(program: Command, deps?: PublicCommandDeps): Command;
20
+ export {};
@@ -0,0 +1,281 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { createReadStream } from "node:fs";
3
+ import { lstat, readdir } from "node:fs/promises";
4
+ import { basename, extname, relative, resolve } from "node:path";
5
+ import { createClient } from "../client.js";
6
+ import { error, handleHttp, json as outJson, jsonRequested } from "../output.js";
7
+ import { resolveSpace } from "../space.js";
8
+ const UPLOAD_CONCURRENCY = 4;
9
+ const MIME_TYPES = {
10
+ ".aac": "audio/aac",
11
+ ".avi": "video/x-msvideo",
12
+ ".avif": "image/avif",
13
+ ".css": "text/css; charset=utf-8",
14
+ ".csv": "text/csv; charset=utf-8",
15
+ ".gif": "image/gif",
16
+ ".htm": "text/html; charset=utf-8",
17
+ ".html": "text/html; charset=utf-8",
18
+ ".ico": "image/x-icon",
19
+ ".jpeg": "image/jpeg",
20
+ ".jpg": "image/jpeg",
21
+ ".js": "text/javascript; charset=utf-8",
22
+ ".json": "application/json; charset=utf-8",
23
+ ".m4a": "audio/mp4",
24
+ ".md": "text/markdown; charset=utf-8",
25
+ ".mov": "video/quicktime",
26
+ ".mp3": "audio/mpeg",
27
+ ".mp4": "video/mp4",
28
+ ".ogg": "audio/ogg",
29
+ ".ogv": "video/ogg",
30
+ ".pdf": "application/pdf",
31
+ ".png": "image/png",
32
+ ".svg": "image/svg+xml",
33
+ ".txt": "text/plain; charset=utf-8",
34
+ ".wasm": "application/wasm",
35
+ ".wav": "audio/wav",
36
+ ".webm": "video/webm",
37
+ ".webp": "image/webp",
38
+ ".woff": "font/woff",
39
+ ".woff2": "font/woff2",
40
+ ".xml": "application/xml; charset=utf-8",
41
+ ".zip": "application/zip",
42
+ };
43
+ function hasControlCharacters(value) {
44
+ return [...value].some((char) => {
45
+ const code = char.charCodeAt(0);
46
+ return code <= 0x1f || (code >= 0x7f && code <= 0x9f);
47
+ });
48
+ }
49
+ function terminalText(value) {
50
+ return [...value].map((char) => {
51
+ const code = char.charCodeAt(0);
52
+ return code <= 0x1f || (code >= 0x7f && code <= 0x9f)
53
+ ? `\\u${code.toString(16).padStart(4, "0")}`
54
+ : char;
55
+ }).join("");
56
+ }
57
+ function normalizePublicPath(input) {
58
+ const value = input.replace(/\\/g, "/").replace(/^\.\//, "").replace(/\/+$/, "");
59
+ if (!value || value.startsWith("/") || hasControlCharacters(value)) {
60
+ return error("Invalid public path");
61
+ }
62
+ const parts = value.split("/");
63
+ if (parts.some((part) => !part || part === "." || part === "..")) {
64
+ return error("Invalid public path");
65
+ }
66
+ return parts.join("/");
67
+ }
68
+ function mimeTypeForPath(path) {
69
+ return MIME_TYPES[extname(path).toLowerCase()] ?? "application/octet-stream";
70
+ }
71
+ async function walkDirectory(root, directory, destination) {
72
+ const names = await readdir(directory);
73
+ names.sort((left, right) => left.localeCompare(right));
74
+ const files = [];
75
+ for (const name of names) {
76
+ const localPath = resolve(directory, name);
77
+ const info = await lstat(localPath);
78
+ if (info.isSymbolicLink())
79
+ return error("Symlinks are not supported", relative(root, localPath));
80
+ if (info.isDirectory()) {
81
+ files.push(...await walkDirectory(root, localPath, destination));
82
+ continue;
83
+ }
84
+ if (!info.isFile())
85
+ continue;
86
+ const nestedPath = relative(root, localPath).replace(/\\/g, "/");
87
+ const publicPath = normalizePublicPath(`${destination}/${nestedPath}`);
88
+ files.push({
89
+ id: randomUUID(),
90
+ localPath,
91
+ publicPath,
92
+ size: info.size,
93
+ mimeType: mimeTypeForPath(localPath),
94
+ });
95
+ }
96
+ return files;
97
+ }
98
+ export async function collectPublicUpload(source, destination) {
99
+ const localPath = resolve(source);
100
+ const info = await lstat(localPath).catch(() => null);
101
+ if (!info)
102
+ return error("Source not found", source);
103
+ if (info.isSymbolicLink())
104
+ return error("Symlinks are not supported", source);
105
+ if (info.isFile()) {
106
+ const publicPath = normalizePublicPath(destination
107
+ ? destination.endsWith("/") ? `${destination}${basename(localPath)}` : destination
108
+ : basename(localPath));
109
+ return {
110
+ destination: publicPath,
111
+ entryPath: publicPath,
112
+ files: [{
113
+ id: randomUUID(),
114
+ localPath,
115
+ publicPath,
116
+ size: info.size,
117
+ mimeType: mimeTypeForPath(localPath),
118
+ }],
119
+ };
120
+ }
121
+ if (!info.isDirectory())
122
+ return error("Source must be a file or directory", source);
123
+ const target = normalizePublicPath(destination ?? basename(localPath));
124
+ const files = await walkDirectory(localPath, localPath, target);
125
+ if (files.length === 0)
126
+ return error("Directory contains no files", source);
127
+ const indexPath = `${target}/index.html`;
128
+ return {
129
+ files,
130
+ destination: `${target}/`,
131
+ entryPath: files.some((file) => file.publicPath === indexPath) ? indexPath : null,
132
+ };
133
+ }
134
+ async function mapSettledWithConcurrency(items, concurrency, mapper) {
135
+ const errors = [];
136
+ let nextIndex = 0;
137
+ const workers = Array.from({ length: Math.min(concurrency, items.length) }, async () => {
138
+ while (nextIndex < items.length) {
139
+ const index = nextIndex++;
140
+ try {
141
+ await mapper(items[index]);
142
+ }
143
+ catch (cause) {
144
+ errors.push(cause instanceof Error ? cause : new Error(String(cause)));
145
+ }
146
+ }
147
+ });
148
+ await Promise.all(workers);
149
+ return errors;
150
+ }
151
+ async function putPublicFile(file, plan, fetchImpl) {
152
+ const response = await fetchImpl(plan.uploadUrl, {
153
+ method: "PUT",
154
+ headers: plan.headers,
155
+ body: createReadStream(file.localPath),
156
+ duplex: "half",
157
+ });
158
+ if (response.ok)
159
+ return;
160
+ const detail = await response.text().catch(() => "");
161
+ if (response.status === 409 || response.status === 412) {
162
+ throw new Error(`${file.publicPath} already exists. Use --overwrite.`);
163
+ }
164
+ throw new Error(`Failed to upload ${file.publicPath}: HTTP ${response.status}${detail ? ` — ${detail}` : ""}`);
165
+ }
166
+ function uploadFailure(errors) {
167
+ if (errors.length === 1)
168
+ return errors[0];
169
+ const first = errors[0]?.message ?? "Upload failed";
170
+ return new Error(`${first} (${errors.length} files failed)`);
171
+ }
172
+ async function uploadPublic(command, source, destination, opts, deps) {
173
+ const client = deps.createClient?.() ?? createClient();
174
+ const spaceId = resolveSpace(command);
175
+ try {
176
+ const upload = await collectPublicUpload(source, destination);
177
+ const plan = await client.space(spaceId).publicFiles.createUpload({
178
+ overwrite: Boolean(opts.overwrite),
179
+ entries: upload.files.map((file) => ({
180
+ id: file.id,
181
+ relativePath: file.publicPath,
182
+ size: file.size,
183
+ mimeType: file.mimeType,
184
+ })),
185
+ });
186
+ if (opts.overwrite && !jsonRequested(opts)) {
187
+ process.stderr.write(`Overwrite enabled for ${upload.destination}\n`);
188
+ }
189
+ const filesById = new Map(upload.files.map((file) => [file.id, file]));
190
+ const plansByPath = new Map(plan.entries.map((entry) => [entry.path, entry]));
191
+ const entryPlan = upload.entryPath ? plansByPath.get(upload.entryPath) : undefined;
192
+ const assetPlans = entryPlan
193
+ ? plan.entries.filter((entry) => entry.id !== entryPlan.id)
194
+ : plan.entries;
195
+ const uploadPlans = async (entries) => mapSettledWithConcurrency(entries, UPLOAD_CONCURRENCY, async (entry) => {
196
+ const file = filesById.get(entry.id);
197
+ if (!file)
198
+ throw new Error(`Missing local file for ${entry.path}`);
199
+ await putPublicFile(file, entry, deps.fetch ?? fetch);
200
+ });
201
+ const assetErrors = await uploadPlans(assetPlans);
202
+ if (assetErrors.length > 0)
203
+ throw uploadFailure(assetErrors);
204
+ if (entryPlan) {
205
+ const entryErrors = await uploadPlans([entryPlan]);
206
+ if (entryErrors.length > 0)
207
+ throw uploadFailure(entryErrors);
208
+ }
209
+ const entryUrl = entryPlan?.publicUrl ?? null;
210
+ if (jsonRequested(opts)) {
211
+ outJson({
212
+ uploaded: upload.files.length,
213
+ overwrite: Boolean(opts.overwrite),
214
+ destination: upload.destination,
215
+ url: entryUrl,
216
+ });
217
+ return;
218
+ }
219
+ console.log(entryUrl ?? `Uploaded ${upload.files.length} files to ${upload.destination}`);
220
+ }
221
+ catch (exception) {
222
+ handleHttp(exception);
223
+ }
224
+ }
225
+ async function listPublic(command, path, opts, deps) {
226
+ const client = deps.createClient?.() ?? createClient();
227
+ try {
228
+ const publicFiles = client.space(resolveSpace(command)).publicFiles;
229
+ const entries = [];
230
+ let cursor;
231
+ do {
232
+ const page = await publicFiles.list(path ?? "", {
233
+ recursive: opts.recursive,
234
+ limit: 1000,
235
+ cursor,
236
+ });
237
+ if (jsonRequested(opts))
238
+ entries.push(...page.entries);
239
+ else
240
+ for (const entry of page.entries) {
241
+ console.log(`${terminalText(entry.name)}${entry.kind === "directory" ? "/" : ""}`);
242
+ }
243
+ cursor = page.nextCursor ?? undefined;
244
+ } while (cursor);
245
+ if (jsonRequested(opts))
246
+ return outJson({ path: path ?? "", entries, nextCursor: null });
247
+ }
248
+ catch (exception) {
249
+ handleHttp(exception);
250
+ }
251
+ }
252
+ async function printPublicUrl(command, path, deps) {
253
+ const client = deps.createClient?.() ?? createClient();
254
+ try {
255
+ const result = await client.space(resolveSpace(command)).publicFiles.url(path);
256
+ console.log(result.url);
257
+ }
258
+ catch (exception) {
259
+ handleHttp(exception);
260
+ }
261
+ }
262
+ export function registerPublic(program, deps = {}) {
263
+ const publicCommand = program
264
+ .command("public")
265
+ .description("Upload and manage public Space files");
266
+ publicCommand
267
+ .command("upload <source> [destination]")
268
+ .description("Upload a file or directory")
269
+ .option("--overwrite", "Replace existing public files")
270
+ .action((source, destination, opts, command) => uploadPublic(command, source, destination, opts, deps));
271
+ publicCommand
272
+ .command("ls [path]")
273
+ .description("List public files")
274
+ .option("-r, --recursive", "List files recursively")
275
+ .action((path, opts, command) => listPublic(command, path, opts, deps));
276
+ publicCommand
277
+ .command("url <path>")
278
+ .description("Print a public file URL")
279
+ .action((path, _opts, command) => printPublicUrl(command, path, deps));
280
+ return publicCommand;
281
+ }
@@ -1,7 +1,7 @@
1
1
  import { createClient } from "../client.js";
2
2
  import { error, handleHttp, json as outJson, jsonRequested, ok, table } from "../output.js";
3
3
  function referralUrl(code) {
4
- const origin = process.env.COHUB_WEB_URL?.replace(/\/+$/, "") ?? "https://cohub.run";
4
+ const origin = process.env.COHUB_WEB_URL?.replace(/\/+$/, "") ?? "https://cohub.live";
5
5
  return `${origin}/referrals/${code}`;
6
6
  }
7
7
  async function confirmRotate(opts) {
@@ -9,7 +9,7 @@ import { error, json as outJson, jsonRequested, ok, spinner } from "../output.js
9
9
  import { resolveSpace } from "../space.js";
10
10
  import { ensureSandboxdBinary, SandboxdDownloadError } from "./sandboxd-binary.js";
11
11
  // Derive the gateway relay control endpoint from the realtime websocket URL,
12
- // e.g. wss://gateway.cohub.run/ws -> wss://gateway.cohub.run/sandbox/relay.
12
+ // e.g. wss://gateway.cohub.live/ws -> wss://gateway.cohub.live/sandbox/relay.
13
13
  const resolveRelayUrl = () => {
14
14
  const explicit = process.env.COHUB_RELAY_URL?.trim();
15
15
  if (explicit)
@@ -27,7 +27,7 @@ const confirm = async (question) => {
27
27
  rl.close();
28
28
  }
29
29
  };
30
- const webBaseUrl = () => resolveCohubEnvironment() === "prod" ? "https://cohub.run" : "https://dev.cohub.run";
30
+ const webBaseUrl = () => resolveCohubEnvironment() === "prod" ? "https://cohub.live" : "https://dev.cohub.live";
31
31
  export const resolveLocalSpaceName = (rootDir, requestedName) => requestedName?.trim() || basename(rootDir) || "local-space";
32
32
  // Consent copy is deliberately explicit: a local sandbox runs agent-issued
33
33
  // shell commands as the current OS user. File RPCs are fenced to the folder,
@@ -34,7 +34,7 @@ export function parseSpaceInvitationCreateOptions(options) {
34
34
  return { role, ttlSeconds: days * 24 * 60 * 60, maxUses };
35
35
  }
36
36
  function invitationUrl(invitation) {
37
- const origin = process.env.COHUB_WEB_URL?.replace(/\/+$/, "") ?? "https://cohub.run";
37
+ const origin = process.env.COHUB_WEB_URL?.replace(/\/+$/, "") ?? "https://cohub.live";
38
38
  return `${origin}${buildSpaceInvitePath({
39
39
  spaceId: invitation.spaceId,
40
40
  ownerUsername: invitation.ownerUsername,
@@ -72,7 +72,7 @@ resolved from request provenance. Nothing else can be targeted.
72
72
  Examples:
73
73
  cohub ui preview <work-id>
74
74
  cohub ui preview alice/studio/launch
75
- cohub ui preview https://cohub.run/alice/studio/w/launch?view=timeline
75
+ cohub ui preview https://cohub.live/alice/studio/w/launch?view=timeline
76
76
  cohub ui preview <work-id> --call selection.get
77
77
  cohub ui preview <work-id> --call board.focus --data '{"nodeId":"n1"}'
78
78
  `);
package/dist/index.js CHANGED
@@ -10,6 +10,7 @@ import { registerMe } from "./commands/me.js";
10
10
  import { registerModels } from "./commands/models.js";
11
11
  import { registerProfile } from "./commands/profile.js";
12
12
  import { registerPrompts } from "./commands/prompts.js";
13
+ import { registerPublic } from "./commands/public.js";
13
14
  import { registerSkills } from "./commands/skills.js";
14
15
  import { registerSearch } from "./commands/search.js";
15
16
  import { registerReferences } from "./commands/references.js";
@@ -33,9 +34,9 @@ const program = new Command("cohub");
33
34
  program
34
35
  .name("cohub")
35
36
  .summary("Work with Cohub from your terminal")
36
- .description("Send prompts, inspect sessions, manage space files, and generate multimodal outputs.")
37
+ .description("Send prompts, manage Space files, and publish public output.")
37
38
  .version(VERSION, "-v, --version", "Show version")
38
- .option("-s, --space <id>", "Target space ID for prompt, files, sessions, and space-scoped commands")
39
+ .option("-s, --space <id>", "Target Space ID")
39
40
  .option("--json", "Print machine-readable JSON when supported")
40
41
  .helpOption("-h, --help", "Show help")
41
42
  .addHelpText("after", `
@@ -53,6 +54,7 @@ Common commands:
53
54
  cohub -s <space-id> spaces turns ls --author others
54
55
  cohub -s <space-id> spaces sessions turns ls <session-id>
55
56
  cohub -s <space-id> spaces files ls
57
+ cohub -s <space-id> public upload ./dist demo
56
58
  cohub -s <space-id> works publish demo --file dist/index.html
57
59
  cohub ui preview <work-id> --call selection.get
58
60
  cohub -s <space-id> spaces commerce products list
@@ -75,6 +77,7 @@ registerChannels(program);
75
77
  registerGenerations(program);
76
78
  registerModels(program);
77
79
  registerPrompts(program);
80
+ registerPublic(program);
78
81
  registerSkills(program);
79
82
  registerSearch(program);
80
83
  registerReferences(program);
package/dist/output.js CHANGED
@@ -112,11 +112,22 @@ function errorPresentationFromHttpError(e) {
112
112
  return null;
113
113
  }
114
114
  export function handleHttp(e) {
115
+ const status = e.status;
116
+ const body = e.body;
117
+ if (jsonRequested()) {
118
+ const payload = body && typeof body === "object"
119
+ ? body
120
+ : {
121
+ code: e.code ?? "CLI_ERROR",
122
+ message: e instanceof Error ? e.message : String(e),
123
+ ...(status ? { status } : {}),
124
+ };
125
+ process.stderr.write(`${JSON.stringify(payload, null, 2)}\n`);
126
+ process.exit(1);
127
+ }
115
128
  if (e instanceof Error && e.name === "AuthRequiredError") {
116
129
  return error("not authenticated", "run `cohub auth login`");
117
130
  }
118
- const status = e.status;
119
- const body = e.body;
120
131
  if (status === 402) {
121
132
  const conversion = extractBillingPayload(body)?.conversion;
122
133
  if (conversion) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@neta-art/cohub-cli",
3
- "version": "3.9.5",
3
+ "version": "3.10.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.6.0"
22
+ "@neta-art/cohub": "5.8.0"
23
23
  },
24
24
  "publishConfig": {
25
25
  "access": "public"