@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.
- package/.claude-plugin/plugin.json +14 -0
- package/.mcp.json +12 -0
- package/LICENSE +21 -0
- package/README.md +303 -0
- package/dist/cn.d.ts +3 -0
- package/dist/cn.d.ts.map +1 -0
- package/dist/cn.js +124 -0
- package/dist/cn.js.map +1 -0
- package/dist/commands/auth.d.ts +10 -0
- package/dist/commands/auth.d.ts.map +1 -0
- package/dist/commands/auth.js +188 -0
- package/dist/commands/auth.js.map +1 -0
- package/dist/commands/canvas.d.ts +3 -0
- package/dist/commands/canvas.d.ts.map +1 -0
- package/dist/commands/canvas.js +1383 -0
- package/dist/commands/canvas.js.map +1 -0
- package/dist/commands/claude-hook.d.ts +28 -0
- package/dist/commands/claude-hook.d.ts.map +1 -0
- package/dist/commands/claude-hook.js +59 -0
- package/dist/commands/claude-hook.js.map +1 -0
- package/dist/commands/config.d.ts +3 -0
- package/dist/commands/config.d.ts.map +1 -0
- package/dist/commands/config.js +47 -0
- package/dist/commands/config.js.map +1 -0
- package/dist/commands/files.d.ts +3 -0
- package/dist/commands/files.d.ts.map +1 -0
- package/dist/commands/files.js +119 -0
- package/dist/commands/files.js.map +1 -0
- package/dist/commands/init.d.ts +3 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +473 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/mcp.d.ts +15 -0
- package/dist/commands/mcp.d.ts.map +1 -0
- package/dist/commands/mcp.js +118 -0
- package/dist/commands/mcp.js.map +1 -0
- package/dist/commands/memory.d.ts +3 -0
- package/dist/commands/memory.d.ts.map +1 -0
- package/dist/commands/memory.js +150 -0
- package/dist/commands/memory.js.map +1 -0
- package/dist/commands/notes.d.ts +3 -0
- package/dist/commands/notes.d.ts.map +1 -0
- package/dist/commands/notes.js +706 -0
- package/dist/commands/notes.js.map +1 -0
- package/dist/commands/operations.d.ts +18 -0
- package/dist/commands/operations.d.ts.map +1 -0
- package/dist/commands/operations.js +231 -0
- package/dist/commands/operations.js.map +1 -0
- package/dist/commands/relationships.d.ts +3 -0
- package/dist/commands/relationships.d.ts.map +1 -0
- package/dist/commands/relationships.js +94 -0
- package/dist/commands/relationships.js.map +1 -0
- package/dist/commands/schema.d.ts +12 -0
- package/dist/commands/schema.d.ts.map +1 -0
- package/dist/commands/schema.js +85 -0
- package/dist/commands/schema.js.map +1 -0
- package/dist/commands/search.d.ts +3 -0
- package/dist/commands/search.d.ts.map +1 -0
- package/dist/commands/search.js +57 -0
- package/dist/commands/search.js.map +1 -0
- package/dist/commands/theme.d.ts +3 -0
- package/dist/commands/theme.d.ts.map +1 -0
- package/dist/commands/theme.js +184 -0
- package/dist/commands/theme.js.map +1 -0
- package/dist/commands/timeline.d.ts +3 -0
- package/dist/commands/timeline.d.ts.map +1 -0
- package/dist/commands/timeline.js +97 -0
- package/dist/commands/timeline.js.map +1 -0
- package/dist/commands/types.d.ts +3 -0
- package/dist/commands/types.d.ts.map +1 -0
- package/dist/commands/types.js +139 -0
- package/dist/commands/types.js.map +1 -0
- package/dist/commands/versions.d.ts +3 -0
- package/dist/commands/versions.d.ts.map +1 -0
- package/dist/commands/versions.js +120 -0
- package/dist/commands/versions.js.map +1 -0
- package/dist/commands/workspace.d.ts +13 -0
- package/dist/commands/workspace.d.ts.map +1 -0
- package/dist/commands/workspace.js +176 -0
- package/dist/commands/workspace.js.map +1 -0
- package/dist/lib/api-client.d.ts +45 -0
- package/dist/lib/api-client.d.ts.map +1 -0
- package/dist/lib/api-client.js +198 -0
- package/dist/lib/api-client.js.map +1 -0
- package/dist/lib/auth-store.d.ts +47 -0
- package/dist/lib/auth-store.d.ts.map +1 -0
- package/dist/lib/auth-store.js +116 -0
- package/dist/lib/auth-store.js.map +1 -0
- package/dist/lib/brand.d.ts +32 -0
- package/dist/lib/brand.d.ts.map +1 -0
- package/dist/lib/brand.js +32 -0
- package/dist/lib/brand.js.map +1 -0
- package/dist/lib/build-schema.d.ts +97 -0
- package/dist/lib/build-schema.d.ts.map +1 -0
- package/dist/lib/build-schema.js +139 -0
- package/dist/lib/build-schema.js.map +1 -0
- package/dist/lib/canvas-read.d.ts +54 -0
- package/dist/lib/canvas-read.d.ts.map +1 -0
- package/dist/lib/canvas-read.js +145 -0
- package/dist/lib/canvas-read.js.map +1 -0
- package/dist/lib/claude-session.d.ts +73 -0
- package/dist/lib/claude-session.d.ts.map +1 -0
- package/dist/lib/claude-session.js +104 -0
- package/dist/lib/claude-session.js.map +1 -0
- package/dist/lib/config.d.ts +28 -0
- package/dist/lib/config.d.ts.map +1 -0
- package/dist/lib/config.js +66 -0
- package/dist/lib/config.js.map +1 -0
- package/dist/lib/env.d.ts +14 -0
- package/dist/lib/env.d.ts.map +1 -0
- package/dist/lib/env.js +16 -0
- package/dist/lib/env.js.map +1 -0
- package/dist/lib/errors.d.ts +47 -0
- package/dist/lib/errors.d.ts.map +1 -0
- package/dist/lib/errors.js +194 -0
- package/dist/lib/errors.js.map +1 -0
- package/dist/lib/fs-utils.d.ts +2 -0
- package/dist/lib/fs-utils.d.ts.map +1 -0
- package/dist/lib/fs-utils.js +16 -0
- package/dist/lib/fs-utils.js.map +1 -0
- package/dist/lib/install-hook.d.ts +86 -0
- package/dist/lib/install-hook.d.ts.map +1 -0
- package/dist/lib/install-hook.js +168 -0
- package/dist/lib/install-hook.js.map +1 -0
- package/dist/lib/install-mcp.d.ts +21 -0
- package/dist/lib/install-mcp.d.ts.map +1 -0
- package/dist/lib/install-mcp.js +133 -0
- package/dist/lib/install-mcp.js.map +1 -0
- package/dist/lib/install-skill.d.ts +49 -0
- package/dist/lib/install-skill.d.ts.map +1 -0
- package/dist/lib/install-skill.js +113 -0
- package/dist/lib/install-skill.js.map +1 -0
- package/dist/lib/output.d.ts +29 -0
- package/dist/lib/output.d.ts.map +1 -0
- package/dist/lib/output.js +78 -0
- package/dist/lib/output.js.map +1 -0
- package/dist/lib/resolve-note.d.ts +7 -0
- package/dist/lib/resolve-note.d.ts.map +1 -0
- package/dist/lib/resolve-note.js +23 -0
- package/dist/lib/resolve-note.js.map +1 -0
- package/dist/lib/stdin.d.ts +5 -0
- package/dist/lib/stdin.d.ts.map +1 -0
- package/dist/lib/stdin.js +11 -0
- package/dist/lib/stdin.js.map +1 -0
- package/dist/lib/style.d.ts +10 -0
- package/dist/lib/style.d.ts.map +1 -0
- package/dist/lib/style.js +17 -0
- package/dist/lib/style.js.map +1 -0
- package/dist/lib/themes.d.ts +44 -0
- package/dist/lib/themes.d.ts.map +1 -0
- package/dist/lib/themes.js +168 -0
- package/dist/lib/themes.js.map +1 -0
- package/dist/mcp-server.d.ts +3 -0
- package/dist/mcp-server.d.ts.map +1 -0
- package/dist/mcp-server.js +782 -0
- package/dist/mcp-server.js.map +1 -0
- package/package.json +66 -0
- 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
|