@creator-notes/cnotes 0.16.11

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.
Files changed (158) hide show
  1. package/.claude-plugin/plugin.json +14 -0
  2. package/.mcp.json +12 -0
  3. package/LICENSE +21 -0
  4. package/README.md +303 -0
  5. package/dist/cn.d.ts +3 -0
  6. package/dist/cn.d.ts.map +1 -0
  7. package/dist/cn.js +124 -0
  8. package/dist/cn.js.map +1 -0
  9. package/dist/commands/auth.d.ts +10 -0
  10. package/dist/commands/auth.d.ts.map +1 -0
  11. package/dist/commands/auth.js +188 -0
  12. package/dist/commands/auth.js.map +1 -0
  13. package/dist/commands/canvas.d.ts +3 -0
  14. package/dist/commands/canvas.d.ts.map +1 -0
  15. package/dist/commands/canvas.js +1383 -0
  16. package/dist/commands/canvas.js.map +1 -0
  17. package/dist/commands/claude-hook.d.ts +28 -0
  18. package/dist/commands/claude-hook.d.ts.map +1 -0
  19. package/dist/commands/claude-hook.js +59 -0
  20. package/dist/commands/claude-hook.js.map +1 -0
  21. package/dist/commands/config.d.ts +3 -0
  22. package/dist/commands/config.d.ts.map +1 -0
  23. package/dist/commands/config.js +47 -0
  24. package/dist/commands/config.js.map +1 -0
  25. package/dist/commands/files.d.ts +3 -0
  26. package/dist/commands/files.d.ts.map +1 -0
  27. package/dist/commands/files.js +119 -0
  28. package/dist/commands/files.js.map +1 -0
  29. package/dist/commands/init.d.ts +3 -0
  30. package/dist/commands/init.d.ts.map +1 -0
  31. package/dist/commands/init.js +473 -0
  32. package/dist/commands/init.js.map +1 -0
  33. package/dist/commands/mcp.d.ts +15 -0
  34. package/dist/commands/mcp.d.ts.map +1 -0
  35. package/dist/commands/mcp.js +118 -0
  36. package/dist/commands/mcp.js.map +1 -0
  37. package/dist/commands/memory.d.ts +3 -0
  38. package/dist/commands/memory.d.ts.map +1 -0
  39. package/dist/commands/memory.js +150 -0
  40. package/dist/commands/memory.js.map +1 -0
  41. package/dist/commands/notes.d.ts +3 -0
  42. package/dist/commands/notes.d.ts.map +1 -0
  43. package/dist/commands/notes.js +706 -0
  44. package/dist/commands/notes.js.map +1 -0
  45. package/dist/commands/operations.d.ts +18 -0
  46. package/dist/commands/operations.d.ts.map +1 -0
  47. package/dist/commands/operations.js +231 -0
  48. package/dist/commands/operations.js.map +1 -0
  49. package/dist/commands/relationships.d.ts +3 -0
  50. package/dist/commands/relationships.d.ts.map +1 -0
  51. package/dist/commands/relationships.js +94 -0
  52. package/dist/commands/relationships.js.map +1 -0
  53. package/dist/commands/schema.d.ts +12 -0
  54. package/dist/commands/schema.d.ts.map +1 -0
  55. package/dist/commands/schema.js +85 -0
  56. package/dist/commands/schema.js.map +1 -0
  57. package/dist/commands/search.d.ts +3 -0
  58. package/dist/commands/search.d.ts.map +1 -0
  59. package/dist/commands/search.js +57 -0
  60. package/dist/commands/search.js.map +1 -0
  61. package/dist/commands/theme.d.ts +3 -0
  62. package/dist/commands/theme.d.ts.map +1 -0
  63. package/dist/commands/theme.js +184 -0
  64. package/dist/commands/theme.js.map +1 -0
  65. package/dist/commands/timeline.d.ts +3 -0
  66. package/dist/commands/timeline.d.ts.map +1 -0
  67. package/dist/commands/timeline.js +97 -0
  68. package/dist/commands/timeline.js.map +1 -0
  69. package/dist/commands/types.d.ts +3 -0
  70. package/dist/commands/types.d.ts.map +1 -0
  71. package/dist/commands/types.js +139 -0
  72. package/dist/commands/types.js.map +1 -0
  73. package/dist/commands/versions.d.ts +3 -0
  74. package/dist/commands/versions.d.ts.map +1 -0
  75. package/dist/commands/versions.js +120 -0
  76. package/dist/commands/versions.js.map +1 -0
  77. package/dist/commands/workspace.d.ts +13 -0
  78. package/dist/commands/workspace.d.ts.map +1 -0
  79. package/dist/commands/workspace.js +176 -0
  80. package/dist/commands/workspace.js.map +1 -0
  81. package/dist/lib/api-client.d.ts +45 -0
  82. package/dist/lib/api-client.d.ts.map +1 -0
  83. package/dist/lib/api-client.js +198 -0
  84. package/dist/lib/api-client.js.map +1 -0
  85. package/dist/lib/auth-store.d.ts +47 -0
  86. package/dist/lib/auth-store.d.ts.map +1 -0
  87. package/dist/lib/auth-store.js +116 -0
  88. package/dist/lib/auth-store.js.map +1 -0
  89. package/dist/lib/brand.d.ts +32 -0
  90. package/dist/lib/brand.d.ts.map +1 -0
  91. package/dist/lib/brand.js +32 -0
  92. package/dist/lib/brand.js.map +1 -0
  93. package/dist/lib/build-schema.d.ts +97 -0
  94. package/dist/lib/build-schema.d.ts.map +1 -0
  95. package/dist/lib/build-schema.js +139 -0
  96. package/dist/lib/build-schema.js.map +1 -0
  97. package/dist/lib/canvas-read.d.ts +54 -0
  98. package/dist/lib/canvas-read.d.ts.map +1 -0
  99. package/dist/lib/canvas-read.js +145 -0
  100. package/dist/lib/canvas-read.js.map +1 -0
  101. package/dist/lib/claude-session.d.ts +73 -0
  102. package/dist/lib/claude-session.d.ts.map +1 -0
  103. package/dist/lib/claude-session.js +104 -0
  104. package/dist/lib/claude-session.js.map +1 -0
  105. package/dist/lib/config.d.ts +28 -0
  106. package/dist/lib/config.d.ts.map +1 -0
  107. package/dist/lib/config.js +66 -0
  108. package/dist/lib/config.js.map +1 -0
  109. package/dist/lib/env.d.ts +14 -0
  110. package/dist/lib/env.d.ts.map +1 -0
  111. package/dist/lib/env.js +16 -0
  112. package/dist/lib/env.js.map +1 -0
  113. package/dist/lib/errors.d.ts +47 -0
  114. package/dist/lib/errors.d.ts.map +1 -0
  115. package/dist/lib/errors.js +194 -0
  116. package/dist/lib/errors.js.map +1 -0
  117. package/dist/lib/fs-utils.d.ts +2 -0
  118. package/dist/lib/fs-utils.d.ts.map +1 -0
  119. package/dist/lib/fs-utils.js +16 -0
  120. package/dist/lib/fs-utils.js.map +1 -0
  121. package/dist/lib/install-hook.d.ts +86 -0
  122. package/dist/lib/install-hook.d.ts.map +1 -0
  123. package/dist/lib/install-hook.js +168 -0
  124. package/dist/lib/install-hook.js.map +1 -0
  125. package/dist/lib/install-mcp.d.ts +21 -0
  126. package/dist/lib/install-mcp.d.ts.map +1 -0
  127. package/dist/lib/install-mcp.js +133 -0
  128. package/dist/lib/install-mcp.js.map +1 -0
  129. package/dist/lib/install-skill.d.ts +49 -0
  130. package/dist/lib/install-skill.d.ts.map +1 -0
  131. package/dist/lib/install-skill.js +113 -0
  132. package/dist/lib/install-skill.js.map +1 -0
  133. package/dist/lib/output.d.ts +29 -0
  134. package/dist/lib/output.d.ts.map +1 -0
  135. package/dist/lib/output.js +78 -0
  136. package/dist/lib/output.js.map +1 -0
  137. package/dist/lib/resolve-note.d.ts +7 -0
  138. package/dist/lib/resolve-note.d.ts.map +1 -0
  139. package/dist/lib/resolve-note.js +23 -0
  140. package/dist/lib/resolve-note.js.map +1 -0
  141. package/dist/lib/stdin.d.ts +5 -0
  142. package/dist/lib/stdin.d.ts.map +1 -0
  143. package/dist/lib/stdin.js +11 -0
  144. package/dist/lib/stdin.js.map +1 -0
  145. package/dist/lib/style.d.ts +10 -0
  146. package/dist/lib/style.d.ts.map +1 -0
  147. package/dist/lib/style.js +17 -0
  148. package/dist/lib/style.js.map +1 -0
  149. package/dist/lib/themes.d.ts +44 -0
  150. package/dist/lib/themes.d.ts.map +1 -0
  151. package/dist/lib/themes.js +168 -0
  152. package/dist/lib/themes.js.map +1 -0
  153. package/dist/mcp-server.d.ts +3 -0
  154. package/dist/mcp-server.d.ts.map +1 -0
  155. package/dist/mcp-server.js +782 -0
  156. package/dist/mcp-server.js.map +1 -0
  157. package/package.json +66 -0
  158. package/skills/cnotes/SKILL.md +680 -0
@@ -0,0 +1,1383 @@
1
+ import { Option } from "commander";
2
+ import { ApiClient } from "../lib/api-client.js";
3
+ import { resolveConfig } from "../lib/config.js";
4
+ import { handleError, ValidationError } from "../lib/errors.js";
5
+ import { readFileSafe } from "../lib/fs-utils.js";
6
+ import { outputJson, outputTable, outputQuiet, truncate, timeAgo } from "../lib/output.js";
7
+ import { readCanvasMarkdown, renderCanvasMarkdown } from "../lib/canvas-read.js";
8
+ import { resolveAgentSession } from "../lib/claude-session.js";
9
+ import { cnotesEnv } from "../lib/env.js";
10
+ import { WARN, dim } from "../lib/style.js";
11
+ /**
12
+ * A list item is a *collection of member notes* with a short markdown description
13
+ * that frames them — not a place to dump prose. When a description carries several
14
+ * distinct items (a markdown list, or multiple paragraphs), those items almost always
15
+ * want to be notes. Detect that shape so we can warn when a list is created with such a
16
+ * description but zero `--notes`. See the "list items are collections of notes" rule in
17
+ * the CLI skill (SKILL.md).
18
+ */
19
+ function descriptionLooksLikeContent(description) {
20
+ const lines = description.split(/\r?\n/);
21
+ const listMarker = /^\s*(?:[-*+]\s+|\d+[.)]\s+)/;
22
+ const bulletLines = lines.filter((l) => listMarker.test(l)).length;
23
+ if (bulletLines >= 2)
24
+ return true;
25
+ // Multiple non-empty paragraphs also read as content rather than a one-line frame.
26
+ const paragraphs = description.split(/\n\s*\n/).map((p) => p.trim()).filter(Boolean);
27
+ return paragraphs.length >= 2;
28
+ }
29
+ /** Emit a fail-loud (stderr, non-blocking) nudge when a list item is being used as a text block. */
30
+ function warnListItemMisuse(description, noteCount, label) {
31
+ if (noteCount > 0)
32
+ return;
33
+ if (!descriptionLooksLikeContent(description))
34
+ return;
35
+ const which = label ? ` "${label}"` : "";
36
+ console.error(`${WARN} List item${which} has no member notes, but its description looks like content (multiple items). ` +
37
+ `A list item is a collection of notes, not a text block.\n` +
38
+ ` ${dim("→ Create a note per item (e.g. a Question note per open question), then pass --notes <ids>.")}\n` +
39
+ ` ${dim("→ For a standalone formatted block, use `cnotes canvas add-richtext` instead.")}`);
40
+ }
41
+ /** Deep-walk a layout doc and warn for every leaf list item used as a text block. */
42
+ function warnLayoutListMisuse(node) {
43
+ if (Array.isArray(node)) {
44
+ for (const child of node)
45
+ warnLayoutListMisuse(child);
46
+ return;
47
+ }
48
+ if (!node || typeof node !== "object")
49
+ return;
50
+ const n = node;
51
+ if (n.type === "list" && typeof n.description === "string") {
52
+ const noteIds = Array.isArray(n.noteIds) ? n.noteIds : [];
53
+ const label = n.description.split(/\n\s*\n/)[0]?.trim().slice(0, 60);
54
+ warnListItemMisuse(n.description, noteIds.length, label);
55
+ }
56
+ for (const value of Object.values(n))
57
+ warnLayoutListMisuse(value);
58
+ }
59
+ function parseArrowMode(mode) {
60
+ switch (mode) {
61
+ case "end":
62
+ return { arrowStart: false, arrowEnd: true };
63
+ case "start":
64
+ return { arrowStart: true, arrowEnd: false };
65
+ case "both":
66
+ return { arrowStart: true, arrowEnd: true };
67
+ case "none":
68
+ return { arrowStart: false, arrowEnd: false };
69
+ default:
70
+ throw new ValidationError(`Invalid --arrow value "${mode}". Expected one of: end, start, both, none.`);
71
+ }
72
+ }
73
+ function parseLineStyle(style) {
74
+ if (style === "solid" || style === "dashed")
75
+ return style;
76
+ throw new ValidationError(`Invalid --line-style value "${style}". Expected: solid or dashed.`);
77
+ }
78
+ export function registerCanvasCommands(program) {
79
+ const canvas = program
80
+ .command("canvas")
81
+ .alias("c")
82
+ .description("Manage canvases");
83
+ canvas
84
+ .command("list")
85
+ .description("List canvases in the current workspace")
86
+ .option("--include-archived", "Include archived canvases")
87
+ .addHelpText("after", `
88
+ Examples:
89
+ $ cnotes canvas list
90
+ $ cnotes canvas list --include-archived --json
91
+ `)
92
+ .action(async (opts) => {
93
+ const cfg = resolveConfig(program.opts());
94
+ const api = new ApiClient(cfg.serverUrl, cfg.token);
95
+ try {
96
+ const params = { workspaceId: cfg.workspaceId };
97
+ if (opts.includeArchived) {
98
+ params.includeArchived = "true";
99
+ }
100
+ const data = await api.get("/api/canvas", params);
101
+ const list = data.canvases || data;
102
+ if (cfg.json) {
103
+ outputJson(list, cfg.fields);
104
+ return;
105
+ }
106
+ const items = Array.isArray(list) ? list : [];
107
+ if (items.length === 0) {
108
+ console.log("No canvases found.");
109
+ return;
110
+ }
111
+ const rows = items.map((c) => [
112
+ String(c._id || c.id || ""),
113
+ truncate(String(c.name || "(unnamed)"), 30),
114
+ c.isDefault ? "(default)" : c.archivedAt ? "(archived)" : "",
115
+ c.updatedAt ? timeAgo(c.updatedAt) : "",
116
+ ]);
117
+ outputTable(["ID", "NAME", "", "UPDATED"], rows);
118
+ }
119
+ catch (error) {
120
+ handleError(error, cfg.json);
121
+ }
122
+ });
123
+ canvas
124
+ .command("get")
125
+ .description("Get canvas details with nodes and edges")
126
+ .argument("<canvasId>", "Canvas ID")
127
+ .addHelpText("after", `
128
+ Examples:
129
+ $ cnotes canvas get <canvasId> --json
130
+ `)
131
+ .action(async (canvasId) => {
132
+ const cfg = resolveConfig(program.opts());
133
+ const api = new ApiClient(cfg.serverUrl, cfg.token);
134
+ try {
135
+ const data = await api.get(`/api/canvas/${encodeURIComponent(canvasId)}?workspaceId=${encodeURIComponent(cfg.workspaceId)}`);
136
+ if (cfg.json) {
137
+ outputJson(data);
138
+ return;
139
+ }
140
+ console.log(`Canvas: ${data.name || "(unnamed)"}`);
141
+ console.log(`ID: ${canvasId}`);
142
+ const canvas = data.canvas;
143
+ const goal = canvas?.goal || data.goal;
144
+ const targetAudience = canvas?.targetAudience || data.targetAudience;
145
+ if (goal)
146
+ console.log(`Goal: ${goal}`);
147
+ if (targetAudience)
148
+ console.log(`Audience: ${targetAudience}`);
149
+ const nodes = data.nodes;
150
+ const edges = data.edges;
151
+ if (nodes && nodes.length > 0) {
152
+ console.log(`\nNodes (${nodes.length}):`);
153
+ for (const node of nodes) {
154
+ const noteTitle = node.note?.title || node.content || node.noteId || "?";
155
+ console.log(` ${node._id || node.id}: ${noteTitle} (${node.positionX}, ${node.positionY})`);
156
+ }
157
+ }
158
+ if (edges && edges.length > 0) {
159
+ console.log(`\nEdges (${edges.length}):`);
160
+ for (const edge of edges) {
161
+ const label = edge.label ? ` [${edge.label}]` : "";
162
+ console.log(` ${edge.sourceNodeId} -> ${edge.targetNodeId}${label}`);
163
+ }
164
+ }
165
+ const linkNodes = data.canvasLinkNodes;
166
+ if (linkNodes && linkNodes.length > 0) {
167
+ console.log(`\nCanvas Links (${linkNodes.length}):`);
168
+ for (const ln of linkNodes) {
169
+ const linked = ln.linkedCanvas;
170
+ const name = linked?.name || ln.linkedCanvasId || "?";
171
+ console.log(` ${ln.id}: -> ${name} (${ln.positionX}, ${ln.positionY})`);
172
+ }
173
+ }
174
+ }
175
+ catch (error) {
176
+ handleError(error, cfg.json);
177
+ }
178
+ });
179
+ canvas
180
+ .command("digest")
181
+ .description("Get the AI-generated digest (summary) of a canvas")
182
+ .argument("<canvasId>", "Canvas ID")
183
+ .action(async (canvasId) => {
184
+ const cfg = resolveConfig(program.opts());
185
+ const api = new ApiClient(cfg.serverUrl, cfg.token);
186
+ try {
187
+ const data = await api.get(`/api/canvas/${encodeURIComponent(canvasId)}/digest?workspaceId=${encodeURIComponent(cfg.workspaceId)}`);
188
+ if (cfg.json) {
189
+ outputJson(data);
190
+ return;
191
+ }
192
+ const status = String(data.status || "none");
193
+ if (status === "none") {
194
+ console.log("No digest available for this canvas.");
195
+ return;
196
+ }
197
+ if (status !== "ready") {
198
+ console.log(`Digest status: ${status} — try again shortly.`);
199
+ return;
200
+ }
201
+ const canvasName = data.canvasName || "(unnamed)";
202
+ console.log(`Canvas: ${canvasName}\n`);
203
+ console.log(String(data.digest || ""));
204
+ const themes = data.themes;
205
+ if (themes && themes.length > 0) {
206
+ console.log(`\nThemes:`);
207
+ for (const t of themes) {
208
+ console.log(` • ${t}`);
209
+ }
210
+ }
211
+ const notes = data.notes;
212
+ if (notes && notes.length > 0) {
213
+ console.log(`\nNotes (${notes.length}):`);
214
+ for (const n of notes) {
215
+ console.log(` [${n.displayId}] ${n.title} (${n.type})`);
216
+ if (n.summary) {
217
+ const trimmed = n.summary.replace(/\s+/g, " ").trim();
218
+ const truncated = trimmed.length > 160 ? trimmed.slice(0, 157) + "..." : trimmed;
219
+ console.log(` ${truncated}`);
220
+ }
221
+ else {
222
+ console.log(` (no summary yet)`);
223
+ }
224
+ }
225
+ }
226
+ else {
227
+ console.log(`\nNotes: ${data.noteCount || 0}`);
228
+ }
229
+ if (data.generatedAt) {
230
+ console.log(`Generated: ${timeAgo(data.generatedAt)}`);
231
+ }
232
+ }
233
+ catch (error) {
234
+ handleError(error, cfg.json);
235
+ }
236
+ });
237
+ canvas
238
+ .command("activity")
239
+ .description("Show recent activity (edits, agent runs, gestures, restores) on a canvas, newest first. " +
240
+ "Use this to see what changed and who changed it before making further edits, or to confirm " +
241
+ "an agent run actually applied.")
242
+ .argument("<canvasId>", "Canvas ID")
243
+ .option("--limit <n>", "Max entries (default 50, max 200)", "50")
244
+ .action(async (canvasId, opts) => {
245
+ const cfg = resolveConfig(program.opts());
246
+ const api = new ApiClient(cfg.serverUrl, cfg.token);
247
+ try {
248
+ const params = { workspaceId: cfg.workspaceId };
249
+ if (opts.limit !== undefined)
250
+ params.limit = String(opts.limit);
251
+ const data = await api.get(`/api/canvas/${encodeURIComponent(canvasId)}/activity`, params);
252
+ if (cfg.json) {
253
+ outputJson(data);
254
+ return;
255
+ }
256
+ const ops = data.operations ?? [];
257
+ if (ops.length === 0) {
258
+ console.log("No activity recorded yet.");
259
+ return;
260
+ }
261
+ console.log(`Canvas: ${data.canvasName}\n`);
262
+ const rows = ops.map((op) => {
263
+ const summary = op.structuredSummary?.headline ||
264
+ op.summary ||
265
+ op.userPrompt ||
266
+ "";
267
+ return [
268
+ timeAgo(op.startedAt),
269
+ truncate(op.actorName || "—", 18),
270
+ truncate(op.operationType, 22),
271
+ op.status,
272
+ String(op.eventCount ?? 0),
273
+ truncate(summary, 60),
274
+ ];
275
+ });
276
+ outputTable(["WHEN", "WHO", "TYPE", "STATUS", "EVENTS", "SUMMARY"], rows);
277
+ }
278
+ catch (error) {
279
+ handleError(error, cfg.json);
280
+ }
281
+ });
282
+ canvas
283
+ .command("read")
284
+ .description("Read every note on a canvas as one concatenated markdown document, " +
285
+ "in display order (top-to-bottom, then left-to-right). Use this to think " +
286
+ "across a canvas without making multiple round-trips.")
287
+ .argument("<canvasId>", "Canvas ID")
288
+ .addHelpText("after", `
289
+ Examples:
290
+ $ cnotes canvas read <canvasId>
291
+ `)
292
+ .action(async (canvasId) => {
293
+ const cfg = resolveConfig(program.opts());
294
+ const api = new ApiClient(cfg.serverUrl, cfg.token);
295
+ try {
296
+ const result = await readCanvasMarkdown(api, cfg.workspaceId, canvasId);
297
+ if (cfg.json) {
298
+ outputJson(result);
299
+ return;
300
+ }
301
+ console.log(renderCanvasMarkdown(result));
302
+ }
303
+ catch (error) {
304
+ handleError(error, cfg.json);
305
+ }
306
+ });
307
+ canvas
308
+ .command("create")
309
+ .description("Create a new canvas")
310
+ .argument("<name>", "Canvas name")
311
+ .option("--goal <text>", "Canvas goal — the purpose, desired outcome, and what it should communicate")
312
+ .option("--audience <text>", "Target audience — who this canvas is meant to serve or inform")
313
+ .action(async (name, opts) => {
314
+ const cfg = resolveConfig(program.opts());
315
+ const api = new ApiClient(cfg.serverUrl, cfg.token);
316
+ try {
317
+ const body = {
318
+ workspaceId: cfg.workspaceId,
319
+ title: name,
320
+ };
321
+ if (opts.goal !== undefined)
322
+ body.goal = opts.goal;
323
+ if (opts.audience !== undefined)
324
+ body.targetAudience = opts.audience;
325
+ const data = await api.post("/api/canvas", body);
326
+ const canvas = data.canvas ?? {};
327
+ if (cfg.quiet) {
328
+ outputQuiet(String(canvas.id || canvas._id || ""));
329
+ return;
330
+ }
331
+ if (cfg.json) {
332
+ outputJson(data);
333
+ }
334
+ else {
335
+ console.log(`Created canvas: ${name}`);
336
+ console.log(`ID: ${canvas.id || canvas._id || "(unknown)"}`);
337
+ }
338
+ }
339
+ catch (error) {
340
+ handleError(error, cfg.json);
341
+ }
342
+ });
343
+ canvas
344
+ .command("update")
345
+ .description("Update canvas metadata (title, goal, audience)")
346
+ .argument("<canvasId>", "Canvas ID")
347
+ .option("--title <text>", "New canvas title")
348
+ .option("--goal <text>", "Canvas goal — the purpose, desired outcome, and what it should communicate")
349
+ .option("--audience <text>", "Target audience — who this canvas is meant to serve or inform")
350
+ .action(async (canvasId, opts) => {
351
+ const cfg = resolveConfig(program.opts());
352
+ const api = new ApiClient(cfg.serverUrl, cfg.token);
353
+ try {
354
+ if (opts.title === undefined && opts.goal === undefined && opts.audience === undefined) {
355
+ handleError(new ValidationError("At least one of --title, --goal, or --audience is required"), cfg.json);
356
+ return;
357
+ }
358
+ const body = {};
359
+ if (opts.title !== undefined)
360
+ body.title = opts.title;
361
+ if (opts.goal !== undefined)
362
+ body.goal = opts.goal;
363
+ if (opts.audience !== undefined)
364
+ body.targetAudience = opts.audience;
365
+ const data = await api.patch(`/api/canvas/${encodeURIComponent(canvasId)}`, body, { workspaceId: cfg.workspaceId });
366
+ if (cfg.json) {
367
+ outputJson(data);
368
+ }
369
+ else {
370
+ const changes = [];
371
+ if (opts.title)
372
+ changes.push(`title="${opts.title}"`);
373
+ if (opts.goal)
374
+ changes.push("goal");
375
+ if (opts.audience)
376
+ changes.push("audience");
377
+ console.log(`Updated canvas ${canvasId}: ${changes.join(", ")}`);
378
+ }
379
+ }
380
+ catch (error) {
381
+ handleError(error, cfg.json);
382
+ }
383
+ });
384
+ canvas
385
+ .command("delete")
386
+ .description("Delete a canvas")
387
+ .argument("<canvasId>", "Canvas ID")
388
+ .action(async (canvasId) => {
389
+ const cfg = resolveConfig(program.opts());
390
+ const api = new ApiClient(cfg.serverUrl, cfg.token);
391
+ try {
392
+ const data = await api.delete(`/api/canvas/${canvasId}`);
393
+ if (cfg.json) {
394
+ outputJson(data);
395
+ }
396
+ else {
397
+ console.log(`Deleted canvas ${canvasId}`);
398
+ }
399
+ }
400
+ catch (error) {
401
+ handleError(error, cfg.json);
402
+ }
403
+ });
404
+ canvas
405
+ .command("set-as-home")
406
+ .description("Set a canvas as the workspace home/default canvas")
407
+ .argument("<canvasId>", "Canvas ID")
408
+ .action(async (canvasId) => {
409
+ const cfg = resolveConfig(program.opts());
410
+ const api = new ApiClient(cfg.serverUrl, cfg.token);
411
+ try {
412
+ const data = await api.post(`/api/canvas/${canvasId}`, {
413
+ action: "setAsHome",
414
+ });
415
+ if (cfg.json) {
416
+ outputJson(data);
417
+ }
418
+ else {
419
+ console.log(`Canvas ${canvasId} set as workspace home.`);
420
+ }
421
+ }
422
+ catch (error) {
423
+ handleError(error, cfg.json);
424
+ }
425
+ });
426
+ canvas
427
+ .command("archive")
428
+ .description("Archive a canvas (soft delete)")
429
+ .argument("<canvasId>", "Canvas ID")
430
+ .action(async (canvasId) => {
431
+ const cfg = resolveConfig(program.opts());
432
+ const api = new ApiClient(cfg.serverUrl, cfg.token);
433
+ try {
434
+ const data = await api.post(`/api/canvas/${canvasId}`, {
435
+ action: "archive",
436
+ });
437
+ if (cfg.json) {
438
+ outputJson(data);
439
+ }
440
+ else {
441
+ console.log(`Archived canvas ${canvasId}`);
442
+ }
443
+ }
444
+ catch (error) {
445
+ handleError(error, cfg.json);
446
+ }
447
+ });
448
+ canvas
449
+ .command("unarchive")
450
+ .description("Unarchive (restore) an archived canvas")
451
+ .argument("<canvasId>", "Canvas ID")
452
+ .action(async (canvasId) => {
453
+ const cfg = resolveConfig(program.opts());
454
+ const api = new ApiClient(cfg.serverUrl, cfg.token);
455
+ try {
456
+ const data = await api.post(`/api/canvas/${canvasId}`, {
457
+ action: "unarchive",
458
+ });
459
+ if (cfg.json) {
460
+ outputJson(data);
461
+ }
462
+ else {
463
+ console.log(`Unarchived canvas ${canvasId}`);
464
+ }
465
+ }
466
+ catch (error) {
467
+ handleError(error, cfg.json);
468
+ }
469
+ });
470
+ canvas
471
+ .command("add-node")
472
+ .description("Add a note to a canvas")
473
+ .argument("<canvasId>", "Canvas ID")
474
+ .requiredOption("--note <noteId>", "Note display ID or Convex ID")
475
+ .option("--x <n>", "X position", "100")
476
+ .option("--y <n>", "Y position", "100")
477
+ .addHelpText("after", `
478
+ Examples:
479
+ $ cnotes canvas add-node <canvasId> --note MEETING-12 --x 200 --y 300
480
+ `)
481
+ .action(async (canvasId, opts) => {
482
+ const cfg = resolveConfig(program.opts());
483
+ const api = new ApiClient(cfg.serverUrl, cfg.token);
484
+ try {
485
+ const data = await api.post(`/api/canvas/${canvasId}`, {
486
+ action: "addNode",
487
+ noteId: opts.note,
488
+ positionX: parseInt(opts.x, 10),
489
+ positionY: parseInt(opts.y, 10),
490
+ });
491
+ if (cfg.quiet) {
492
+ outputQuiet(String(data.nodeId || data._id || ""));
493
+ return;
494
+ }
495
+ if (cfg.json) {
496
+ outputJson(data);
497
+ }
498
+ else {
499
+ console.log(`Added note ${opts.note} to canvas at (${opts.x}, ${opts.y})`);
500
+ }
501
+ }
502
+ catch (error) {
503
+ handleError(error, cfg.json);
504
+ }
505
+ });
506
+ canvas
507
+ .command("add-text")
508
+ .description("Add a text annotation to a canvas")
509
+ .argument("<canvasId>", "Canvas ID")
510
+ .requiredOption("--text <content>", "Text content")
511
+ .addOption(new Option("--size <size>", "Text size: heading (default, larger) or paragraph (smaller readable text)").choices(["heading", "paragraph"]).default("heading"))
512
+ .addOption(new Option("--color <variant>", "Color variant: normal (heading color), muted (body text color), highlighted (accent)").choices(["normal", "muted", "highlighted"]))
513
+ .option("--x <n>", "X position", "100")
514
+ .option("--y <n>", "Y position", "100")
515
+ .addHelpText("after", `
516
+ Examples:
517
+ $ cnotes canvas add-text <canvasId> --text "Section A" --size heading
518
+ $ cnotes canvas add-text <canvasId> --text "Note body" --size paragraph --color muted
519
+ `)
520
+ .action(async (canvasId, opts) => {
521
+ const cfg = resolveConfig(program.opts());
522
+ const api = new ApiClient(cfg.serverUrl, cfg.token);
523
+ const fontSize = opts.size === "paragraph" ? 18 : 32;
524
+ try {
525
+ const data = await api.post(`/api/canvas/${canvasId}`, {
526
+ action: "addTextNode",
527
+ content: opts.text,
528
+ fontSize,
529
+ colorVariant: opts.color,
530
+ positionX: parseInt(opts.x, 10),
531
+ positionY: parseInt(opts.y, 10),
532
+ });
533
+ if (cfg.json) {
534
+ outputJson(data);
535
+ }
536
+ else {
537
+ console.log(`Added text node to canvas at (${opts.x}, ${opts.y})`);
538
+ }
539
+ }
540
+ catch (error) {
541
+ handleError(error, cfg.json);
542
+ }
543
+ });
544
+ canvas
545
+ .command("add-richtext")
546
+ .description("Add a richtext annotation to a canvas (supports formatted text with headings, lists, bold, etc.)")
547
+ .argument("<canvasId>", "Canvas ID")
548
+ .option("--content <content>", "Richtext content inline (markdown)")
549
+ .option("--content-file <path>", "Read richtext content (markdown) from a file")
550
+ .addOption(new Option("--size <size>", "Display size: small (560px), medium (1120px), large (1680px billboard)").choices(["small", "medium", "large"]))
551
+ .option("--color <hex>", "Background color hex (e.g. #3B82F6)")
552
+ .option("--x <n>", "X position", "100")
553
+ .option("--y <n>", "Y position", "100")
554
+ .action(async (canvasId, opts) => {
555
+ const cfg = resolveConfig(program.opts());
556
+ const api = new ApiClient(cfg.serverUrl, cfg.token);
557
+ let content;
558
+ if (opts.contentFile) {
559
+ content = readFileSafe(opts.contentFile);
560
+ }
561
+ else if (opts.content) {
562
+ content = opts.content;
563
+ }
564
+ else {
565
+ handleError(new ValidationError("Either --content or --content-file is required"), cfg.json);
566
+ return;
567
+ }
568
+ try {
569
+ const data = await api.post(`/api/canvas/${canvasId}`, {
570
+ action: "addRichtextNode",
571
+ content,
572
+ size: opts.size,
573
+ colorHex: opts.color,
574
+ positionX: parseInt(opts.x, 10),
575
+ positionY: parseInt(opts.y, 10),
576
+ });
577
+ if (cfg.json) {
578
+ outputJson(data);
579
+ }
580
+ else {
581
+ const heightInfo = data.estimatedHeight ? ` — estimated height: ${data.estimatedHeight}px` : '';
582
+ console.log(`Added richtext node to canvas at (${opts.x}, ${opts.y})${heightInfo}`);
583
+ }
584
+ }
585
+ catch (error) {
586
+ handleError(error, cfg.json);
587
+ }
588
+ });
589
+ canvas
590
+ .command("add-list")
591
+ .description("Add a list/grid node grouping notes on a canvas. The description is the only label; its first paragraph derives the searchable title.")
592
+ .argument("<canvasId>", "Canvas ID")
593
+ .requiredOption("--description <markdown>", "Rich-text description (markdown). Required. First paragraph derives the searchable title.")
594
+ .option("--notes <ids>", "Note IDs (comma-separated, display IDs like TASK-6 or Convex IDs)")
595
+ .option("--view <mode>", "View mode: list or grid", "list")
596
+ .option("--x <n>", "X position", "100")
597
+ .option("--y <n>", "Y position", "100")
598
+ .action(async (canvasId, opts) => {
599
+ const cfg = resolveConfig(program.opts());
600
+ const api = new ApiClient(cfg.serverUrl, cfg.token);
601
+ try {
602
+ if (opts.view !== "list" && opts.view !== "grid") {
603
+ throw new ValidationError(`Invalid view mode "${opts.view}". Must be "list" or "grid".`);
604
+ }
605
+ const noteIds = opts.notes ? opts.notes.split(",").map((id) => id.trim()) : [];
606
+ warnListItemMisuse(opts.description, noteIds.length);
607
+ const data = await api.post(`/api/canvas/${canvasId}`, {
608
+ action: "addListNode",
609
+ description: opts.description,
610
+ noteIds: noteIds.length > 0 ? noteIds : undefined,
611
+ viewMode: opts.view,
612
+ positionX: parseInt(opts.x, 10),
613
+ positionY: parseInt(opts.y, 10),
614
+ });
615
+ if (cfg.json) {
616
+ outputJson(data);
617
+ }
618
+ else {
619
+ const label = opts.description.split(/\n\s*\n/)[0]?.trim().slice(0, 60) ?? "list";
620
+ console.log(`Added "${label}" (${opts.view}) with ${noteIds.length} notes to canvas`);
621
+ }
622
+ }
623
+ catch (error) {
624
+ handleError(error, cfg.json);
625
+ }
626
+ });
627
+ canvas
628
+ .command("add-link")
629
+ .description("Add a link to another canvas")
630
+ .argument("<canvasId>", "Canvas ID")
631
+ .requiredOption("--target <canvasId>", "Target canvas ID to link to")
632
+ .option("--x <n>", "X position", "100")
633
+ .option("--y <n>", "Y position", "100")
634
+ .action(async (canvasId, opts) => {
635
+ const cfg = resolveConfig(program.opts());
636
+ const api = new ApiClient(cfg.serverUrl, cfg.token);
637
+ try {
638
+ const data = await api.post(`/api/canvas/${canvasId}`, {
639
+ action: "addCanvasLink",
640
+ linkedCanvasId: opts.target,
641
+ positionX: parseInt(opts.x, 10),
642
+ positionY: parseInt(opts.y, 10),
643
+ });
644
+ if (cfg.quiet) {
645
+ outputQuiet(String(data.nodeId || data._id || ""));
646
+ return;
647
+ }
648
+ if (cfg.json) {
649
+ outputJson(data);
650
+ }
651
+ else {
652
+ console.log(`Added link to canvas ${opts.target} at (${opts.x}, ${opts.y})`);
653
+ }
654
+ }
655
+ catch (error) {
656
+ handleError(error, cfg.json);
657
+ }
658
+ });
659
+ canvas
660
+ .command("bulk-add")
661
+ .description("Add multiple notes to a canvas in one operation with positions")
662
+ .argument("<canvasId>", "Canvas ID")
663
+ .requiredOption("--notes <json>", "JSON array of {noteId, x, y} objects")
664
+ .addHelpText("after", `
665
+ Examples:
666
+ $ cnotes canvas bulk-add <canvasId> --notes '[{"noteId":"TASK-1","x":100,"y":200},{"noteId":"TASK-2","x":400,"y":200}]'
667
+ `)
668
+ .action(async (canvasId, opts) => {
669
+ const cfg = resolveConfig(program.opts());
670
+ const api = new ApiClient(cfg.serverUrl, cfg.token);
671
+ try {
672
+ let items;
673
+ try {
674
+ items = JSON.parse(opts.notes);
675
+ }
676
+ catch {
677
+ throw new ValidationError("--notes must be valid JSON array, e.g. '[{\"noteId\":\"TASK-1\",\"x\":100,\"y\":200}]'");
678
+ }
679
+ if (!Array.isArray(items) || items.length === 0) {
680
+ throw new ValidationError("--notes must be a non-empty JSON array");
681
+ }
682
+ const notes = items.map((item, i) => {
683
+ const positionX = Number(item.x);
684
+ const positionY = Number(item.y);
685
+ if (isNaN(positionX) || isNaN(positionY)) {
686
+ throw new ValidationError(`Item ${i}: x and y must be numbers`);
687
+ }
688
+ return {
689
+ noteId: String(item.noteId),
690
+ positionX,
691
+ positionY,
692
+ };
693
+ });
694
+ const data = await api.post(`/api/canvas/${canvasId}`, {
695
+ action: "bulkAddNodes",
696
+ notes,
697
+ });
698
+ if (cfg.json) {
699
+ outputJson(data);
700
+ }
701
+ else {
702
+ const results = (data.results || []);
703
+ const added = results.filter((r) => r.status === "added").length;
704
+ const skipped = results.filter((r) => r.status !== "added").length;
705
+ console.log(`Added ${added} notes to canvas${skipped > 0 ? ` (${skipped} skipped/failed)` : ""}`);
706
+ }
707
+ }
708
+ catch (error) {
709
+ handleError(error, cfg.json);
710
+ }
711
+ });
712
+ canvas
713
+ .command("place")
714
+ .description("Place items on a canvas using a declarative layout (no x/y math). The server measures every item, packs them with no overlaps, and inserts them in one batch. Per-item failures are reported in the response (HTTP 207). Prefer this over bulk-add whenever you can express the layout as stacks/grids/anchors.")
715
+ .argument("<canvasId>", "Canvas ID")
716
+ .option("--spec <path>", "Path to a JSON file containing the layout document")
717
+ .option("--spec-stdin", "Read the layout document from stdin")
718
+ .option("--spec-inline <json>", "Pass the layout document inline as a JSON string (short specs only)")
719
+ .option("--dry-run", "Validate the spec and return solved positions without mutating the canvas")
720
+ .option("--placement <mode>", "Where to drop the layout vs existing content: auto (default, lands in a clear band below existing nodes / de-collides), append (always below everything), or exact (honor origin verbatim). Overrides any placement in the spec.")
721
+ .addHelpText("after", `
722
+ Examples:
723
+ $ cnotes canvas place <canvasId> --spec ./layout.json
724
+ $ cnotes canvas place <canvasId> --spec ./layout.json --placement append
725
+ $ echo '{"root":{"type":"stack","items":[...]}}' | cnotes canvas place <canvasId> --spec-stdin
726
+ $ cnotes canvas place <canvasId> --spec ./layout.json --dry-run --json
727
+
728
+ Placement (relative to nodes already on the canvas):
729
+ auto default — lands below existing content when no origin is set, and
730
+ slides the whole block down if it would overlap. You never have to
731
+ guess a free coordinate.
732
+ append always drop just below the lowest existing node.
733
+ exact honor the spec's origin exactly, even if it overlaps (pixel-perfect
734
+ templates only).
735
+ `)
736
+ .action(async (canvasId, opts) => {
737
+ const cfg = resolveConfig(program.opts());
738
+ const api = new ApiClient(cfg.serverUrl, cfg.token);
739
+ try {
740
+ let raw;
741
+ if (opts.spec) {
742
+ raw = readFileSafe(opts.spec);
743
+ }
744
+ else if (opts.specInline) {
745
+ raw = opts.specInline;
746
+ }
747
+ else if (opts.specStdin) {
748
+ raw = await new Promise((resolve, reject) => {
749
+ const chunks = [];
750
+ process.stdin.on("data", (c) => chunks.push(c));
751
+ process.stdin.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
752
+ process.stdin.on("error", reject);
753
+ });
754
+ }
755
+ else {
756
+ throw new ValidationError("One of --spec, --spec-inline, or --spec-stdin is required");
757
+ }
758
+ let doc;
759
+ try {
760
+ doc = JSON.parse(raw);
761
+ }
762
+ catch (e) {
763
+ throw new ValidationError(`Layout spec is not valid JSON: ${e instanceof Error ? e.message : String(e)}`);
764
+ }
765
+ // --placement overrides whatever the spec carries (or sets it if absent).
766
+ if (opts.placement !== undefined) {
767
+ const allowed = ["auto", "append", "exact"];
768
+ if (!allowed.includes(opts.placement)) {
769
+ throw new ValidationError(`--placement must be one of ${allowed.join(", ")} (got "${opts.placement}")`);
770
+ }
771
+ if (doc === null || typeof doc !== "object" || Array.isArray(doc)) {
772
+ throw new ValidationError("Layout spec must be a JSON object to apply --placement");
773
+ }
774
+ doc.placement = opts.placement;
775
+ }
776
+ warnLayoutListMisuse(doc);
777
+ const data = await api.post(`/api/canvas/${encodeURIComponent(canvasId)}?workspaceId=${encodeURIComponent(cfg.workspaceId)}`, {
778
+ action: "placeLayout",
779
+ doc,
780
+ ...(opts.dryRun ? { dryRun: true } : {}),
781
+ });
782
+ if (cfg.json) {
783
+ outputJson(data);
784
+ }
785
+ else {
786
+ const items = (data.items || []);
787
+ const edges = (data.edges || []);
788
+ const bbox = data.bbox;
789
+ if (data.dryRun) {
790
+ console.log(`[dry run] Would place ${items.length} item${items.length === 1 ? "" : "s"} ` +
791
+ `and ${edges.length} edge${edges.length === 1 ? "" : "s"} (no changes made).`);
792
+ if (bbox?.width && bbox?.height) {
793
+ console.log(`Layout bounding box: ${Math.round(bbox.width)} × ${Math.round(bbox.height)}px`);
794
+ }
795
+ return;
796
+ }
797
+ const placed = items.filter((r) => r.id && !r.error).length;
798
+ const failed = items.filter((r) => !r.id || r.error).length;
799
+ const edgesAdded = edges.filter((e) => e.id && !e.error).length;
800
+ const edgesFailed = edges.filter((e) => !e.id || e.error).length;
801
+ console.log(`Placed ${placed} items${failed > 0 ? ` (${failed} failed)` : ""}, ` +
802
+ `${edgesAdded} edges${edgesFailed > 0 ? ` (${edgesFailed} failed)` : ""}`);
803
+ if (bbox?.width && bbox?.height) {
804
+ console.log(`Layout bounding box: ${Math.round(bbox.width)} × ${Math.round(bbox.height)}px`);
805
+ }
806
+ // Surface item-level errors so the user doesn't have to re-run with --json
807
+ for (const item of items) {
808
+ if (item.error)
809
+ console.log(` ✗ item[${item.index}] (${item.type}): ${item.error}`);
810
+ }
811
+ for (const edge of edges) {
812
+ if (edge.error)
813
+ console.log(` ✗ edge ${edge.from} → ${edge.to}: ${edge.error}`);
814
+ }
815
+ }
816
+ }
817
+ catch (error) {
818
+ handleError(error, cfg.json);
819
+ }
820
+ });
821
+ canvas
822
+ .command("bulk-move")
823
+ .description("Move multiple nodes to new positions in one operation")
824
+ .argument("<canvasId>", "Canvas ID (used for routing/auth)")
825
+ .requiredOption("--moves <json>", "JSON array of {nodeId, nodeType, x, y} objects (nodeType: note|text|list|canvas|richtext)")
826
+ .action(async (canvasId, opts) => {
827
+ const cfg = resolveConfig(program.opts());
828
+ const api = new ApiClient(cfg.serverUrl, cfg.token);
829
+ try {
830
+ let items;
831
+ try {
832
+ items = JSON.parse(opts.moves);
833
+ }
834
+ catch {
835
+ throw new ValidationError("--moves must be valid JSON array, e.g. '[{\"nodeId\":\"abc\",\"nodeType\":\"note\",\"x\":100,\"y\":200}]'");
836
+ }
837
+ if (!Array.isArray(items) || items.length === 0) {
838
+ throw new ValidationError("--moves must be a non-empty JSON array");
839
+ }
840
+ const validNodeTypes = new Set(["note", "text", "list", "canvas", "richtext"]);
841
+ const moves = items.map((item, i) => {
842
+ const positionX = Number(item.x);
843
+ const positionY = Number(item.y);
844
+ if (isNaN(positionX) || isNaN(positionY)) {
845
+ throw new ValidationError(`Item ${i}: x and y must be numbers`);
846
+ }
847
+ const nodeType = item.nodeType || "note";
848
+ if (!validNodeTypes.has(nodeType)) {
849
+ throw new ValidationError(`Item ${i}: nodeType must be one of: note, text, list, canvas, richtext`);
850
+ }
851
+ return {
852
+ nodeId: String(item.nodeId),
853
+ nodeType: nodeType,
854
+ positionX,
855
+ positionY,
856
+ };
857
+ });
858
+ const data = await api.post(`/api/canvas/${canvasId}`, {
859
+ action: "bulkMoveNodes",
860
+ moves,
861
+ });
862
+ if (cfg.json) {
863
+ outputJson(data);
864
+ }
865
+ else {
866
+ const count = typeof data.count === "number" ? data.count : moves.length;
867
+ console.log(`Moved ${count} nodes`);
868
+ }
869
+ }
870
+ catch (error) {
871
+ handleError(error, cfg.json);
872
+ }
873
+ });
874
+ canvas
875
+ .command("bulk-remove")
876
+ .description("Remove multiple nodes from a canvas in one operation")
877
+ .argument("<canvasId>", "Canvas ID")
878
+ .requiredOption("--nodes <json>", "JSON array of node ID strings, e.g. '[\"nodeId1\",\"nodeId2\"]'")
879
+ .action(async (canvasId, opts) => {
880
+ const cfg = resolveConfig(program.opts());
881
+ const api = new ApiClient(cfg.serverUrl, cfg.token);
882
+ try {
883
+ let nodeIds;
884
+ try {
885
+ nodeIds = JSON.parse(opts.nodes);
886
+ }
887
+ catch {
888
+ throw new ValidationError("--nodes must be valid JSON array, e.g. '[\"nodeId1\",\"nodeId2\"]'");
889
+ }
890
+ if (!Array.isArray(nodeIds) || nodeIds.length === 0) {
891
+ throw new ValidationError("--nodes must be a non-empty JSON array of strings");
892
+ }
893
+ nodeIds = nodeIds.map(String);
894
+ const data = await api.post(`/api/canvas/${canvasId}`, {
895
+ action: "bulkRemoveNodes",
896
+ nodeIds,
897
+ });
898
+ if (cfg.json) {
899
+ outputJson(data);
900
+ }
901
+ else {
902
+ const results = (data.results || []);
903
+ const deleted = results.filter((r) => r.status === "deleted").length;
904
+ const failed = results.filter((r) => r.status !== "deleted").length;
905
+ console.log(`Removed ${deleted} nodes from canvas${failed > 0 ? ` (${failed} failed)` : ""}`);
906
+ }
907
+ }
908
+ catch (error) {
909
+ handleError(error, cfg.json);
910
+ }
911
+ });
912
+ canvas
913
+ .command("add-edge")
914
+ .description("Connect two nodes on a canvas")
915
+ .argument("<canvasId>", "Canvas ID")
916
+ .requiredOption("--source <nodeId>", "Source node ID")
917
+ .requiredOption("--target <nodeId>", "Target node ID")
918
+ .option("--label <text>", "Edge label")
919
+ .option("--arrow <mode>", "Arrow direction: end (default), start, both, none", "end")
920
+ .option("--line-style <style>", "Line style: solid (default) | dashed", "solid")
921
+ .action(async (canvasId, opts) => {
922
+ const cfg = resolveConfig(program.opts());
923
+ const api = new ApiClient(cfg.serverUrl, cfg.token);
924
+ try {
925
+ const { arrowStart, arrowEnd } = parseArrowMode(opts.arrow);
926
+ const lineStyle = parseLineStyle(opts.lineStyle);
927
+ const data = await api.post(`/api/canvas/${canvasId}`, {
928
+ action: "addEdge",
929
+ sourceNodeId: opts.source,
930
+ targetNodeId: opts.target,
931
+ label: opts.label,
932
+ arrowStart,
933
+ arrowEnd,
934
+ lineStyle,
935
+ });
936
+ if (cfg.json) {
937
+ outputJson(data);
938
+ }
939
+ else {
940
+ const label = opts.label ? ` [${opts.label}]` : "";
941
+ console.log(`Connected ${opts.source} -> ${opts.target}${label}`);
942
+ }
943
+ }
944
+ catch (error) {
945
+ handleError(error, cfg.json);
946
+ }
947
+ });
948
+ canvas
949
+ .command("update-edge")
950
+ .description("Update an edge's label, arrows, line style, or direction")
951
+ .argument("<canvasId>", "Canvas ID")
952
+ .requiredOption("--edge <edgeId>", "Edge ID")
953
+ .option("--label <text>", "Set a new label (use --clear-label to remove)")
954
+ .option("--clear-label", "Remove the edge label")
955
+ .option("--arrow <mode>", "Arrow direction: end | start | both | none")
956
+ .option("--line-style <style>", "Line style: solid | dashed")
957
+ .option("--reverse", "Swap source and target nodes")
958
+ .action(async (canvasId, opts) => {
959
+ const cfg = resolveConfig(program.opts());
960
+ const api = new ApiClient(cfg.serverUrl, cfg.token);
961
+ try {
962
+ const body = {
963
+ action: "updateEdge",
964
+ edgeId: opts.edge,
965
+ };
966
+ if (opts.clearLabel) {
967
+ body.label = null;
968
+ }
969
+ else if (typeof opts.label === "string") {
970
+ body.label = opts.label;
971
+ }
972
+ if (opts.arrow !== undefined) {
973
+ const { arrowStart, arrowEnd } = parseArrowMode(opts.arrow);
974
+ body.arrowStart = arrowStart;
975
+ body.arrowEnd = arrowEnd;
976
+ }
977
+ if (opts.lineStyle !== undefined) {
978
+ body.lineStyle = parseLineStyle(opts.lineStyle);
979
+ }
980
+ if (opts.reverse)
981
+ body.reverse = true;
982
+ const data = await api.post(`/api/canvas/${canvasId}`, body);
983
+ if (cfg.json) {
984
+ outputJson(data);
985
+ }
986
+ else {
987
+ console.log(`Updated edge ${opts.edge}`);
988
+ }
989
+ }
990
+ catch (error) {
991
+ handleError(error, cfg.json);
992
+ }
993
+ });
994
+ canvas
995
+ .command("move-node")
996
+ .description("Move a node to a new position on a canvas")
997
+ .argument("<canvasId>", "Canvas ID")
998
+ .requiredOption("--node <nodeId>", "Canvas node ID")
999
+ .requiredOption("--x <n>", "X position")
1000
+ .requiredOption("--y <n>", "Y position")
1001
+ .action(async (canvasId, opts) => {
1002
+ const cfg = resolveConfig(program.opts());
1003
+ const api = new ApiClient(cfg.serverUrl, cfg.token);
1004
+ try {
1005
+ const positionX = Number(opts.x);
1006
+ const positionY = Number(opts.y);
1007
+ await api.post(`/api/canvas/${canvasId}`, {
1008
+ action: "moveNode",
1009
+ nodeId: opts.node,
1010
+ positionX,
1011
+ positionY,
1012
+ });
1013
+ if (cfg.json) {
1014
+ outputJson({ success: true, nodeId: opts.node, positionX, positionY });
1015
+ }
1016
+ else {
1017
+ console.log(`Moved node ${opts.node} to (${positionX}, ${positionY})`);
1018
+ }
1019
+ }
1020
+ catch (error) {
1021
+ handleError(error, cfg.json);
1022
+ }
1023
+ });
1024
+ canvas
1025
+ .command("remove-node")
1026
+ .description("Remove a node from a canvas")
1027
+ .argument("<canvasId>", "Canvas ID")
1028
+ .requiredOption("--node <nodeId>", "Canvas node ID")
1029
+ .action(async (canvasId, opts) => {
1030
+ const cfg = resolveConfig(program.opts());
1031
+ const api = new ApiClient(cfg.serverUrl, cfg.token);
1032
+ try {
1033
+ await api.post(`/api/canvas/${canvasId}`, {
1034
+ action: "removeNode",
1035
+ nodeId: opts.node,
1036
+ });
1037
+ if (cfg.json) {
1038
+ outputJson({ success: true, nodeId: opts.node });
1039
+ }
1040
+ else {
1041
+ console.log(`Removed node ${opts.node} from canvas`);
1042
+ }
1043
+ }
1044
+ catch (error) {
1045
+ handleError(error, cfg.json);
1046
+ }
1047
+ });
1048
+ canvas
1049
+ .command("remove-edge")
1050
+ .description("Remove an edge from a canvas")
1051
+ .argument("<canvasId>", "Canvas ID")
1052
+ .requiredOption("--edge <edgeId>", "Canvas edge ID")
1053
+ .action(async (canvasId, opts) => {
1054
+ const cfg = resolveConfig(program.opts());
1055
+ const api = new ApiClient(cfg.serverUrl, cfg.token);
1056
+ try {
1057
+ await api.post(`/api/canvas/${canvasId}`, {
1058
+ action: "removeEdge",
1059
+ edgeId: opts.edge,
1060
+ });
1061
+ if (cfg.json) {
1062
+ outputJson({ success: true, edgeId: opts.edge });
1063
+ }
1064
+ else {
1065
+ console.log(`Removed edge ${opts.edge} from canvas`);
1066
+ }
1067
+ }
1068
+ catch (error) {
1069
+ handleError(error, cfg.json);
1070
+ }
1071
+ });
1072
+ // ---- Template commands ----
1073
+ canvas
1074
+ .command("templates")
1075
+ .description("List available canvas templates")
1076
+ .action(async () => {
1077
+ const cfg = resolveConfig(program.opts());
1078
+ const api = new ApiClient(cfg.serverUrl, cfg.token);
1079
+ try {
1080
+ const data = await api.get("/api/canvas/template");
1081
+ const templates = data.templates || [];
1082
+ if (cfg.json) {
1083
+ outputJson(templates);
1084
+ return;
1085
+ }
1086
+ if (templates.length === 0) {
1087
+ console.log("No templates available.");
1088
+ return;
1089
+ }
1090
+ const rows = templates.map((t) => [
1091
+ String(t.id || ""),
1092
+ String(t.name || ""),
1093
+ truncate(String(t.description || ""), 40),
1094
+ String(t.category || ""),
1095
+ String(t.zoneCount || 0),
1096
+ ]);
1097
+ outputTable(["ID", "NAME", "DESCRIPTION", "CATEGORY", "ZONES"], rows);
1098
+ }
1099
+ catch (error) {
1100
+ handleError(error, cfg.json);
1101
+ }
1102
+ });
1103
+ canvas
1104
+ .command("from-template")
1105
+ .description("Create a canvas from a predefined template")
1106
+ .argument("<templateId>", "Template ID (use 'cnotes canvas templates' to list)")
1107
+ .option("--title <title>", "Custom canvas title")
1108
+ .option("--density <mode>", "Layout density: tight, medium, or spacious")
1109
+ .option("--populate", "Auto-populate zones with matching notes from workspace")
1110
+ .addHelpText("after", `
1111
+ Examples:
1112
+ $ cnotes canvas templates
1113
+ $ cnotes canvas from-template retro --title "Q4 Retro" --populate
1114
+ `)
1115
+ .action(async (templateId, opts) => {
1116
+ const cfg = resolveConfig(program.opts());
1117
+ const api = new ApiClient(cfg.serverUrl, cfg.token);
1118
+ try {
1119
+ // Validate template exists by fetching template list
1120
+ const templateData = await api.get("/api/canvas/template");
1121
+ const templates = templateData.templates || [];
1122
+ const template = templates.find((t) => t.id === templateId);
1123
+ if (!template) {
1124
+ const available = templates.map((t) => t.id).join(", ");
1125
+ throw new ValidationError(`Unknown template "${templateId}". Available: ${available}`);
1126
+ }
1127
+ // Validate density (must match LAYOUT_DENSITIES in lib/canvas/templates/types.ts)
1128
+ const validDensities = ["tight", "medium", "spacious"];
1129
+ if (opts.density && !validDensities.includes(opts.density)) {
1130
+ throw new ValidationError(`Invalid density "${opts.density}". Must be: ${validDensities.join(", ")}`);
1131
+ }
1132
+ // If --populate, search notes for each zone
1133
+ let zonePopulations;
1134
+ if (opts.populate) {
1135
+ zonePopulations = [];
1136
+ const zones = template.zones;
1137
+ if (zones) {
1138
+ for (const zone of zones) {
1139
+ const hint = zone.populationHint;
1140
+ if (!hint || hint.startsEmpty)
1141
+ continue;
1142
+ const searchQuery = hint.searchQuery;
1143
+ const noteTypes = hint.noteTypes;
1144
+ const maxNotes = hint.maxNotes || 8;
1145
+ // Search for matching notes
1146
+ const searchParams = {
1147
+ workspaceId: cfg.workspaceId,
1148
+ limit: String(maxNotes),
1149
+ };
1150
+ if (searchQuery)
1151
+ searchParams.search = searchQuery;
1152
+ if (noteTypes && noteTypes.length > 0)
1153
+ searchParams.types = noteTypes.join(",");
1154
+ try {
1155
+ const searchResult = await api.get("/api/notes", searchParams);
1156
+ const notes = searchResult.notes || [];
1157
+ if (notes.length > 0) {
1158
+ zonePopulations.push({
1159
+ zoneId: String(zone.id),
1160
+ noteIds: notes.map((n) => String(n.displayId || n.id || n._id)),
1161
+ });
1162
+ }
1163
+ }
1164
+ catch {
1165
+ // Search failed for this zone, continue without population
1166
+ }
1167
+ }
1168
+ }
1169
+ }
1170
+ // Create canvas from template
1171
+ const data = await api.post("/api/canvas/template", {
1172
+ workspaceId: cfg.workspaceId,
1173
+ templateId,
1174
+ title: opts.title,
1175
+ density: opts.density,
1176
+ zonePopulations,
1177
+ });
1178
+ if (cfg.quiet) {
1179
+ outputQuiet(String(data.canvasId || ""));
1180
+ return;
1181
+ }
1182
+ if (cfg.json) {
1183
+ outputJson(data);
1184
+ }
1185
+ else {
1186
+ const name = opts.title || template.name;
1187
+ console.log(`Created canvas from template "${templateId}": ${name}`);
1188
+ console.log(`Canvas ID: ${data.canvasId}`);
1189
+ console.log(`Zones: ${data.zoneCount}, Notes: ${data.noteCount}`);
1190
+ }
1191
+ }
1192
+ catch (error) {
1193
+ handleError(error, cfg.json);
1194
+ }
1195
+ });
1196
+ // ============================================
1197
+ // AGENT-RUN LIFECYCLE
1198
+ // ============================================
1199
+ //
1200
+ // Bracket a sequence of canvas commands with auto-checkpoints so the
1201
+ // server treats them as a single agent operation. The pattern:
1202
+ //
1203
+ // eval "$(npx cnotes canvas agent-run begin --canvas <id> --prompt "Reorganize")"
1204
+ // npx cnotes canvas add-node ...
1205
+ // npx cnotes canvas add-edge ...
1206
+ // npx cnotes canvas agent-run end --canvas <id>
1207
+ //
1208
+ // `begin` prints `export CN_AGENT_RUN_ID=...` for shell eval. Subsequent
1209
+ // commands pick up the env var and the api-client adds it as a header,
1210
+ // so all mutations between begin and end share one operation row + a
1211
+ // before/after snapshot pair, recoverable later via "Revert this run".
1212
+ const agentRun = canvas
1213
+ .command("agent-run")
1214
+ .description("Bracket canvas commands with before/after auto-checkpoints");
1215
+ agentRun
1216
+ .command("begin")
1217
+ .description("Open an agent-run session — captures a before-snapshot and prints CN_AGENT_RUN_ID")
1218
+ .requiredOption("--canvas <id>", "Canvas ID to bracket")
1219
+ .requiredOption("--prompt <text>", "User prompt or summary describing the run")
1220
+ .option("--rationale <text>", "Optional rationale (e.g. agent's plan)")
1221
+ .action(async (opts) => {
1222
+ const cfg = resolveConfig(program.opts());
1223
+ const api = new ApiClient(cfg.serverUrl, cfg.token);
1224
+ // This command is designed to be used via `eval "$(...)"`, which makes
1225
+ // stdout a non-TTY. resolveConfig's default would then flip cfg.json on
1226
+ // and we'd emit JSON instead of shell exports — breaking the eval form.
1227
+ // Honour JSON only when the user asked for it explicitly.
1228
+ const wantsJson = program.opts().json === true || cnotesEnv("JSON") === "1";
1229
+ try {
1230
+ const body = {
1231
+ action: "agentRunBegin",
1232
+ prompt: opts.prompt,
1233
+ };
1234
+ if (opts.rationale)
1235
+ body.rationale = opts.rationale;
1236
+ const agentSession = resolveAgentSession();
1237
+ if (agentSession)
1238
+ body.agentSession = agentSession;
1239
+ const data = await api.post(`/api/canvas/${encodeURIComponent(opts.canvas)}`, body);
1240
+ if (cfg.quiet) {
1241
+ outputQuiet(data.agentRunId);
1242
+ return;
1243
+ }
1244
+ if (wantsJson) {
1245
+ outputJson(data);
1246
+ return;
1247
+ }
1248
+ // Print eval-friendly export so users can:
1249
+ // eval "$(npx cnotes canvas agent-run begin ...)"
1250
+ // Only the runId + before-snapshot id need to ride along on
1251
+ // each request — prompt + rationale were already persisted on
1252
+ // the operation row at agentRunBegin time.
1253
+ console.log(`export CN_AGENT_RUN_ID=${data.agentRunId}`);
1254
+ console.log(`export CN_AGENT_BEFORE_SNAPSHOT_ID=${data.beforeCheckpointId}`);
1255
+ console.error(`# agent run started: v${data.beforeSnapshotNumber} (${data.agentRunId})`);
1256
+ console.error(`# end with: npx cnotes canvas agent-run end --canvas ${opts.canvas}`);
1257
+ }
1258
+ catch (error) {
1259
+ handleError(error, wantsJson);
1260
+ }
1261
+ });
1262
+ agentRun
1263
+ .command("wrap")
1264
+ .description("Run a shell command inside an agent-run scope (auto begin + end). Recommended for AI agents — no eval gymnastics, no env-var leakage, auto-closes the run even on subprocess failure.")
1265
+ .requiredOption("--canvas <id>", "Canvas ID")
1266
+ .requiredOption("--prompt <text>", "Prompt describing the run")
1267
+ .option("--rationale <text>", "Optional rationale (e.g. agent's plan)")
1268
+ .argument("<command...>", "Command to execute (e.g. -- bash -c '...')")
1269
+ .addHelpText("after", `
1270
+ Examples:
1271
+ $ cnotes canvas agent-run wrap --canvas <id> --prompt "Reorganize" -- bash -c 'cnotes canvas add-node <id> --note A-1'
1272
+ $ cnotes canvas agent-run wrap --canvas <id> --prompt "Lay out retro" -- cnotes canvas place <id> --spec ./layout.json
1273
+ `)
1274
+ .action(async (command, opts) => {
1275
+ const cfg = resolveConfig(program.opts());
1276
+ const api = new ApiClient(cfg.serverUrl, cfg.token);
1277
+ if (command.length === 0) {
1278
+ throw new ValidationError('No command to run. Usage:\n cnotes canvas agent-run wrap --canvas <id> --prompt "..." -- <command>');
1279
+ }
1280
+ // Step 1: open the run
1281
+ const agentSession = resolveAgentSession();
1282
+ const begin = await api.post(`/api/canvas/${encodeURIComponent(opts.canvas)}`, {
1283
+ action: "agentRunBegin",
1284
+ prompt: opts.prompt,
1285
+ ...(opts.rationale ? { rationale: opts.rationale } : {}),
1286
+ ...(agentSession ? { agentSession } : {}),
1287
+ });
1288
+ console.error(`# agent run started: v${begin.beforeSnapshotNumber} (${begin.agentRunId})`);
1289
+ // Step 2: spawn the wrapped command with the run env var set.
1290
+ // stdio is inherited so the subprocess prints to the user's terminal
1291
+ // exactly as if they'd run it themselves. Only runId + before-snapshot
1292
+ // need to propagate — prompt + rationale are persisted on the
1293
+ // operation row at agentRunBegin time, so resending them per-request
1294
+ // would be redundant (and would add a spoofing surface).
1295
+ const { spawn } = await import("node:child_process");
1296
+ const child = spawn(command[0], command.slice(1), {
1297
+ stdio: "inherit",
1298
+ env: {
1299
+ ...process.env,
1300
+ CN_AGENT_RUN_ID: begin.agentRunId,
1301
+ CN_AGENT_BEFORE_SNAPSHOT_ID: begin.beforeCheckpointId,
1302
+ },
1303
+ });
1304
+ const exitCode = await new Promise((resolve) => {
1305
+ child.on("exit", (code, signal) => {
1306
+ if (signal) {
1307
+ console.error(`# subprocess killed by signal ${signal}`);
1308
+ resolve(128);
1309
+ return;
1310
+ }
1311
+ resolve(code ?? 1);
1312
+ });
1313
+ child.on("error", (err) => {
1314
+ console.error(`# subprocess failed to start: ${err.message}`);
1315
+ resolve(1);
1316
+ });
1317
+ });
1318
+ // Step 3: ALWAYS close the run, even if the subprocess failed.
1319
+ // The after-checkpoint captures whatever state was reached.
1320
+ try {
1321
+ const end = await api.post(`/api/canvas/${encodeURIComponent(opts.canvas)}`, {
1322
+ action: "agentRunEnd",
1323
+ agentRunId: begin.agentRunId,
1324
+ });
1325
+ const status = exitCode === 0 ? "completed" : `exited ${exitCode}`;
1326
+ console.error(`# agent run ${status}: v${end.afterSnapshotNumber} (${begin.agentRunId})`);
1327
+ if (cfg.json) {
1328
+ outputJson({
1329
+ agentRunId: begin.agentRunId,
1330
+ beforeCheckpointId: begin.beforeCheckpointId,
1331
+ beforeSnapshotNumber: begin.beforeSnapshotNumber,
1332
+ afterCheckpointId: end.afterCheckpointId,
1333
+ afterSnapshotNumber: end.afterSnapshotNumber,
1334
+ exitCode,
1335
+ });
1336
+ }
1337
+ }
1338
+ catch (err) {
1339
+ console.error(`# WARNING: agent run did not close cleanly:`, err instanceof Error ? err.message : err);
1340
+ console.error(`# you can close it manually: cnotes canvas agent-run end --canvas ${opts.canvas} --run-id ${begin.agentRunId}`);
1341
+ }
1342
+ process.exit(exitCode);
1343
+ });
1344
+ agentRun
1345
+ .command("end")
1346
+ .description("Close an agent-run session — captures an after-snapshot and prints unset commands")
1347
+ .requiredOption("--canvas <id>", "Canvas ID to close")
1348
+ .option("--run-id <id>", "Agent run ID (defaults to CN_AGENT_RUN_ID env var)")
1349
+ .action(async (opts) => {
1350
+ const cfg = resolveConfig(program.opts());
1351
+ const api = new ApiClient(cfg.serverUrl, cfg.token);
1352
+ const runId = opts.runId ?? cnotesEnv("AGENT_RUN_ID");
1353
+ if (!runId) {
1354
+ throw new ValidationError("No agent run ID found. Pass --run-id or set CNOTES_AGENT_RUN_ID.");
1355
+ }
1356
+ // Same reasoning as `begin`: eval form makes stdout non-TTY, so don't
1357
+ // let the auto-detect flip us into JSON mode.
1358
+ const wantsJson = program.opts().json === true || cnotesEnv("JSON") === "1";
1359
+ try {
1360
+ const data = await api.post(`/api/canvas/${encodeURIComponent(opts.canvas)}`, {
1361
+ action: "agentRunEnd",
1362
+ agentRunId: runId,
1363
+ });
1364
+ if (cfg.quiet) {
1365
+ outputQuiet(String(data.afterSnapshotNumber));
1366
+ return;
1367
+ }
1368
+ if (wantsJson) {
1369
+ outputJson(data);
1370
+ return;
1371
+ }
1372
+ // Tell the user how to clear the env vars; eval-safe so users
1373
+ // running `eval "$(cnotes canvas agent-run end ...)"` get them
1374
+ // unset automatically.
1375
+ console.log(`unset CN_AGENT_RUN_ID CN_AGENT_BEFORE_SNAPSHOT_ID`);
1376
+ console.error(`# agent run closed: v${data.afterSnapshotNumber} (${runId})`);
1377
+ }
1378
+ catch (error) {
1379
+ handleError(error, wantsJson);
1380
+ }
1381
+ });
1382
+ }
1383
+ //# sourceMappingURL=canvas.js.map