@neta-art/cohub-cli 2.7.0 → 3.0.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
@@ -121,6 +121,55 @@ cohub -s <spaceId> spaces sessions rename <sessionId> "<new title>"
121
121
 
122
122
  Use `spaces prompt --session <sessionId>` to send to a Chat.
123
123
 
124
+ ## Boards
125
+
126
+ Board commands use the selected Space and support `-h` at every level:
127
+
128
+ ```bash
129
+ cohub boards -h
130
+ cohub boards inspect -h
131
+ cohub -s <spaceId> boards create boards/plan.board --title "Plan"
132
+ cohub -s <spaceId> boards inspect <boardId> --json
133
+ cohub -s <spaceId> boards capabilities <boardId>
134
+ cohub -s <spaceId> boards watch <boardId> --json
135
+ ```
136
+
137
+ Pass nodes, effects, and sequences as JSON when creating a Board. The path and
138
+ title stay explicit in the command:
139
+
140
+ ```bash
141
+ cohub -s <spaceId> boards create boards/plan.board \
142
+ --title "Plan" \
143
+ --input board-content.json
144
+ ```
145
+
146
+ Transactions are JSON objects without `boardId`; the bound Board supplies it.
147
+ `txId` is generated when omitted, while `baseVersion` must be provided in the
148
+ input or with `--base-version`:
149
+
150
+ ```json
151
+ {
152
+ "baseVersion": 3,
153
+ "operations": [
154
+ {
155
+ "type": "board.patch",
156
+ "payload": { "patch": { "title": "Updated plan" } }
157
+ }
158
+ ]
159
+ }
160
+ ```
161
+
162
+ ```bash
163
+ cohub -s <spaceId> boards validate <boardId> --input transaction.json
164
+ cat transaction.json | cohub -s <spaceId> boards apply <boardId> --input - --json
165
+ cohub -s <spaceId> boards play <boardId> <sequenceId>
166
+ cohub -s <spaceId> boards seek <boardId> <playbackId> 400
167
+ cohub -s <spaceId> boards stop <boardId> <playbackId>
168
+ ```
169
+
170
+ Pass `--tx-id` or `--command-id` when a script needs a stable idempotency key
171
+ across retries.
172
+
124
173
  ## Search
125
174
 
126
175
  Search Spaces, Chats, and prior turns:
package/dist/client.d.ts CHANGED
@@ -1,2 +1,3 @@
1
- import { CohubHttpClient } from "@neta-art/cohub";
1
+ import { CohubClient, CohubHttpClient } from "@neta-art/cohub";
2
2
  export declare function createClient(): CohubHttpClient;
3
+ export declare function createRealtimeClient(): CohubClient;
package/dist/client.js CHANGED
@@ -1,11 +1,15 @@
1
- import { CohubHttpClient, readRequestSourceFromEnv } from "@neta-art/cohub";
1
+ import { CohubClient, CohubHttpClient, readRequestSourceFromEnv } from "@neta-art/cohub";
2
2
  import { clearAuthSession, resolveAccessToken } from "./auth.js";
3
+ const clientOptions = () => ({
4
+ getAccessToken: resolveAccessToken,
5
+ onUnauthorized: clearAuthSession,
6
+ requestSource: () => readRequestSourceFromEnv(process.env, { via: "cli" }) ?? {
7
+ via: "cli",
8
+ },
9
+ });
3
10
  export function createClient() {
4
- return new CohubHttpClient({
5
- getAccessToken: resolveAccessToken,
6
- onUnauthorized: clearAuthSession,
7
- requestSource: () => readRequestSourceFromEnv(process.env, { via: "cli" }) ?? {
8
- via: "cli",
9
- },
10
- });
11
+ return new CohubHttpClient(clientOptions());
12
+ }
13
+ export function createRealtimeClient() {
14
+ return new CohubClient(clientOptions());
11
15
  }
@@ -0,0 +1,14 @@
1
+ import type { BoardInspectInput, BoardTransactionInput } from "@neta-art/cohub";
2
+ import type { Command } from "commander";
3
+ declare const INSPECT_SECTIONS: readonly ["nodes", "effects", "sequences", "clips", "playback"];
4
+ type InspectSection = (typeof INSPECT_SECTIONS)[number];
5
+ export declare function parseJsonObject(text: string, source?: string): Record<string, unknown>;
6
+ export declare function readJsonObject(source: string): Promise<Record<string, unknown>>;
7
+ export declare function parseInspectSections(value?: string): InspectSection[] | undefined;
8
+ export declare function parseViewport(value?: string): BoardInspectInput["viewport"];
9
+ export declare function createTransactionInput(input: Record<string, unknown>, options: {
10
+ txId?: string;
11
+ baseVersion?: string;
12
+ }): BoardTransactionInput;
13
+ export declare function registerBoards(program: Command): Command;
14
+ export {};
@@ -0,0 +1,336 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { readFile } from "node:fs/promises";
3
+ import { createClient, createRealtimeClient } from "../client.js";
4
+ import { handleHttp, json as outJson, jsonRequested, ok, table } from "../output.js";
5
+ import { resolveSpace } from "../space.js";
6
+ const INSPECT_SECTIONS = ["nodes", "effects", "sequences", "clips", "playback"];
7
+ function isObject(value) {
8
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
9
+ }
10
+ export function parseJsonObject(text, source = "input") {
11
+ if (!text.trim())
12
+ throw new Error(`${source} is empty`);
13
+ let value;
14
+ try {
15
+ value = JSON.parse(text);
16
+ }
17
+ catch (cause) {
18
+ throw new Error(`${source} must contain valid JSON`, { cause });
19
+ }
20
+ if (!isObject(value))
21
+ throw new Error(`${source} must contain a JSON object`);
22
+ return value;
23
+ }
24
+ async function readStdin() {
25
+ const chunks = [];
26
+ for await (const chunk of process.stdin)
27
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
28
+ return Buffer.concat(chunks).toString("utf8");
29
+ }
30
+ export async function readJsonObject(source) {
31
+ const text = source === "-" ? await readStdin() : await readFile(source, "utf8");
32
+ return parseJsonObject(text, source === "-" ? "stdin" : source);
33
+ }
34
+ function parseNumber(value, name, options = {}) {
35
+ if (!value.trim())
36
+ throw new Error(`${name} must be a finite number`);
37
+ const parsed = Number(value);
38
+ if (!Number.isFinite(parsed))
39
+ throw new Error(`${name} must be a finite number`);
40
+ if (options.integer && !Number.isSafeInteger(parsed))
41
+ throw new Error(`${name} must be an integer`);
42
+ if (options.min !== undefined && parsed < options.min)
43
+ throw new Error(`${name} must be at least ${options.min}`);
44
+ if (options.max !== undefined && parsed > options.max)
45
+ throw new Error(`${name} must be at most ${options.max}`);
46
+ return parsed;
47
+ }
48
+ export function parseInspectSections(value) {
49
+ if (!value)
50
+ return undefined;
51
+ const sections = value.split(",").map((item) => item.trim()).filter(Boolean);
52
+ const unknown = sections.filter((section) => !INSPECT_SECTIONS.includes(section));
53
+ if (unknown.length > 0)
54
+ throw new Error(`Unknown Board section: ${unknown.join(", ")}`);
55
+ return [...new Set(sections)];
56
+ }
57
+ export function parseViewport(value) {
58
+ if (!value)
59
+ return undefined;
60
+ const parts = value.split(",").map((part) => part.trim());
61
+ if (parts.length !== 4)
62
+ throw new Error("viewport must be x,y,width,height");
63
+ const [xValue, yValue, widthValue, heightValue] = parts;
64
+ if (xValue === undefined || yValue === undefined || widthValue === undefined || heightValue === undefined) {
65
+ throw new Error("viewport must be x,y,width,height");
66
+ }
67
+ const x = parseNumber(xValue, "viewport x");
68
+ const y = parseNumber(yValue, "viewport y");
69
+ const width = parseNumber(widthValue, "viewport width");
70
+ const height = parseNumber(heightValue, "viewport height");
71
+ if (width <= 0 || height <= 0)
72
+ throw new Error("viewport width and height must be greater than zero");
73
+ return { x, y, width, height };
74
+ }
75
+ export function createTransactionInput(input, options) {
76
+ if ("boardId" in input)
77
+ throw new Error("transaction input must not contain boardId");
78
+ if (!Array.isArray(input.operations))
79
+ throw new Error("transaction input must contain an operations array");
80
+ const rawBaseVersion = options.baseVersion ?? input.baseVersion;
81
+ if (rawBaseVersion === undefined)
82
+ throw new Error("baseVersion is required in input or --base-version");
83
+ const baseVersion = parseNumber(String(rawBaseVersion), "baseVersion", { min: 0, integer: true });
84
+ const rawTxId = options.txId ?? input.txId;
85
+ if (rawTxId !== undefined && (typeof rawTxId !== "string" || !rawTxId.trim())) {
86
+ throw new Error("txId must be a non-empty string");
87
+ }
88
+ return {
89
+ ...input,
90
+ txId: typeof rawTxId === "string" ? rawTxId : randomUUID(),
91
+ baseVersion,
92
+ operations: input.operations,
93
+ };
94
+ }
95
+ function showBoard(result) {
96
+ table([
97
+ {
98
+ id: result.board.id,
99
+ title: result.board.title,
100
+ version: result.board.version,
101
+ nodes: result.nodes.length,
102
+ effects: result.effects.length,
103
+ sequences: result.sequences.length,
104
+ clips: result.clips.length,
105
+ },
106
+ ], [
107
+ { key: "id", label: "ID" },
108
+ { key: "title", label: "Title" },
109
+ { key: "version", label: "Version" },
110
+ { key: "nodes", label: "Nodes" },
111
+ { key: "effects", label: "Effects" },
112
+ { key: "sequences", label: "Sequences" },
113
+ { key: "clips", label: "Clips" },
114
+ ]);
115
+ }
116
+ function showValidation(result) {
117
+ table([{ valid: result.valid, diagnostics: result.diagnostics.length }], [
118
+ { key: "valid", label: "Valid" },
119
+ { key: "diagnostics", label: "Diagnostics" },
120
+ ]);
121
+ if (result.diagnostics.length > 0) {
122
+ console.log();
123
+ table(result.diagnostics, [
124
+ { key: "severity", label: "Severity" },
125
+ { key: "code", label: "Code" },
126
+ { key: "path", label: "Path" },
127
+ { key: "message", label: "Message" },
128
+ ]);
129
+ }
130
+ console.log();
131
+ table([result.peakCost], Object.keys(result.peakCost).map((key) => ({ key, label: key })));
132
+ }
133
+ function showPlayback(result) {
134
+ table([result], [
135
+ { key: "playbackId", label: "Playback ID" },
136
+ { key: "sequenceId", label: "Sequence" },
137
+ { key: "status", label: "Status" },
138
+ { key: "position", label: "Position" },
139
+ { key: "timeScale", label: "Time Scale" },
140
+ ]);
141
+ }
142
+ function withJson(command) {
143
+ return command.option("--json", "Output as JSON");
144
+ }
145
+ function registerTransactionCommand(boards, name) {
146
+ withJson(boards.command(`${name} <board-id>`)
147
+ .description(name === "validate" ? "Validate a transaction" : "Apply a transaction")
148
+ .requiredOption("-i, --input <file>", "Transaction JSON file; use - for stdin")
149
+ .option("--tx-id <id>", "Override txId; generated when omitted")
150
+ .option("--base-version <version>", "Override baseVersion"))
151
+ .action(async (boardId, options) => {
152
+ try {
153
+ const transaction = createTransactionInput(await readJsonObject(options.input), options);
154
+ const board = createClient().space(resolveSpace(boards)).board(boardId);
155
+ const result = await board[name](transaction);
156
+ if (jsonRequested(options))
157
+ return outJson(result);
158
+ if (name === "validate")
159
+ showValidation(result);
160
+ else {
161
+ ok(`Board updated to version ${result.board.version}`);
162
+ showBoard(result);
163
+ }
164
+ }
165
+ catch (cause) {
166
+ handleHttp(cause);
167
+ }
168
+ });
169
+ }
170
+ function commandId(options) {
171
+ return options.commandId?.trim() || randomUUID();
172
+ }
173
+ export function registerBoards(program) {
174
+ const boards = program
175
+ .command("boards")
176
+ .description("Inspect and update Boards")
177
+ .hook("preAction", () => { resolveSpace(boards); });
178
+ withJson(boards.command("create <path>")
179
+ .description("Create a Board")
180
+ .option("--title <title>", "Board title")
181
+ .option("-i, --input <file>", "Board content JSON file; use - for stdin"))
182
+ .action(async (path, options) => {
183
+ try {
184
+ const content = options.input ? await readJsonObject(options.input) : {};
185
+ if ("path" in content || "title" in content) {
186
+ throw new Error("create input must not contain path or title; use the command argument and --title");
187
+ }
188
+ const input = { ...content, path, ...(options.title ? { title: options.title } : {}) };
189
+ const result = await createClient().space(resolveSpace(boards)).boards.create(input);
190
+ if (jsonRequested(options))
191
+ return outJson(result);
192
+ ok(`Board created: ${result.board.id}`);
193
+ showBoard(result);
194
+ }
195
+ catch (cause) {
196
+ handleHttp(cause);
197
+ }
198
+ });
199
+ withJson(boards.command("inspect <board-id>")
200
+ .alias("get")
201
+ .description("Inspect a Board")
202
+ .option("--include <sections>", "Comma-separated nodes,effects,sequences,clips,playback")
203
+ .option("--viewport <rect>", "Viewport as x,y,width,height"))
204
+ .action(async (boardId, options) => {
205
+ try {
206
+ const result = await createClient().space(resolveSpace(boards)).board(boardId).inspect({
207
+ include: parseInspectSections(options.include),
208
+ viewport: parseViewport(options.viewport),
209
+ });
210
+ if (jsonRequested(options))
211
+ return outJson(result);
212
+ showBoard(result);
213
+ }
214
+ catch (cause) {
215
+ handleHttp(cause);
216
+ }
217
+ });
218
+ withJson(boards.command("capabilities <board-id>")
219
+ .description("Show supported capabilities"))
220
+ .action(async (boardId, options) => {
221
+ try {
222
+ const result = await createClient().space(resolveSpace(boards)).board(boardId).capabilities();
223
+ if (jsonRequested(options))
224
+ return outJson(result);
225
+ table(result.capabilities.map((capability) => ({
226
+ ...capability,
227
+ renderers: capability.renderers?.join(", ") ?? "",
228
+ })), [
229
+ { key: "kind", label: "Kind" },
230
+ { key: "id", label: "ID" },
231
+ { key: "version", label: "Version" },
232
+ { key: "renderers", label: "Renderers" },
233
+ { key: "digest", label: "Digest" },
234
+ ]);
235
+ }
236
+ catch (cause) {
237
+ handleHttp(cause);
238
+ }
239
+ });
240
+ registerTransactionCommand(boards, "validate");
241
+ registerTransactionCommand(boards, "apply");
242
+ withJson(boards.command("play <board-id> <sequence-id>")
243
+ .description("Start shared playback")
244
+ .option("--position <time>", "Initial position in milliseconds")
245
+ .option("--time-scale <scale>", "Playback speed from 0 to 4")
246
+ .option("--seed <seed>", "Deterministic playback seed")
247
+ .option("--command-id <id>", "Idempotency command ID"))
248
+ .action(async (boardId, sequenceId, options) => {
249
+ try {
250
+ const result = await createClient().space(resolveSpace(boards)).board(boardId).play({
251
+ commandId: commandId(options),
252
+ type: "play",
253
+ sequenceId,
254
+ shared: true,
255
+ ...(options.position === undefined ? {} : { position: parseNumber(options.position, "position", { min: 0 }) }),
256
+ ...(options.timeScale === undefined ? {} : { timeScale: parseNumber(options.timeScale, "timeScale", { min: Number.EPSILON, max: 4 }) }),
257
+ ...(options.seed ? { seed: options.seed } : {}),
258
+ });
259
+ if (jsonRequested(options))
260
+ return outJson(result);
261
+ showPlayback(result);
262
+ }
263
+ catch (cause) {
264
+ handleHttp(cause);
265
+ }
266
+ });
267
+ const playbackAction = (type) => async (boardId, playbackId, options) => {
268
+ try {
269
+ const board = createClient().space(resolveSpace(boards)).board(boardId);
270
+ const id = commandId(options);
271
+ const result = type === "pause"
272
+ ? await board.pause({ commandId: id, type: "pause", playbackId })
273
+ : await board.stop({ commandId: id, type: "stop", playbackId });
274
+ if (jsonRequested(options))
275
+ return outJson(result);
276
+ showPlayback(result);
277
+ }
278
+ catch (cause) {
279
+ handleHttp(cause);
280
+ }
281
+ };
282
+ withJson(boards.command("pause <board-id> <playback-id>")
283
+ .description("Pause playback")
284
+ .option("--command-id <id>", "Idempotency command ID"))
285
+ .action(playbackAction("pause"));
286
+ withJson(boards.command("seek <board-id> <playback-id> <position>")
287
+ .description("Seek playback")
288
+ .option("--command-id <id>", "Idempotency command ID"))
289
+ .action(async (boardId, playbackId, position, options) => {
290
+ try {
291
+ const result = await createClient().space(resolveSpace(boards)).board(boardId).seek({
292
+ commandId: commandId(options),
293
+ type: "seek",
294
+ playbackId,
295
+ position: parseNumber(position, "position", { min: 0 }),
296
+ });
297
+ if (jsonRequested(options))
298
+ return outJson(result);
299
+ showPlayback(result);
300
+ }
301
+ catch (cause) {
302
+ handleHttp(cause);
303
+ }
304
+ });
305
+ withJson(boards.command("stop <board-id> <playback-id>")
306
+ .description("Stop playback")
307
+ .option("--command-id <id>", "Idempotency command ID"))
308
+ .action(playbackAction("stop"));
309
+ withJson(boards.command("watch <board-id>")
310
+ .description("Stream Board events"))
311
+ .action((boardId, options) => {
312
+ try {
313
+ const board = createRealtimeClient().space(resolveSpace(boards)).board(boardId);
314
+ if (!jsonRequested(options))
315
+ process.stderr.write(`Listening for Board ${boardId} events...\n`);
316
+ board.subscribe({
317
+ event(event) {
318
+ if (jsonRequested(options)) {
319
+ process.stdout.write(`${JSON.stringify(event)}\n`);
320
+ return;
321
+ }
322
+ if (event.type === "board.transaction.applied") {
323
+ process.stdout.write(`version ${event.payload.version} transaction ${event.payload.txId} operations ${event.payload.operations.length}\n`);
324
+ }
325
+ else {
326
+ process.stdout.write(`${event.payload.status} sequence ${event.payload.sequenceId} position ${event.payload.position}\n`);
327
+ }
328
+ },
329
+ });
330
+ }
331
+ catch (cause) {
332
+ handleHttp(cause);
333
+ }
334
+ });
335
+ return boards;
336
+ }
@@ -381,16 +381,30 @@ export function registerSpaces(program) {
381
381
  .command("ls")
382
382
  .alias("list")
383
383
  .description("List all spaces")
384
+ .option("--mine", "Only spaces you own")
385
+ .option("--pinned", "Only pinned spaces")
384
386
  .option("--json", "Output as JSON")
385
387
  .action(async (opts) => {
386
388
  const client = createClient();
387
389
  try {
388
- const items = await client.spaces.list();
390
+ const [items, me] = await Promise.all([
391
+ client.spaces.list(),
392
+ opts.mine ? client.user.getMe() : Promise.resolve(null),
393
+ ]);
394
+ const myUuid = me?.uuid ?? null;
395
+ const filtered = items.filter((item) => {
396
+ if (opts.mine && myUuid && item.userUuid !== myUuid)
397
+ return false;
398
+ if (opts.pinned && !item.isPinned)
399
+ return false;
400
+ return true;
401
+ });
389
402
  if (jsonRequested(opts))
390
- return outJson(items);
391
- table(items, [
403
+ return outJson(filtered);
404
+ table(filtered, [
392
405
  { key: "id", label: "ID" },
393
406
  { key: "name", label: "Name" },
407
+ { key: "isPinned", label: "Pinned" },
394
408
  { key: "createdAt", label: "Created" },
395
409
  ]);
396
410
  }
@@ -605,6 +619,33 @@ export function registerSpaces(program) {
605
619
  registerMods(spacesCmd);
606
620
  // ── spaces labels ──
607
621
  registerLabels(spacesCmd);
622
+ // ── spaces pin / unpin (user-scope label convenience) ──
623
+ spacesCmd
624
+ .command("pin <id>")
625
+ .description("Pin a space (add the Pinned user label)")
626
+ .action(async (id) => {
627
+ const client = createClient();
628
+ try {
629
+ await client.user.labels.patchResourceLabels("space", id.trim(), { addLabelRefs: ["Pinned"] });
630
+ ok("Space pinned");
631
+ }
632
+ catch (e) {
633
+ handleHttp(e);
634
+ }
635
+ });
636
+ spacesCmd
637
+ .command("unpin <id>")
638
+ .description("Unpin a space (remove the Pinned user label)")
639
+ .action(async (id) => {
640
+ const client = createClient();
641
+ try {
642
+ await client.user.labels.patchResourceLabels("space", id.trim(), { removeLabelRefs: ["Pinned"] });
643
+ ok("Space unpinned");
644
+ }
645
+ catch (e) {
646
+ handleHttp(e);
647
+ }
648
+ });
608
649
  // ── spaces commerce ──
609
650
  registerSpaceCommerce(spacesCmd);
610
651
  // ── spaces usage ──
package/dist/index.js CHANGED
@@ -2,6 +2,7 @@
2
2
  import { Command } from "commander";
3
3
  import { readFileSync } from "node:fs";
4
4
  import { registerAuth } from "./commands/auth.js";
5
+ import { registerBoards } from "./commands/boards.js";
5
6
  import { registerChannels } from "./commands/channels.js";
6
7
  import { registerCronJobs } from "./commands/cron-jobs.js";
7
8
  import { registerGenerations } from "./commands/generations.js";
@@ -48,6 +49,7 @@ Common commands:
48
49
  cohub -s <space-id> run -- git status
49
50
  cohub sandbox up ./my-project
50
51
  cohub search "release notes"
52
+ cohub -s <space-id> boards inspect <board-id>
51
53
  cohub -s <space-id> spaces sessions turns ls <session-id>
52
54
  cohub -s <space-id> spaces files ls
53
55
  cohub -s <space-id> works publish demo --file dist/index.html
@@ -61,6 +63,7 @@ Environment:
61
63
  ENV=dev Use the development Cohub environment
62
64
  `);
63
65
  registerAuth(program);
66
+ registerBoards(program);
64
67
  registerProfile(program);
65
68
  registerMe(program);
66
69
  registerPrompt(program);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@neta-art/cohub-cli",
3
- "version": "2.7.0",
3
+ "version": "3.0.0",
4
4
  "description": "CLI for Cohub — spaces, sessions, and agent collaboration.",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -18,7 +18,7 @@
18
18
  "@neta-art/generation": "^0.1.16",
19
19
  "commander": "^15.0.0",
20
20
  "sharp": "^0.35.3",
21
- "@neta-art/cohub": "2.15.0"
21
+ "@neta-art/cohub": "3.0.0"
22
22
  },
23
23
  "publishConfig": {
24
24
  "access": "public"