@neta-art/cohub-cli 4.0.0 → 5.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
@@ -157,72 +157,44 @@ cohub -s <spaceId> boards capabilities <boardId>
157
157
  cohub -s <spaceId> boards watch <boardId> --json
158
158
  ```
159
159
 
160
- Pass nodes, effects, and sequences as JSON when creating a Board. The path and
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
- }
160
+ Pass semantic items, effects, and compositions as JSON when creating a Board. The path and title stay explicit in the command. Generate editable templates instead of guessing fields:
161
+
162
+ ```bash
163
+ cohub boards examples create > board-content.json
164
+ cohub boards examples item geo > item.json
165
+ cohub boards examples effect pulse > effect.json
166
+ cohub boards examples composition fade > intro.json
191
167
  ```
192
168
 
169
+ Inspect `boards capabilities --json` for supported Item types, animation channels, effect kinds, and coordinate spaces.
170
+
193
171
  ```bash
194
172
  cohub -s <spaceId> boards create boards/plan.board \
195
173
  --title "Plan" \
196
174
  --input board-content.json
197
175
  ```
198
176
 
199
- Transactions are JSON objects without `boardId`; the bound Board supplies it.
200
- `txId` is generated when omitted, while `baseVersion` must be provided in the
201
- input or with `--base-version`:
202
-
203
- ```json
204
- {
205
- "baseVersion": 3,
206
- "operations": [
207
- {
208
- "type": "board.patch",
209
- "payload": { "patch": { "title": "Updated plan" } }
210
- }
211
- ]
212
- }
177
+ Generate editable semantic JSON templates instead of authoring storage transactions:
178
+
179
+ ```bash
180
+ cohub boards examples item text > item.json
181
+ cohub boards items create <boardId> --input item.json
182
+
183
+ cohub boards examples composition fade > intro.json
184
+ cohub boards compositions apply <boardId> --input intro.json
185
+
186
+ cohub boards examples effect pulse > effect.json
187
+ cohub boards effects apply <boardId> --input effect.json
213
188
  ```
214
189
 
190
+ Use `--mutation-id` or `--command-id` when a script needs a stable idempotency key across retries.
191
+
215
192
  ```bash
216
- cohub -s <spaceId> boards validate <boardId> --input transaction.json
217
- cat transaction.json | cohub -s <spaceId> boards apply <boardId> --input - --json
218
- cohub -s <spaceId> boards play <boardId> <sequenceId>
193
+ cohub -s <spaceId> boards play <boardId> <compositionId>
219
194
  cohub -s <spaceId> boards seek <boardId> <playbackId> 400
220
195
  cohub -s <spaceId> boards stop <boardId> <playbackId>
221
196
  ```
222
197
 
223
- Pass `--tx-id` or `--command-id` when a script needs a stable idempotency key
224
- across retries.
225
-
226
198
  ## Search
227
199
 
228
200
  Search Spaces, Chats, and prior turns:
@@ -9,7 +9,7 @@
9
9
  import { existsSync } from "node:fs";
10
10
  import { createRequire } from "node:module";
11
11
  import { dirname, join } from "node:path";
12
- import { boardBootstrapToDocument, boardImageKeySource, imageAssetKey, planBoardExport, selectBoardExportAssets, } from "@neta-art/cohub/board";
12
+ import { boardAuthoringSnapshotToDocument, boardImageKeySource, imageAssetKey, planBoardExport, selectBoardExportAssets, } from "@neta-art/cohub/board";
13
13
  import { createBoardHeadlessRenderer, exportBoardImageBytes, } from "@neta-art/cohub/board/headless";
14
14
  import { resolveBoardId } from "./board-command-support.js";
15
15
  import { createClient } from "./client.js";
@@ -64,11 +64,11 @@ export function resolveBundledFonts() {
64
64
  export async function loadBoardDocument(spaceId, target) {
65
65
  const client = createClient();
66
66
  const boardId = await resolveBoardId(spaceId, target);
67
- const bootstrap = await client.space(spaceId).board(boardId).inspect({ include: ["nodes"] });
67
+ const snapshot = await client.space(spaceId).board(boardId).authoring({ include: ["items", "connections"] });
68
68
  return {
69
- document: boardBootstrapToDocument(bootstrap),
70
- boardId: bootstrap.board.id,
71
- title: bootstrap.board.title ?? null,
69
+ document: boardAuthoringSnapshotToDocument(snapshot),
70
+ boardId: snapshot.board.id,
71
+ title: snapshot.board.title ?? null,
72
72
  };
73
73
  }
74
74
  /**
@@ -1,8 +1,10 @@
1
1
  import { registerBoardAnimationCommands } from "./boards/animation.js";
2
2
  import { registerBoardAppearanceCommands } from "./boards/appearance.js";
3
+ import { registerBoardExampleCommands } from "./boards/examples.js";
3
4
  import { registerBoardItemCommands } from "./boards/items.js";
4
5
  import { registerBoardNodeCommands } from "./boards/nodes.js";
5
6
  export function registerBoardDomainCommands(boards) {
7
+ registerBoardExampleCommands(boards);
6
8
  registerBoardAppearanceCommands(boards);
7
9
  registerBoardNodeCommands(boards);
8
10
  registerBoardItemCommands(boards);
@@ -1,18 +1,18 @@
1
- import { boardCompositionApplyOperation, boardCompositionDeleteOperation, boardEffectDeleteOperation, boardEffectUpsertOperation, } from "@neta-art/cohub/board";
2
- import { BoardEffectSchema, parseBoardCompositionInput, } from "@neta-art/cohub";
1
+ import { parseBoardEffectInput, parseBoardCompositionInput, } from "@neta-art/cohub";
3
2
  import { BOARD_DOMAIN_INPUT_MAX_BYTES, readBoardJsonObject, } from "../../board-command-support.js";
4
3
  import { handleHttp, json, jsonRequested, table } from "../../output.js";
5
- import { resolvedBoard, showUpdated, withJson, } from "./context.js";
4
+ import { mutateSemantic, resolvedBoard, showUpdated, withJson, } from "./context.js";
6
5
  export function registerBoardAnimationCommands(boards) {
7
6
  const effects = boards.command("effects").description("Manage Board effects");
8
7
  withJson(effects.command("list <board>").alias("ls").description("List effects"))
9
8
  .action(async (target, options) => {
10
9
  try {
11
10
  const board = await resolvedBoard(boards, target);
12
- const result = await board.inspect({ include: ["effects"] });
11
+ const result = await board.authoring({ include: ["effects"] });
12
+ const effects = result.effects ?? [];
13
13
  if (jsonRequested(options))
14
- return json(result.effects);
15
- table(result.effects, [
14
+ return json(effects);
15
+ table(effects, [
16
16
  { key: "id", label: "ID" },
17
17
  { key: "kind", label: "KIND" },
18
18
  { key: "enabled", label: "ENABLED" },
@@ -25,13 +25,19 @@ export function registerBoardAnimationCommands(boards) {
25
25
  });
26
26
  withJson(effects.command("apply <board>")
27
27
  .description("Atomically create or replace an effect")
28
- .requiredOption("-i, --input <file>", "Board effect JSON; use - for stdin"))
28
+ .requiredOption("-i, --input <file>", "Board effect JSON; use - for stdin")
29
+ .addHelpText("after", `
30
+ Create a template:
31
+ cohub boards examples effect pulse > effect.json
32
+
33
+ Discover supported effect kinds:
34
+ cohub boards capabilities <board> --json`))
29
35
  .action(async (target, options) => {
30
36
  try {
31
37
  const input = await readBoardJsonObject(options.input, BOARD_DOMAIN_INPUT_MAX_BYTES);
32
- const effect = BoardEffectSchema.omit({ boardId: true, revision: true }).parse(input);
38
+ const effect = parseBoardEffectInput(input);
33
39
  const board = await resolvedBoard(boards, target);
34
- showUpdated(await board.mutate({ build: () => [boardEffectUpsertOperation(effect)] }), options);
40
+ showUpdated(await mutateSemantic(board, [{ type: "effect.apply", effect }]), options);
35
41
  }
36
42
  catch (cause) {
37
43
  handleHttp(cause);
@@ -41,7 +47,7 @@ export function registerBoardAnimationCommands(boards) {
41
47
  .action(async (target, effectId, options) => {
42
48
  try {
43
49
  const board = await resolvedBoard(boards, target);
44
- showUpdated(await board.mutate({ build: () => [boardEffectDeleteOperation(effectId)] }), options);
50
+ showUpdated(await mutateSemantic(board, [{ type: "effect.delete", effectId }]), options);
45
51
  }
46
52
  catch (cause) {
47
53
  handleHttp(cause);
@@ -54,10 +60,11 @@ export function registerBoardAnimationCommands(boards) {
54
60
  .action(async (target, options) => {
55
61
  try {
56
62
  const board = await resolvedBoard(boards, target);
57
- const result = await board.inspect({ include: ["compositions"] });
63
+ const result = await board.authoring({ include: ["compositions"] });
64
+ const compositions = result.compositions ?? [];
58
65
  if (jsonRequested(options))
59
- return json(result.compositions);
60
- table(result.compositions.map((composition) => ({
66
+ return json(compositions);
67
+ table(compositions.map((composition) => ({
61
68
  id: composition.id,
62
69
  name: composition.name,
63
70
  duration: composition.timeline.duration,
@@ -81,8 +88,8 @@ export function registerBoardAnimationCommands(boards) {
81
88
  .action(async (target, compositionId, options) => {
82
89
  try {
83
90
  const board = await resolvedBoard(boards, target);
84
- const result = await board.inspect({ include: ["compositions"] });
85
- const composition = result.compositions.find((item) => item.id === compositionId);
91
+ const result = await board.authoring({ include: ["compositions"] });
92
+ const composition = (result.compositions ?? []).find((item) => item.id === compositionId);
86
93
  if (!composition)
87
94
  throw new Error(`Composition not found: ${compositionId}`);
88
95
  if (jsonRequested(options))
@@ -114,6 +121,8 @@ export function registerBoardAnimationCommands(boards) {
114
121
  Property changes use timeline.tracks with registered channels and keyframes.
115
122
  Procedural behavior such as text reveal, particles, and camera focus uses timeline.clips.
116
123
  Run boards capabilities to discover channels and clip schemas.
124
+ Create an editable template with:
125
+ cohub boards examples composition fade > intro.json
117
126
 
118
127
  Minimal fade composition:
119
128
  {"id":"intro","name":"Intro","timeline":{"duration":800,"tracks":[{"id":"title-opacity","target":{"type":"item","itemId":"title"},"channel":"style.opacity","fill":"both","keyframes":[{"time":0,"value":0},{"time":800,"value":1,"easing":"ease-out-cubic"}]}],"clips":[],"markers":[]},"playback":{"loop":false,"endBehavior":"hold","reducedMotion":{"mode":"base"}}}`))
@@ -122,9 +131,7 @@ Minimal fade composition:
122
131
  const input = await readBoardJsonObject(options.input, BOARD_DOMAIN_INPUT_MAX_BYTES);
123
132
  const composition = parseBoardCompositionInput(input);
124
133
  const board = await resolvedBoard(boards, target);
125
- showUpdated(await board.mutate({
126
- build: () => [boardCompositionApplyOperation(composition)],
127
- }), options);
134
+ showUpdated(await mutateSemantic(board, [{ type: "composition.apply", composition }]), options);
128
135
  }
129
136
  catch (cause) {
130
137
  handleHttp(cause);
@@ -134,9 +141,7 @@ Minimal fade composition:
134
141
  .action(async (target, compositionId, options) => {
135
142
  try {
136
143
  const board = await resolvedBoard(boards, target);
137
- showUpdated(await board.mutate({
138
- build: () => [boardCompositionDeleteOperation(compositionId)],
139
- }), options);
144
+ showUpdated(await mutateSemantic(board, [{ type: "composition.delete", compositionId }]), options);
140
145
  }
141
146
  catch (cause) {
142
147
  handleHttp(cause);
@@ -1,6 +1,6 @@
1
- import { BoardAppearanceSchema, DEFAULT_BOARD_APPEARANCE, boardAppearanceOperation, boardPlaybackPolicyOperation, boardTitleOperation, patchBoardAppearance, } from "@neta-art/cohub/board";
1
+ import { BoardAppearanceSchema, DEFAULT_BOARD_APPEARANCE, patchBoardAppearance, } from "@neta-art/cohub/board";
2
2
  import { handleHttp } from "../../output.js";
3
- import { finite, resolvedBoard, showUpdated, withJson, } from "./context.js";
3
+ import { finite, mutateSemantic, resolvedBoard, showUpdated, withJson, } from "./context.js";
4
4
  function appearanceFrom(metadata) {
5
5
  const parsed = BoardAppearanceSchema.safeParse(metadata.appearance);
6
6
  return parsed.success ? parsed.data : DEFAULT_BOARD_APPEARANCE;
@@ -10,7 +10,7 @@ export function registerBoardAppearanceCommands(boards) {
10
10
  .action(async (target, title, options) => {
11
11
  try {
12
12
  const board = await resolvedBoard(boards, target);
13
- showUpdated(await board.mutate({ build: () => [boardTitleOperation(title)] }), options);
13
+ showUpdated(await mutateSemantic(board, [{ type: "board.patch", patch: { title } }]), options);
14
14
  }
15
15
  catch (cause) {
16
16
  handleHttp(cause);
@@ -41,24 +41,24 @@ Examples:
41
41
  if (opacity < 0 || opacity > 1)
42
42
  throw new Error("opacity must be between 0 and 1");
43
43
  const board = await resolvedBoard(boards, target);
44
- const result = await board.mutate({
45
- build(current) {
46
- const appearance = appearanceFrom(current.board.metadata);
47
- const background = options.reset
48
- ? { kind: "solid" }
49
- : options.color
50
- ? { kind: "solid", color: options.color }
51
- : {
52
- kind: "image",
53
- imageUrl: options.image,
54
- fit: options.fit,
55
- position: options.position,
56
- opacity,
57
- color: appearance.background.color,
58
- };
59
- return [boardAppearanceOperation(patchBoardAppearance(appearance, { background }))];
60
- },
61
- });
44
+ const current = await board.summary();
45
+ const appearance = appearanceFrom(current.board.metadata);
46
+ const background = options.reset
47
+ ? { kind: "solid" }
48
+ : options.color
49
+ ? { kind: "solid", color: options.color }
50
+ : {
51
+ kind: "image",
52
+ imageUrl: options.image,
53
+ fit: options.fit,
54
+ position: options.position,
55
+ opacity,
56
+ color: appearance.background.color,
57
+ };
58
+ const result = await mutateSemantic(board, [{
59
+ type: "board.patch",
60
+ patch: { metadataPatch: { appearance: patchBoardAppearance(appearance, { background }) } },
61
+ }], { baseVersion: current.board.version });
62
62
  showUpdated(result, options);
63
63
  }
64
64
  catch (cause) {
@@ -81,9 +81,13 @@ Examples:
81
81
  ? null
82
82
  : { compositionId: options.composition, delayMs };
83
83
  const board = await resolvedBoard(boards, target);
84
- showUpdated(await board.mutate({
85
- build: (current) => [boardPlaybackPolicyOperation(current.board.metadata, policy)],
86
- }), options);
84
+ const current = await board.summary();
85
+ const patch = policy
86
+ ? { metadataPatch: { playback: policy } }
87
+ : { metadata: { ...current.board.metadata, playback: undefined } };
88
+ if (!policy)
89
+ delete patch.metadata.playback;
90
+ showUpdated(await mutateSemantic(board, [{ type: "board.patch", patch }], { baseVersion: current.board.version }), options);
87
91
  }
88
92
  catch (cause) {
89
93
  handleHttp(cause);
@@ -1,12 +1,28 @@
1
+ import type { BoardClient, BoardSemanticCommand } from "@neta-art/cohub";
1
2
  import type { Command } from "commander";
2
3
  export type JsonOptions = {
3
4
  json?: boolean;
4
5
  };
5
6
  export declare function withJson(command: Command): Command;
6
7
  export declare function finite(value: string | undefined, name: string, fallback?: number): number;
7
- export declare function resolvedBoard(boards: Command, target: string): Promise<import("@neta-art/cohub").BoardClient>;
8
+ export declare function resolvedBoard(boards: Command, target: string): Promise<BoardClient>;
9
+ /**
10
+ * Send semantic commands with one automatic version-conflict retry: a racing
11
+ * edit between our version read and the submit is transparently rebased (same
12
+ * commands, fresh baseVersion, same id — a retry that lands is idempotent).
13
+ * `options.baseVersion` lets an agent chain mutations without re-reading the
14
+ * board between writes.
15
+ */
16
+ export declare function mutateSemantic(board: BoardClient, commands: BoardSemanticCommand[], options?: {
17
+ baseVersion?: number;
18
+ mutationId?: string;
19
+ dryRun?: boolean;
20
+ }): Promise<import("@neta-art/cohub").BoardMutationReceipt>;
8
21
  export declare function showUpdated(result: {
9
22
  board: {
10
23
  version: number;
11
24
  };
25
+ status?: string;
26
+ outcome?: string;
27
+ replayed?: boolean;
12
28
  }, options: JsonOptions): void;
@@ -18,8 +18,45 @@ export async function resolvedBoard(boards, target) {
18
18
  const boardId = await resolveBoardId(spaceId, target);
19
19
  return createClient().space(spaceId).board(boardId);
20
20
  }
21
+ /**
22
+ * Send semantic commands with one automatic version-conflict retry: a racing
23
+ * edit between our version read and the submit is transparently rebased (same
24
+ * commands, fresh baseVersion, same id — a retry that lands is idempotent).
25
+ * `options.baseVersion` lets an agent chain mutations without re-reading the
26
+ * board between writes.
27
+ */
28
+ export async function mutateSemantic(board, commands, options = {}) {
29
+ const baseVersion = options.baseVersion ?? (await board.summary()).board.version;
30
+ const send = (version) => board.mutateSemantic({
31
+ mutationId: options.mutationId,
32
+ baseVersion: version,
33
+ dryRun: options.dryRun ?? false,
34
+ commands,
35
+ });
36
+ try {
37
+ return await send(baseVersion);
38
+ }
39
+ catch (cause) {
40
+ if (options.dryRun || !isVersionConflict(cause))
41
+ throw cause;
42
+ const retry = (await board.summary()).board.version;
43
+ return send(retry);
44
+ }
45
+ }
46
+ function isVersionConflict(error) {
47
+ return Boolean(error && typeof error === "object" &&
48
+ error.code === "VERSION_CONFLICT");
49
+ }
21
50
  export function showUpdated(result, options) {
22
51
  if (jsonRequested(options))
23
52
  return json(result);
24
- ok(`Board updated to version ${result.board.version}`);
53
+ if (result.outcome === "dry-run") {
54
+ ok(`Validated against Board version ${result.board.version}; no changes written`);
55
+ return;
56
+ }
57
+ if (result.outcome === "noop" || result.status === "validated") {
58
+ ok(`Board already matches the requested state at version ${result.board.version}`);
59
+ return;
60
+ }
61
+ ok(`Board updated to version ${result.board.version}${result.replayed ? " (replayed)" : ""}`);
25
62
  }
@@ -0,0 +1,4 @@
1
+ import type { Command } from "commander";
2
+ export declare const BOARD_EXAMPLE_KEYS: string[];
3
+ export declare function boardExample(kind: string, type?: string): unknown;
4
+ export declare function registerBoardExampleCommands(boards: Command): void;
@@ -0,0 +1,224 @@
1
+ import { json } from "../../output.js";
2
+ const frame = { x: 120, y: 80, width: 320, height: 48, rotation: 0 };
3
+ const templates = {
4
+ "create": {
5
+ items: [{
6
+ id: "title",
7
+ type: "text",
8
+ frame,
9
+ props: { text: "Launch plan", fontSize: 32 },
10
+ style: { color: "brand" },
11
+ }],
12
+ connections: [],
13
+ effects: [],
14
+ compositions: [],
15
+ },
16
+ "item:text": {
17
+ id: "title",
18
+ type: "text",
19
+ frame,
20
+ props: { text: "Launch plan", fontSize: 32 },
21
+ style: { color: "brand" },
22
+ },
23
+ "item:image": {
24
+ id: "hero",
25
+ type: "image",
26
+ frame: { x: 120, y: 160, width: 640, height: 360, rotation: 0 },
27
+ source: { kind: "space-file", path: "assets/hero.webp" },
28
+ props: {},
29
+ },
30
+ "item:geo": {
31
+ id: "goal",
32
+ type: "geo",
33
+ frame: { x: 120, y: 160, width: 280, height: 140, rotation: 0 },
34
+ props: { shape: "rounded", text: "Ship" },
35
+ style: { color: "green", fillOpacity: 0.12 },
36
+ },
37
+ "item:frame": {
38
+ id: "scene",
39
+ type: "frame",
40
+ frame: { x: 80, y: 60, width: 960, height: 540, rotation: 0 },
41
+ props: { label: "Scene 1" },
42
+ style: { color: "neutral" },
43
+ },
44
+ "item:draw": {
45
+ id: "stroke",
46
+ type: "draw",
47
+ frame: { x: 100, y: 100, width: 180, height: 100, rotation: 0 },
48
+ props: { points: [{ x: 0, y: 80, p: 0.5 }, { x: 90, y: 10, p: 0.8 }, { x: 180, y: 70, p: 0.5 }] },
49
+ style: { color: "violet", strokeWidth: 4 },
50
+ },
51
+ "item:arrow": {
52
+ id: "arrow",
53
+ type: "arrow",
54
+ frame: { x: 100, y: 100, width: 260, height: 100, rotation: 0 },
55
+ props: { start: { x: 116, y: 150 }, end: { x: 344, y: 150 }, bend: 0, arrowStart: false, arrowEnd: true, label: "next" },
56
+ style: { color: "brand", strokeWidth: 2.5 },
57
+ },
58
+ "item:video": {
59
+ id: "demo-video",
60
+ type: "video",
61
+ frame: { x: 120, y: 160, width: 640, height: 360, rotation: 0 },
62
+ source: { kind: "space-file", path: "assets/demo.mp4" },
63
+ props: {},
64
+ },
65
+ "item:audio": {
66
+ id: "soundtrack",
67
+ type: "audio",
68
+ frame: { x: 120, y: 540, width: 480, height: 96, rotation: 0 },
69
+ source: { kind: "space-file", path: "assets/soundtrack.mp3" },
70
+ props: {},
71
+ },
72
+ "item:file": {
73
+ id: "brief",
74
+ type: "file",
75
+ frame: { x: 120, y: 160, width: 360, height: 220, rotation: 0 },
76
+ source: { kind: "space-file", path: "docs/brief.md" },
77
+ props: {},
78
+ },
79
+ "item:task": {
80
+ id: "task",
81
+ type: "task",
82
+ frame: { x: 120, y: 160, width: 420, height: 240, rotation: 0 },
83
+ props: { taskRunId: "task-run-id", snapshot: { taskType: "generation", status: "running", title: "Generate concept", artifactCount: 0, artifacts: [] } },
84
+ },
85
+ "effect:pulse": {
86
+ id: "pulse-title",
87
+ target: { type: "item", itemId: "title" },
88
+ kind: "effects.pulse",
89
+ kindVersion: 1,
90
+ enabled: true,
91
+ lifecycle: "when-visible",
92
+ timeOrigin: "visible",
93
+ layer: "front",
94
+ seed: "pulse-title",
95
+ params: { amount: 0.04, period: 1600 },
96
+ assetRefs: [],
97
+ metadata: {},
98
+ },
99
+ "effect:float": {
100
+ id: "float-hero",
101
+ target: { type: "item", itemId: "hero" },
102
+ kind: "effects.float",
103
+ kindVersion: 1,
104
+ enabled: true,
105
+ lifecycle: "when-visible",
106
+ timeOrigin: "visible",
107
+ layer: "front",
108
+ seed: "float-hero",
109
+ params: { distance: 8, period: 2200 },
110
+ assetRefs: [],
111
+ metadata: {},
112
+ },
113
+ "composition:fade": {
114
+ id: "intro",
115
+ name: "Intro",
116
+ timeline: {
117
+ duration: 800,
118
+ tracks: [{
119
+ id: "title-opacity",
120
+ target: { type: "item", itemId: "title" },
121
+ channel: "style.opacity",
122
+ channelVersion: 1,
123
+ interpolation: "linear",
124
+ fill: "both",
125
+ keyframes: [
126
+ { time: 0, value: 0 },
127
+ { time: 800, value: 1, easing: "ease-out-cubic" },
128
+ ],
129
+ metadata: {},
130
+ }],
131
+ clips: [],
132
+ markers: [],
133
+ },
134
+ playback: { loop: false, endBehavior: "hold", reducedMotion: { mode: "base" } },
135
+ metadata: {},
136
+ },
137
+ "composition:reveal": {
138
+ id: "reveal",
139
+ name: "Reveal",
140
+ timeline: {
141
+ duration: 900,
142
+ tracks: [{ id: "title-opacity", target: { type: "item", itemId: "title" }, channel: "style.opacity", channelVersion: 1, interpolation: "linear", fill: "both", keyframes: [{ time: 0, value: 0 }, { time: 600, value: 1 }], metadata: {} }],
143
+ clips: [{ id: "reveal-title", kind: "text.reveal", kindVersion: 1, target: { type: "item", itemId: "title" }, start: 0, duration: 600, layer: "content", fill: "both", easing: "ease-out-cubic", params: { mode: "words" }, assetRefs: [], seed: "reveal-title", metadata: {} }],
144
+ markers: [],
145
+ },
146
+ playback: { loop: false, endBehavior: "hold", reducedMotion: { mode: "base" } },
147
+ metadata: {},
148
+ },
149
+ "composition:motion-path": {
150
+ id: "motion",
151
+ name: "Motion path",
152
+ timeline: {
153
+ duration: 1200,
154
+ tracks: [],
155
+ clips: [{ id: "move-hero", kind: "motion.path", kindVersion: 1, target: { type: "item", itemId: "hero" }, start: 0, duration: 1200, layer: "content", fill: "both", easing: "ease-in-out-cubic", params: { points: [{ x: 0, y: 0 }, { x: 160, y: -40 }, { x: 320, y: 0 }], orient: true }, assetRefs: [], seed: "move-hero", metadata: {} }],
156
+ markers: [],
157
+ },
158
+ playback: { loop: false, endBehavior: "hold", reducedMotion: { mode: "base" } },
159
+ metadata: {},
160
+ },
161
+ "composition:particles": {
162
+ id: "celebrate",
163
+ name: "Celebrate",
164
+ timeline: {
165
+ duration: 1000,
166
+ tracks: [],
167
+ clips: [{ id: "particles", kind: "effects.particles", kindVersion: 1, target: { type: "board" }, start: 0, duration: 1000, layer: "front", fill: "none", easing: "linear", params: { count: 420, bounds: { x: 0, y: 0, width: 800, height: 600 } }, assetRefs: [], seed: "particles", metadata: {} }],
168
+ markers: [],
169
+ },
170
+ playback: { loop: false, endBehavior: "reset", reducedMotion: { mode: "base" } },
171
+ metadata: {},
172
+ },
173
+ "composition:camera-focus": {
174
+ id: "tour",
175
+ name: "Tour",
176
+ timeline: {
177
+ duration: 1000,
178
+ tracks: [],
179
+ clips: [{
180
+ id: "focus-title",
181
+ kind: "camera.focus",
182
+ kindVersion: 1,
183
+ target: { type: "camera" },
184
+ start: 0,
185
+ duration: 700,
186
+ layer: "screen",
187
+ fill: "forwards",
188
+ easing: "ease-out-cubic",
189
+ params: { focus: { type: "item", itemId: "title" }, fit: "contain", padding: 32 },
190
+ assetRefs: [],
191
+ seed: "focus-title",
192
+ metadata: {},
193
+ }],
194
+ markers: [],
195
+ },
196
+ playback: { loop: false, endBehavior: "hold", reducedMotion: { mode: "base" } },
197
+ metadata: {},
198
+ },
199
+ };
200
+ export const BOARD_EXAMPLE_KEYS = Object.keys(templates);
201
+ export function boardExample(kind, type) {
202
+ const key = type ? `${kind}:${type}` : kind;
203
+ const template = templates[key];
204
+ if (!template)
205
+ throw new Error(`Unknown Board example: ${key}`);
206
+ return template;
207
+ }
208
+ export function registerBoardExampleCommands(boards) {
209
+ boards.command("examples <kind> [type]")
210
+ .description("Print editable semantic JSON templates")
211
+ .addHelpText("after", `
212
+ Kinds:
213
+ create
214
+ item text|image|video|audio|file|task|geo|frame|draw|arrow
215
+ effect pulse|float
216
+ composition fade|reveal|motion-path|particles|camera-focus
217
+
218
+ Examples:
219
+ cohub boards examples item text > item.json
220
+ cohub boards examples composition fade > intro.json`)
221
+ .action((kind, type) => {
222
+ json(boardExample(kind, type));
223
+ });
224
+ }
@@ -14,34 +14,29 @@ const baseVersion = (value, fallback) => {
14
14
  };
15
15
  async function execute(boards, target, command, options) {
16
16
  const board = await resolvedBoard(boards, target);
17
- const snapshot = await board.authoring();
17
+ const snapshot = await board.summary();
18
18
  const mutation = {
19
19
  mutationId: options.mutationId?.trim() || randomUUID(),
20
20
  baseVersion: baseVersion(options.baseVersion, snapshot.board.version),
21
+ dryRun: Boolean(options.dryRun),
21
22
  commands: [command],
22
23
  };
23
- if (options.dryRun) {
24
- const result = {
25
- mutationId: mutation.mutationId,
26
- status: "prepared",
27
- board: { id: snapshot.board.id, version: snapshot.board.version },
28
- commands: mutation.commands,
29
- };
30
- if (jsonRequested(options))
31
- return json(result);
32
- ok(`Prepared against Board version ${snapshot.board.version}; no server validation or write performed`);
33
- return;
34
- }
24
+ // Server-side validation: schema, version, references, cascade rules — the
25
+ // same checks the real write would run, just without persisting anything.
35
26
  const receipt = await board.mutateSemantic(mutation);
36
27
  if (jsonRequested(options))
37
28
  return json(receipt);
29
+ if (options.dryRun) {
30
+ ok(`Validated against Board version ${receipt.board.version}; no changes written`);
31
+ return;
32
+ }
38
33
  ok(`${receipt.replayed ? "Replayed" : "Applied"} mutation at Board version ${receipt.board.version}`);
39
34
  }
40
35
  function mutationOptions(command) {
41
36
  return withJson(command)
42
37
  .option("--base-version <version>", "Expected Board version; defaults to latest")
43
38
  .option("--mutation-id <id>", "Stable id for safe retries")
44
- .option("--dry-run", "Prepare locally without server validation or writing");
39
+ .option("--dry-run", "Validate on the server (references, version, cascade) without writing");
45
40
  }
46
41
  export function registerBoardItemCommands(boards) {
47
42
  const items = boards.command("items").description("Author Board items with semantic JSON");
@@ -50,9 +45,10 @@ export function registerBoardItemCommands(boards) {
50
45
  try {
51
46
  const board = await resolvedBoard(boards, target);
52
47
  const snapshot = await board.authoring();
48
+ const items = snapshot.items ?? [];
53
49
  if (jsonRequested(options))
54
- return json(snapshot.items);
55
- table(snapshot.items.map((item) => ({
50
+ return json(items);
51
+ table(items.map((item) => ({
56
52
  id: item.id,
57
53
  type: item.type,
58
54
  x: item.frame.x,
@@ -1,5 +1,6 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { createBoardConnection } from "@neta-art/cohub/board";
3
+ import { mutateSemantic } from "./context.js";
3
4
  import { handleHttp } from "../../output.js";
4
5
  import { resolvedBoard, showUpdated, withJson, } from "./context.js";
5
6
  /** Connection shortcuts; Item authoring lives in ./items.ts. */
@@ -21,17 +22,15 @@ export function registerBoardNodeCommands(boards) {
21
22
  const board = await resolvedBoard(boards, target);
22
23
  const connection = createBoardConnection({
23
24
  id: options.id ?? randomUUID(),
24
- sourceNodeId: source,
25
- targetNodeId: destination,
25
+ sourceItemId: source,
26
+ targetItemId: destination,
26
27
  relation: options.relation,
27
28
  direction: direction,
28
29
  label: options.label,
29
30
  sourcePortId: options.sourcePort,
30
31
  targetPortId: options.targetPort,
31
32
  });
32
- showUpdated(await board.mutate({
33
- build: () => [{ type: "connection.create", payload: { connection } }],
34
- }), options);
33
+ showUpdated(await mutateSemantic(board, [{ type: "connection.create", connection }]), options);
35
34
  }
36
35
  catch (cause) {
37
36
  handleHttp(cause);
@@ -42,9 +41,7 @@ export function registerBoardNodeCommands(boards) {
42
41
  .action(async (target, connectionId, options) => {
43
42
  try {
44
43
  const board = await resolvedBoard(boards, target);
45
- showUpdated(await board.mutate({
46
- build: () => [{ type: "connection.delete", payload: { connectionId } }],
47
- }), options);
44
+ showUpdated(await mutateSemantic(board, [{ type: "connection.delete", connectionId }]), options);
48
45
  }
49
46
  catch (cause) {
50
47
  handleHttp(cause);
@@ -1,15 +1,10 @@
1
- import type { BoardInspectInput, BoardTransactionInput } from "@neta-art/cohub";
2
1
  import type { Command } from "commander";
3
2
  import { parseBoardJsonObject } from "../board-command-support.js";
4
- declare const INSPECT_SECTIONS: readonly ["nodes", "connections", "effects", "compositions", "playback"];
5
- type InspectSection = (typeof INSPECT_SECTIONS)[number];
6
3
  export declare const parseJsonObject: typeof parseBoardJsonObject;
7
- export declare function readJsonObject(source: string, maxBytes?: number): Promise<Record<string, unknown>>;
8
- export declare function parseInspectSections(value?: string): InspectSection[] | undefined;
9
- export declare function parseViewport(value?: string): BoardInspectInput["viewport"];
10
- export declare function createTransactionInput(input: Record<string, unknown>, options: {
11
- txId?: string;
12
- baseVersion?: string;
13
- }): BoardTransactionInput;
4
+ export declare function parseViewport(value?: string): {
5
+ x: number;
6
+ y: number;
7
+ width: number;
8
+ height: number;
9
+ } | undefined;
14
10
  export declare function registerBoards(program: Command): Command;
15
- export {};
@@ -1,15 +1,11 @@
1
1
  import { randomUUID } from "node:crypto";
2
- import { BOARD_CREATE_INPUT_MAX_BYTES, BOARD_TRANSACTION_INPUT_MAX_BYTES, parseBoardJsonObject, readBoardJsonObject, resolveBoardId, writeBoardOutput, } from "../board-command-support.js";
2
+ import { BOARD_CREATE_INPUT_MAX_BYTES, parseBoardJsonObject, readBoardJsonObject, resolveBoardId, writeBoardOutput, } from "../board-command-support.js";
3
3
  import { BOARD_EXPORT_FORMATS, formatFromPath, runBoardExport } from "../board-export.js";
4
4
  import { registerBoardDomainCommands } from "./board-domain.js";
5
5
  import { createClient, createRealtimeClient } from "../client.js";
6
6
  import { error, handleHttp, json as outJson, jsonRequested, ok, table } from "../output.js";
7
7
  import { resolveSpace } from "../space.js";
8
- const INSPECT_SECTIONS = ["nodes", "connections", "effects", "compositions", "playback"];
9
8
  export const parseJsonObject = parseBoardJsonObject;
10
- export async function readJsonObject(source, maxBytes = BOARD_TRANSACTION_INPUT_MAX_BYTES) {
11
- return readBoardJsonObject(source, maxBytes);
12
- }
13
9
  function parseNumber(value, name, options = {}) {
14
10
  if (!value.trim())
15
11
  throw new Error(`${name} must be a finite number`);
@@ -24,15 +20,6 @@ function parseNumber(value, name, options = {}) {
24
20
  throw new Error(`${name} must be at most ${options.max}`);
25
21
  return parsed;
26
22
  }
27
- export function parseInspectSections(value) {
28
- if (!value)
29
- return undefined;
30
- const sections = value.split(",").map((item) => item.trim()).filter(Boolean);
31
- const unknown = sections.filter((section) => !INSPECT_SECTIONS.includes(section));
32
- if (unknown.length > 0)
33
- throw new Error(`Unknown Board section: ${unknown.join(", ")}`);
34
- return [...new Set(sections)];
35
- }
36
23
  export function parseViewport(value) {
37
24
  if (!value)
38
25
  return undefined;
@@ -51,45 +38,11 @@ export function parseViewport(value) {
51
38
  throw new Error("viewport width and height must be greater than zero");
52
39
  return { x, y, width, height };
53
40
  }
54
- export function createTransactionInput(input, options) {
55
- if ("boardId" in input)
56
- throw new Error("transaction input must not contain boardId");
57
- if (!Array.isArray(input.operations))
58
- throw new Error("transaction input must contain an operations array");
59
- const rawBaseVersion = options.baseVersion ?? input.baseVersion;
60
- if (rawBaseVersion === undefined)
61
- throw new Error("baseVersion is required in input or --base-version");
62
- const baseVersion = parseNumber(String(rawBaseVersion), "baseVersion", { min: 0, integer: true });
63
- const rawTxId = options.txId ?? input.txId;
64
- if (rawTxId !== undefined && (typeof rawTxId !== "string" || !rawTxId.trim())) {
65
- throw new Error("txId must be a non-empty string");
66
- }
67
- return {
68
- ...input,
69
- txId: typeof rawTxId === "string" ? rawTxId : randomUUID(),
70
- baseVersion,
71
- operations: input.operations,
72
- };
73
- }
74
- function showBoard(result) {
75
- table([
76
- {
77
- id: result.board.id,
78
- title: result.board.title,
79
- version: result.board.version,
80
- nodes: result.nodes.length,
81
- connections: result.connections.length,
82
- effects: result.effects.length,
83
- compositions: result.compositions.length,
84
- },
85
- ], [
41
+ function showCreated(result) {
42
+ table([result.board], [
86
43
  { key: "id", label: "ID" },
87
44
  { key: "title", label: "Title" },
88
45
  { key: "version", label: "Version" },
89
- { key: "nodes", label: "Nodes" },
90
- { key: "connections", label: "Connections" },
91
- { key: "effects", label: "Effects" },
92
- { key: "compositions", label: "Compositions" },
93
46
  ]);
94
47
  }
95
48
  function showSummary(result) {
@@ -105,7 +58,7 @@ function showSummary(result) {
105
58
  { key: "id", label: "ID" },
106
59
  { key: "title", label: "TITLE" },
107
60
  { key: "version", label: "VERSION" },
108
- { key: "nodes", label: "NODES" },
61
+ { key: "items", label: "ITEMS" },
109
62
  { key: "connections", label: "CONNECTIONS" },
110
63
  { key: "effects", label: "EFFECTS" },
111
64
  { key: "compositions", label: "COMPOSITIONS" },
@@ -113,23 +66,6 @@ function showSummary(result) {
113
66
  { key: "updatedAt", label: "UPDATED" },
114
67
  ]);
115
68
  }
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
69
  function showPlayback(result) {
134
70
  table([result], [
135
71
  { key: "playbackId", label: "Playback ID" },
@@ -156,38 +92,6 @@ function capabilityUnits(schema) {
156
92
  return detail ? [`${field}:${detail}`] : [];
157
93
  }).join(", ");
158
94
  }
159
- function registerTransactionCommand(boards, name) {
160
- withJson(boards.command(`${name} <board>`)
161
- .description(name === "validate" ? "Validate a transaction" : "Apply a transaction")
162
- .requiredOption("-i, --input <file>", "Transaction JSON file; use - for stdin")
163
- .option("--tx-id <id>", "Override txId; generated when omitted")
164
- .option("--base-version <version>", "Override baseVersion")
165
- .addHelpText("after", `
166
- Input example:
167
- {"baseVersion":12,"operations":[{"type":"board.patch","payload":{"patch":{"title":"Launch plan"}}}]}
168
-
169
- Prefer semantic commands such as boards background, nodes, effects, or compositions for common edits.`))
170
- .action(async (target, options) => {
171
- try {
172
- const transaction = createTransactionInput(await readJsonObject(options.input, BOARD_TRANSACTION_INPUT_MAX_BYTES), options);
173
- const spaceId = resolveSpace(boards);
174
- const boardId = await resolveBoardId(spaceId, target);
175
- const board = createClient().space(spaceId).board(boardId);
176
- const result = await board[name](transaction);
177
- if (jsonRequested(options))
178
- return outJson(result);
179
- if (name === "validate")
180
- showValidation(result);
181
- else {
182
- const receipt = result;
183
- ok(`Board updated to version ${receipt.board.version}`);
184
- }
185
- }
186
- catch (cause) {
187
- handleHttp(cause);
188
- }
189
- });
190
- }
191
95
  function commandId(options) {
192
96
  return options.commandId?.trim() || randomUUID();
193
97
  }
@@ -313,14 +217,16 @@ export function registerBoards(program) {
313
217
  .description("Create a Board")
314
218
  .option("--title <title>", "Board title")
315
219
  .option("--mutation-id <id>", "Stable id for safe retries")
316
- .option("-i, --input <file>", "BoardCreateInput fields; use - for stdin")
220
+ .option("-i, --input <file>", "Semantic Board seed JSON; use - for stdin")
317
221
  .addHelpText("after", `
318
- For normal use, create an empty Board and add content with boards nodes, effects, and compositions.
319
- --input is intended for bulk creation and accepts BoardCreateInput fields except path and title.`))
222
+ Create an empty Board, or provide items, connections, effects, compositions, and metadata in --input.
223
+ Generate an editable seed:
224
+ cohub boards examples create > board.json
225
+ cohub boards create plan.board -i board.json`))
320
226
  .action(async (path, options) => {
321
227
  try {
322
228
  const content = options.input
323
- ? await readJsonObject(options.input, BOARD_CREATE_INPUT_MAX_BYTES)
229
+ ? await readBoardJsonObject(options.input, BOARD_CREATE_INPUT_MAX_BYTES)
324
230
  : {};
325
231
  if ("path" in content || "title" in content) {
326
232
  throw new Error("create input must not contain path or title; use the command argument and --title");
@@ -336,7 +242,7 @@ For normal use, create an empty Board and add content with boards nodes, effects
336
242
  if (jsonRequested(options))
337
243
  return outJson(result);
338
244
  ok(`Board created: ${result.board.id}`);
339
- showBoard(result);
245
+ showCreated(result);
340
246
  }
341
247
  catch (cause) {
342
248
  handleHttp(cause);
@@ -344,31 +250,30 @@ For normal use, create an empty Board and add content with boards nodes, effects
344
250
  });
345
251
  withJson(boards.command("inspect <board>")
346
252
  .alias("get")
347
- .description("Inspect a Board")
348
- .option("--include <sections>", "Comma-separated nodes,connections,effects,compositions,playback")
349
- .option("--viewport <rect>", "Viewport as x,y,width,height"))
253
+ .description("Show Board metadata and semantic resource counts")
254
+ .addHelpText("after", `
255
+ Inspect content with:
256
+ cohub boards items list <board> --json
257
+ cohub boards effects list <board> --json
258
+ cohub boards compositions list <board> --json`))
350
259
  .action(async (target, options) => {
351
260
  try {
352
261
  const spaceId = resolveSpace(boards);
353
262
  const boardId = await resolveBoardId(spaceId, target);
354
- const board = createClient().space(spaceId).board(boardId);
355
- if (!jsonRequested(options) && !options.include && !options.viewport) {
356
- return showSummary(await board.summary());
357
- }
358
- const result = await board.inspect({
359
- include: parseInspectSections(options.include),
360
- viewport: parseViewport(options.viewport),
361
- });
263
+ const result = await createClient().space(spaceId).board(boardId).summary();
362
264
  if (jsonRequested(options))
363
265
  return outJson(result);
364
- showBoard(result);
266
+ showSummary(result);
365
267
  }
366
268
  catch (cause) {
367
269
  handleHttp(cause);
368
270
  }
369
271
  });
370
272
  withJson(boards.command("capabilities <board>")
371
- .description("Show supported capabilities"))
273
+ .description("Show authoring schemas and runtime capabilities")
274
+ .addHelpText("after", `
275
+ Use --json for complete Item, patch, mutation, effect, Composition, and create JSON Schemas.
276
+ Use boards examples for editable starter JSON.`))
372
277
  .action(async (target, options) => {
373
278
  try {
374
279
  const spaceId = resolveSpace(boards);
@@ -388,30 +293,25 @@ For normal use, create an empty Board and add content with boards nodes, effects
388
293
  { key: "coordinates", label: "Coordinates / units" },
389
294
  { key: "digest", label: "Digest" },
390
295
  ]);
391
- const nodes = result.nodes;
392
- if (nodes) {
393
- console.log();
394
- table([{
395
- types: nodes.types.join(", "),
396
- colors: nodes.colors.join(", "),
397
- geos: nodes.geos.join(", "),
398
- drawPoints: nodes.coordinates.drawPoints,
399
- arrowEndpoints: nodes.coordinates.arrowEndpoints,
400
- }], [
401
- { key: "types", label: "Node types" },
402
- { key: "colors", label: "Colors" },
403
- { key: "geos", label: "Geo kinds" },
404
- { key: "drawPoints", label: "Draw points" },
405
- { key: "arrowEndpoints", label: "Arrow endpoints" },
406
- ]);
407
- }
296
+ console.log();
297
+ table([{
298
+ types: result.items.types.join(", "),
299
+ colors: result.items.colors.join(", "),
300
+ shapes: result.items.shapes.join(", "),
301
+ drawPoints: result.items.coordinates.drawPoints,
302
+ arrowEndpoints: result.items.coordinates.arrowEndpoints,
303
+ }], [
304
+ { key: "types", label: "Item types" },
305
+ { key: "colors", label: "Colors" },
306
+ { key: "shapes", label: "Shapes" },
307
+ { key: "drawPoints", label: "Draw points" },
308
+ { key: "arrowEndpoints", label: "Arrow endpoints" },
309
+ ]);
408
310
  }
409
311
  catch (cause) {
410
312
  handleHttp(cause);
411
313
  }
412
314
  });
413
- registerTransactionCommand(boards, "validate");
414
- registerTransactionCommand(boards, "apply");
415
315
  registerBoardDomainCommands(boards);
416
316
  registerExportCommand(boards);
417
317
  withJson(boards.command("play <board> <composition-id>")
@@ -514,13 +414,13 @@ For normal use, create an empty Board and add content with boards nodes, effects
514
414
  process.stdout.write(`${JSON.stringify(event)}\n`);
515
415
  return;
516
416
  }
517
- if (event.type === "board.transaction.applied") {
518
- process.stdout.write(`version ${event.payload.version} transaction ${event.payload.txId} operations ${event.payload.operations.length}\n`);
417
+ if (event.type === "board.changed") {
418
+ process.stdout.write(`version ${event.payload.version} mutation ${event.payload.mutationId}\n`);
519
419
  }
520
420
  else if (event.type === "board.playback.changed") {
521
421
  process.stdout.write(`${event.payload.status} composition ${event.payload.compositionId} position ${event.payload.position}\n`);
522
422
  }
523
- else {
423
+ else if (event.type === "board.awareness.updated") {
524
424
  process.stdout.write(`awareness ${event.payload.actorName} ${event.payload.update.type}\n`);
525
425
  }
526
426
  },
@@ -9,7 +9,7 @@ function readJsonObject(pathOrJson) {
9
9
  return parsed;
10
10
  }
11
11
  export function registerCronJobs(program) {
12
- const cmd = program.command("cron-jobs", { hidden: true }).description("Scheduled jobs");
12
+ const cmd = program.command("cron-jobs").description("Scheduled jobs");
13
13
  cmd
14
14
  .command("ls [spaceId]")
15
15
  .alias("list")
@@ -2,7 +2,7 @@ import { createClient } from "../client.js";
2
2
  import { table, json as outJson, jsonRequested, handleHttp } from "../output.js";
3
3
  export function registerTasks(program, dependencies = {}) {
4
4
  const getClient = dependencies.createClient ?? createClient;
5
- const cmd = program.command("tasks", { hidden: true }).description("Task runs");
5
+ const cmd = program.command("tasks").description("Task runs");
6
6
  cmd
7
7
  .command("ls")
8
8
  .alias("list")
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@neta-art/cohub-cli",
3
- "version": "4.0.0",
3
+ "version": "5.0.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": "6.0.0"
22
+ "@neta-art/cohub": "7.0.0"
23
23
  },
24
24
  "publishConfig": {
25
25
  "access": "public"