@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,706 @@
1
+ import { select } from "@inquirer/prompts";
2
+ import { ApiClient } from "../lib/api-client.js";
3
+ import { resolveConfig } from "../lib/config.js";
4
+ import { handleError, NotFoundError, ValidationError } from "../lib/errors.js";
5
+ import { readFileSafe } from "../lib/fs-utils.js";
6
+ import { outputJson, outputTable, outputQuiet, timeAgo, truncate } from "../lib/output.js";
7
+ import { resolveNoteId } from "../lib/resolve-note.js";
8
+ export function registerNotesCommands(program) {
9
+ const notes = program
10
+ .command("notes")
11
+ .alias("n")
12
+ .description("Manage notes");
13
+ notes
14
+ .command("list")
15
+ .description("List notes in the current workspace")
16
+ .option("--search <query>", "Full-text search")
17
+ .option("--type <type>", "Filter by note type")
18
+ .option("--tags <tags>", "Filter by tags (comma-separated)")
19
+ .option("--pinned", "Only show pinned notes")
20
+ .option("--limit <n>", "Maximum number of results", "20")
21
+ .addHelpText("after", `
22
+ Examples:
23
+ $ cnotes notes list --type Insight --limit 5
24
+ $ cnotes notes list --search "auth flow" --json
25
+ $ cnotes notes list --fields displayId,title,type --json
26
+ `)
27
+ .action(async (opts) => {
28
+ const cfg = resolveConfig(program.opts());
29
+ const api = new ApiClient(cfg.serverUrl, cfg.token);
30
+ try {
31
+ const params = {
32
+ workspaceId: cfg.workspaceId,
33
+ search: opts.search,
34
+ types: opts.type,
35
+ tags: opts.tags,
36
+ onlyPinned: opts.pinned ? "true" : undefined,
37
+ excludeDrafts: "true",
38
+ excludeContent: "true",
39
+ };
40
+ const notesList = await api.get("/api/notes", params);
41
+ // The server caps results (and ignores `limit` when a search/type
42
+ // filter is active), so apply the requested limit here. This also
43
+ // fixes the JSON path, which previously emitted the entire result
44
+ // set regardless of --limit.
45
+ const limit = parseInt(opts.limit, 10);
46
+ const all = Array.isArray(notesList) ? notesList : [];
47
+ if (cfg.json) {
48
+ const shown = all.slice(0, limit);
49
+ if (all.length > shown.length) {
50
+ // stderr keeps stdout valid JSON while still signalling to the
51
+ // caller (often an agent) that the list was truncated.
52
+ console.error(`Showing ${shown.length} of ${all.length} — raise --limit to see more.`);
53
+ }
54
+ outputJson(shown, cfg.fields);
55
+ return;
56
+ }
57
+ await displayNotesList(all, limit, cfg, api, "notes found — select to view:");
58
+ }
59
+ catch (error) {
60
+ handleError(error, cfg.json);
61
+ }
62
+ });
63
+ notes
64
+ .command("get")
65
+ .description("Get one or more notes by display ID. Always returns an array, in input order, " +
66
+ "in a single round-trip. Pass one ID for one note, or many IDs to batch-fetch.")
67
+ .argument("<ids...>", "One or more display IDs (e.g. MEETING-12 PRD-3 IDEA-7)")
68
+ .option("--all-versions", "Include all versions")
69
+ .option("--content-only", "Print only the raw markdown content (no metadata, no JSON envelope). " +
70
+ "Best for reading large notes/transcripts without a giant clipped JSON line.")
71
+ .addHelpText("after", `
72
+ Examples:
73
+ $ cnotes notes get MEETING-12
74
+ $ cnotes notes get MEETING-12 PRD-3 IDEA-7 --json
75
+ $ cnotes notes get TRANSCRIPT-1 --content-only # raw markdown, no JSON to parse
76
+ `)
77
+ .action(async (ids, opts) => {
78
+ const cfg = resolveConfig(program.opts());
79
+ const api = new ApiClient(cfg.serverUrl, cfg.token);
80
+ try {
81
+ const notesList = await api.get(`/api/notes`, {
82
+ workspaceId: cfg.workspaceId,
83
+ includeVersions: opts.allVersions ? "true" : undefined,
84
+ exact_ids: ids.join(","),
85
+ format: "markdown",
86
+ });
87
+ const found = Array.isArray(notesList) ? notesList : [];
88
+ const byDisplayId = new Map();
89
+ for (const n of found) {
90
+ if (typeof n.displayId === "string")
91
+ byDisplayId.set(n.displayId, n);
92
+ }
93
+ const missing = ids.filter((id) => !byDisplayId.has(id));
94
+ if (missing.length > 0) {
95
+ throw new NotFoundError(`Notes not found: ${missing.join(", ")}`, "Run `cnotes notes list` to see valid display IDs in this workspace.");
96
+ }
97
+ // Preserve input order so callers can index by position.
98
+ const ordered = ids.map((id) => byDisplayId.get(id));
99
+ if (opts.contentOnly) {
100
+ // Raw markdown only — no JSON envelope, no chrome. When several notes
101
+ // are requested, delimit each with an HTML comment (valid markdown,
102
+ // renders to nothing) so the stream stays parseable.
103
+ const parts = ordered.map((note) => {
104
+ // Mirror printNote's resolution: the API populates top-level
105
+ // `content`, but fall back to latestVersion in case a shape only
106
+ // carries it there.
107
+ const raw = note.content ?? note.latestVersion?.content;
108
+ const content = typeof raw === "string" ? raw : "";
109
+ return ordered.length > 1
110
+ ? `<!-- ${String(note.displayId)} -->\n${content}`
111
+ : content;
112
+ });
113
+ process.stdout.write(parts.join("\n\n") + "\n");
114
+ }
115
+ else if (cfg.json) {
116
+ outputJson(ordered, cfg.fields);
117
+ }
118
+ else {
119
+ ordered.forEach((note) => printNote(note));
120
+ }
121
+ }
122
+ catch (error) {
123
+ handleError(error, cfg.json);
124
+ }
125
+ });
126
+ notes
127
+ .command("delete")
128
+ .description("Delete (archive) a note")
129
+ .argument("<id>", "Note display ID or Convex ID")
130
+ .addHelpText("after", `
131
+ Examples:
132
+ $ cnotes notes delete MEETING-12
133
+ `)
134
+ .action(async (id) => {
135
+ const cfg = resolveConfig(program.opts());
136
+ const api = new ApiClient(cfg.serverUrl, cfg.token);
137
+ try {
138
+ const noteId = await resolveNoteId(api, cfg.workspaceId, id);
139
+ const data = await api.delete(`/api/notes/${noteId}`);
140
+ if (cfg.json) {
141
+ outputJson(data);
142
+ }
143
+ else {
144
+ console.log(`Deleted note ${id}`);
145
+ }
146
+ }
147
+ catch (error) {
148
+ handleError(error, cfg.json);
149
+ }
150
+ });
151
+ notes
152
+ .command("update")
153
+ .description("Update note metadata (type, tags, pin/archive status). For content edits, use `cnotes versions create`.")
154
+ .argument("<id>", "Note display ID or Convex ID")
155
+ .option("--type <type>", "New note type (e.g. Insight, Painpoint)")
156
+ .option("--tags <tags>", "Replace tags (comma-separated). Use --tags '' to clear.")
157
+ .option("--pin", "Pin the note")
158
+ .option("--unpin", "Unpin the note")
159
+ .option("--archive", "Archive the note")
160
+ .option("--unarchive", "Unarchive the note")
161
+ .addHelpText("after", `
162
+ Examples:
163
+ $ cnotes notes update MEETING-12 --type Insight
164
+ $ cnotes notes update MEETING-12 --tags "auth,security" --pin
165
+ `)
166
+ .action(async (id, opts) => {
167
+ const cfg = resolveConfig(program.opts());
168
+ const api = new ApiClient(cfg.serverUrl, cfg.token);
169
+ try {
170
+ if (opts.pin && opts.unpin) {
171
+ throw new ValidationError("--pin and --unpin are mutually exclusive");
172
+ }
173
+ if (opts.archive && opts.unarchive) {
174
+ throw new ValidationError("--archive and --unarchive are mutually exclusive");
175
+ }
176
+ const body = {};
177
+ if (opts.type !== undefined)
178
+ body.type = opts.type;
179
+ if (opts.tags !== undefined) {
180
+ body.tags = opts.tags === ""
181
+ ? []
182
+ : opts.tags.split(",").map((t) => t.trim()).filter(Boolean);
183
+ }
184
+ if (opts.pin)
185
+ body.isPinned = true;
186
+ if (opts.unpin)
187
+ body.isPinned = false;
188
+ if (opts.archive)
189
+ body.isArchived = true;
190
+ if (opts.unarchive)
191
+ body.isArchived = false;
192
+ if (Object.keys(body).length === 0) {
193
+ throw new ValidationError("At least one of --type, --tags, --pin, --unpin, --archive, or --unarchive is required");
194
+ }
195
+ const noteId = await resolveNoteId(api, cfg.workspaceId, id);
196
+ const data = await api.patch(`/api/notes/${noteId}`, body);
197
+ const note = data.note ?? {};
198
+ if (cfg.quiet) {
199
+ outputQuiet(String(note.displayId || note.id || ""));
200
+ return;
201
+ }
202
+ if (cfg.json) {
203
+ outputJson(data);
204
+ }
205
+ else {
206
+ console.log(`Updated note: ${note.displayId || note.id || id}`);
207
+ if (body.type !== undefined)
208
+ console.log(`Type: ${body.type}`);
209
+ if (body.tags !== undefined)
210
+ console.log(`Tags: ${body.tags.join(", ") || "(none)"}`);
211
+ if (body.isPinned !== undefined)
212
+ console.log(`Pinned: ${body.isPinned}`);
213
+ if (body.isArchived !== undefined)
214
+ console.log(`Archived: ${body.isArchived}`);
215
+ }
216
+ }
217
+ catch (error) {
218
+ handleError(error, cfg.json);
219
+ }
220
+ });
221
+ notes
222
+ .command("bulk-archive")
223
+ .description("Archive or unarchive multiple notes in one operation")
224
+ .requiredOption("--ids <json>", "JSON array of note display IDs or Convex IDs, e.g. '[\"MEETING-1\",\"PRD-3\"]'")
225
+ .option("--unarchive", "Unarchive instead of archive")
226
+ .addHelpText("after", `
227
+ Examples:
228
+ $ cnotes notes bulk-archive --ids '["MEETING-1","PRD-3","IDEA-7"]'
229
+ $ cnotes notes bulk-archive --ids '["NOTE-9"]' --unarchive
230
+ `)
231
+ .action(async (opts) => {
232
+ const cfg = resolveConfig(program.opts());
233
+ const api = new ApiClient(cfg.serverUrl, cfg.token);
234
+ try {
235
+ let ids;
236
+ try {
237
+ ids = JSON.parse(opts.ids);
238
+ }
239
+ catch {
240
+ throw new ValidationError("--ids must be valid JSON array, e.g. '[\"MEETING-1\",\"PRD-3\"]'");
241
+ }
242
+ if (!Array.isArray(ids) || ids.length === 0) {
243
+ throw new ValidationError("--ids must be a non-empty JSON array of strings");
244
+ }
245
+ // Resolve display IDs to Convex IDs
246
+ const noteIds = await Promise.all(ids.map((id) => resolveNoteId(api, cfg.workspaceId, String(id))));
247
+ const isArchived = !opts.unarchive;
248
+ const data = await api.post("/api/notes/bulk", {
249
+ action: "bulkArchive",
250
+ noteIds,
251
+ isArchived,
252
+ });
253
+ if (cfg.json) {
254
+ outputJson(data);
255
+ }
256
+ else {
257
+ const results = (data.results || []);
258
+ const verb = isArchived ? "Archived" : "Unarchived";
259
+ const successStatus = isArchived ? "archived" : "unarchived";
260
+ const succeeded = results.filter((r) => r.status === successStatus).length;
261
+ const failed = results.filter((r) => r.status === "error").length;
262
+ console.log(`${verb} ${succeeded} notes${failed > 0 ? ` (${failed} failed)` : ""}`);
263
+ }
264
+ }
265
+ catch (error) {
266
+ handleError(error, cfg.json);
267
+ }
268
+ });
269
+ notes
270
+ .command("create")
271
+ .description("Create one or more interlinked notes in a single transaction.\n" +
272
+ " Pass --notes either as an array of items or a single item object.\n" +
273
+ " Relationship link syntax (use inside markdown):\n" +
274
+ " [@<key>: Title](relationship:references) → batch placeholder; @<key> is\n" +
275
+ " replaced with the new note's\n" +
276
+ " display ID after creation.\n" +
277
+ " [NOTE-12: Title](relationship:references) → existing-note reference.\n" +
278
+ " Always include the `: Title` part — the title-less form `[NOTE-12](relationship:references)`\n" +
279
+ " produces a chip with `title: null`, which renders as \"Untitled\" in the UI.\n" +
280
+ " The relationship name is free-form (e.g. references, supports, triggers); use\n" +
281
+ " `references` if unsure.")
282
+ .requiredOption("--notes <json>", "JSON for items to create — pass a single item object (treated as a\n" +
283
+ " one-item batch) or an array of {key, type, markdown|markdownFile, tags?}.\n" +
284
+ " Examples:\n" +
285
+ " '{\"key\":\"A\",\"type\":\"Insight\",\"markdown\":\"# Solo note\"}'\n" +
286
+ " '[{\"key\":\"PARENT\",\"type\":\"Insight\",\"markdown\":\"# Parent\\nSee [@CHILD: Child](relationship:references).\"},\n" +
287
+ " {\"key\":\"CHILD\",\"type\":\"Insight\",\"markdown\":\"# Child\\nRelated to [@PARENT: Parent](relationship:references).\"}]'")
288
+ .addHelpText("after", `
289
+ Examples:
290
+ $ cnotes notes create --notes '{"key":"A","type":"Insight","markdown":"# Solo note"}'
291
+ $ cnotes notes create --notes '[{"key":"P","type":"Insight","markdown":"# Parent\\nSee [@C: Child](relationship:references)."},{"key":"C","type":"Insight","markdown":"# Child"}]'
292
+
293
+ Output (--json):
294
+ Returns an object, not an array. Created notes are under the "results" key
295
+ (not "notes"/"ok"); each carries displayId + title. Read them with:
296
+ cnotes notes create --json --notes '…' | jq -r '.results[].displayId'
297
+ Success is the exit code (0). Do NOT re-run on a missing key; a re-run
298
+ duplicates the batch. A non-zero exit prints { error, code, exitCode, hint }.
299
+ `)
300
+ .action(async (opts) => {
301
+ const cfg = resolveConfig(program.opts());
302
+ const api = new ApiClient(cfg.serverUrl, cfg.token);
303
+ try {
304
+ let parsed;
305
+ try {
306
+ parsed = JSON.parse(opts.notes);
307
+ }
308
+ catch {
309
+ throw new ValidationError("--notes must be valid JSON (an item object or an array of item objects)");
310
+ }
311
+ // Auto-wrap a single object into a one-item array.
312
+ const items = Array.isArray(parsed)
313
+ ? parsed
314
+ : parsed && typeof parsed === "object"
315
+ ? [parsed]
316
+ : [];
317
+ if (items.length === 0) {
318
+ throw new ValidationError("--notes must contain at least one item (object or non-empty array)");
319
+ }
320
+ const localErrors = [];
321
+ const notes = [];
322
+ items.forEach((raw, index) => {
323
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
324
+ localErrors.push({ index, field: "item", message: "Item must be a JSON object" });
325
+ return;
326
+ }
327
+ const obj = raw;
328
+ const key = typeof obj.key === "string" ? obj.key.trim() : "";
329
+ const type = typeof obj.type === "string" ? obj.type.trim() : "";
330
+ const markdownInline = typeof obj.markdown === "string" ? obj.markdown : undefined;
331
+ const markdownFile = typeof obj.markdownFile === "string" ? obj.markdownFile : undefined;
332
+ if (!key) {
333
+ localErrors.push({ index, field: "key", message: "key is required (non-empty string used to cross-reference items in this batch)" });
334
+ }
335
+ if (!type) {
336
+ localErrors.push({ index, key: key || undefined, field: "type", message: "type is required (e.g. \"Insight\", \"Meeting\")" });
337
+ }
338
+ let markdown = markdownInline;
339
+ if (!markdown && markdownFile) {
340
+ try {
341
+ markdown = readFileSafe(markdownFile);
342
+ }
343
+ catch (e) {
344
+ const reason = e instanceof Error ? e.message : "could not read file";
345
+ localErrors.push({ index, key: key || undefined, field: "markdownFile", message: reason });
346
+ }
347
+ }
348
+ if (!markdown) {
349
+ localErrors.push({ index, key: key || undefined, field: "markdown", message: "markdown or markdownFile is required" });
350
+ }
351
+ let tags;
352
+ if (obj.tags !== undefined && obj.tags !== null) {
353
+ if (!Array.isArray(obj.tags) || !obj.tags.every((t) => typeof t === "string")) {
354
+ localErrors.push({ index, key: key || undefined, field: "tags", message: "tags must be an array of strings" });
355
+ }
356
+ else {
357
+ tags = obj.tags;
358
+ }
359
+ }
360
+ if (key && type && markdown) {
361
+ notes.push({ key, type, markdown, tags });
362
+ }
363
+ });
364
+ if (localErrors.length > 0) {
365
+ const summary = `${localErrors.length} of ${items.length} item${items.length === 1 ? "" : "s"} invalid`;
366
+ const lines = localErrors.map((e) => ` - item ${e.index} (${e.key ?? "?"}): ${e.field}: ${e.message}`);
367
+ throw new ValidationError(`${summary}\n${lines.join("\n")}`);
368
+ }
369
+ const data = await api.post("/api/notes/bulk", {
370
+ action: "bulkCreate",
371
+ workspaceId: cfg.workspaceId,
372
+ notes,
373
+ });
374
+ if (cfg.quiet) {
375
+ const results = (data.results || []);
376
+ results.forEach((r) => outputQuiet(String(r.displayId || r.noteId || "")));
377
+ return;
378
+ }
379
+ if (cfg.json) {
380
+ outputJson(data);
381
+ }
382
+ else {
383
+ const results = (data.results || []);
384
+ const relCount = data.relationshipsCreated || 0;
385
+ const newTypes = (data.relationshipTypesCreated || []);
386
+ const noun = results.length === 1 ? "note" : "notes";
387
+ console.log(`Created ${results.length} ${noun} (${relCount} relationships)`);
388
+ if (newTypes.length > 0) {
389
+ console.log(` + created ${newTypes.length} new relationship type${newTypes.length === 1 ? "" : "s"}: ${newTypes.join(", ")}`);
390
+ }
391
+ for (const r of results) {
392
+ console.log(` ${r.displayId}: ${r.title || "(untitled)"}`);
393
+ }
394
+ }
395
+ }
396
+ catch (error) {
397
+ handleError(error, cfg.json);
398
+ }
399
+ });
400
+ notes
401
+ .command("search")
402
+ .description("Search notes by text")
403
+ .argument("<query>", "Search query")
404
+ .option("--type <type>", "Filter by note type")
405
+ .option("--limit <n>", "Maximum results", "10")
406
+ .addHelpText("after", `
407
+ Examples:
408
+ $ cnotes notes search "rate limit"
409
+ $ cnotes notes search "onboarding" --type Insight --json
410
+ `)
411
+ .action(async (query, opts) => {
412
+ const cfg = resolveConfig(program.opts());
413
+ const api = new ApiClient(cfg.serverUrl, cfg.token);
414
+ try {
415
+ const notesList = await api.get("/api/notes", {
416
+ workspaceId: cfg.workspaceId,
417
+ search: query,
418
+ types: opts.type,
419
+ excludeContent: "true",
420
+ });
421
+ const limit = parseInt(opts.limit, 10);
422
+ const all = Array.isArray(notesList) ? notesList : [];
423
+ if (cfg.json) {
424
+ const shown = all.slice(0, limit);
425
+ if (all.length > shown.length) {
426
+ console.error(`Showing ${shown.length} of ${all.length} — raise --limit to see more.`);
427
+ }
428
+ outputJson(shown, cfg.fields);
429
+ return;
430
+ }
431
+ await displayNotesList(all, limit, cfg, api, "results — select to view:");
432
+ }
433
+ catch (error) {
434
+ handleError(error, cfg.json);
435
+ }
436
+ });
437
+ notes
438
+ .command("retype")
439
+ .description("Change a note's type (atomic: creates new note, archives old one, cross-links both)")
440
+ .argument("<displayId>", "Display ID of the note to retype (e.g., NOTE-1)")
441
+ .requiredOption("--type <type>", "Target type name (must be an existing supertag)")
442
+ .addHelpText("after", `
443
+ Examples:
444
+ $ cnotes notes retype NOTE-1 --type Insight
445
+ `)
446
+ .action(async (displayId, opts) => {
447
+ const cfg = resolveConfig(program.opts());
448
+ const api = new ApiClient(cfg.serverUrl, cfg.token);
449
+ try {
450
+ const data = await api.post("/api/notes/retype", {
451
+ workspaceId: cfg.workspaceId,
452
+ noteId: displayId,
453
+ targetType: opts.type,
454
+ });
455
+ if (cfg.quiet) {
456
+ outputQuiet(data.newNote.displayId);
457
+ return;
458
+ }
459
+ if (cfg.json) {
460
+ outputJson(data);
461
+ }
462
+ else {
463
+ console.log(`Retyped ${displayId} → ${data.newNote.displayId}`);
464
+ }
465
+ }
466
+ catch (error) {
467
+ handleError(error, cfg.json);
468
+ }
469
+ });
470
+ notes
471
+ .command("bulk-retype")
472
+ .description("Retype multiple notes to a new type in one operation")
473
+ .requiredOption("--ids <json>", "JSON array of note display IDs or Convex IDs, e.g. '[\"NOTE-1\",\"NOTE-3\"]'")
474
+ .requiredOption("--type <type>", "Target type name (must be an existing supertag)")
475
+ .addHelpText("after", `
476
+ Examples:
477
+ $ cnotes notes bulk-retype --ids '["NOTE-1","NOTE-3"]' --type Insight
478
+ `)
479
+ .action(async (opts) => {
480
+ const cfg = resolveConfig(program.opts());
481
+ const api = new ApiClient(cfg.serverUrl, cfg.token);
482
+ try {
483
+ let ids;
484
+ try {
485
+ ids = JSON.parse(opts.ids);
486
+ }
487
+ catch {
488
+ throw new ValidationError("--ids must be valid JSON array, e.g. '[\"NOTE-1\",\"NOTE-3\"]'");
489
+ }
490
+ if (!Array.isArray(ids) || ids.length === 0) {
491
+ throw new ValidationError("--ids must be a non-empty JSON array of strings");
492
+ }
493
+ const data = await api.post("/api/notes/bulk", {
494
+ action: "bulkRetype",
495
+ workspaceId: cfg.workspaceId,
496
+ noteIds: ids,
497
+ targetType: opts.type,
498
+ });
499
+ if (cfg.json) {
500
+ outputJson(data);
501
+ return;
502
+ }
503
+ const results = (data.results || []);
504
+ const succeeded = results.filter((r) => r.status === "retyped").length;
505
+ const failed = results.filter((r) => r.status === "error").length;
506
+ if (cfg.quiet) {
507
+ results
508
+ .filter((r) => r.status === "retyped")
509
+ .forEach((r) => outputQuiet(String(r.newDisplayId || "")));
510
+ return;
511
+ }
512
+ console.log(`Retyped ${succeeded} notes → ${opts.type}${failed > 0 ? ` (${failed} failed)` : ""}`);
513
+ for (const r of results) {
514
+ if (r.status === "retyped") {
515
+ console.log(` ${r.displayId} → ${r.newDisplayId}`);
516
+ }
517
+ else {
518
+ console.log(` ${r.displayId}: error — ${r.error}`);
519
+ }
520
+ }
521
+ }
522
+ catch (error) {
523
+ handleError(error, cfg.json);
524
+ }
525
+ });
526
+ notes
527
+ .command("move")
528
+ .description("Move a note to another workspace (atomic: copies content to the target, archives the source, cross-links both)")
529
+ .argument("<displayId>", "Display ID of the note to move (e.g., NOTE-1)")
530
+ .requiredOption("--to <workspaceId>", "Target workspace ID to move the note into")
531
+ .option("--type <type>", "Target type name in the destination workspace (auto-created from the source type if missing; defaults to the source note's type)")
532
+ .addHelpText("after", `
533
+ Examples:
534
+ $ cnotes notes move NOTE-1 --to pd72y6fyhj9mway3y647bv02r183xk5j
535
+ $ cnotes notes move INS-7 --to <workspaceId> --type Insight
536
+ `)
537
+ .action(async (displayId, opts) => {
538
+ const cfg = resolveConfig(program.opts());
539
+ const api = new ApiClient(cfg.serverUrl, cfg.token);
540
+ if (opts.to === cfg.workspaceId) {
541
+ handleError(new ValidationError("Source and target workspaces are the same — use `cnotes notes retype` to change type within a workspace"), cfg.json);
542
+ return;
543
+ }
544
+ try {
545
+ const data = await api.post("/api/notes/move", {
546
+ workspaceId: cfg.workspaceId,
547
+ noteId: displayId,
548
+ targetWorkspaceId: opts.to,
549
+ targetType: opts.type,
550
+ });
551
+ if (cfg.quiet) {
552
+ outputQuiet(data.newNote.displayId);
553
+ return;
554
+ }
555
+ if (cfg.json) {
556
+ outputJson(data);
557
+ }
558
+ else {
559
+ console.log(`Moved ${displayId} → ${data.newNote.displayId} in ${opts.to} (source archived)`);
560
+ }
561
+ }
562
+ catch (error) {
563
+ handleError(error, cfg.json);
564
+ }
565
+ });
566
+ }
567
+ /**
568
+ * Display a list of notes with interactive selection or plain table.
569
+ */
570
+ async function displayNotesList(notesList, limit, cfg, api, message) {
571
+ const sliced = Array.isArray(notesList) ? notesList.slice(0, limit) : [];
572
+ if (sliced.length === 0) {
573
+ console.log("No notes found.");
574
+ return;
575
+ }
576
+ if (process.stdout.isTTY) {
577
+ const chosenIndex = await select({
578
+ message: `${sliced.length} ${message}`,
579
+ choices: sliced.map((n, i) => ({
580
+ name: `${String(n.displayId || "").padEnd(14)} ${truncate(String(n.title || "(untitled)"), 52).padEnd(54)} ${n.updatedAt ? timeAgo(n.updatedAt) : ""}`,
581
+ value: i,
582
+ })),
583
+ loop: true,
584
+ });
585
+ const selected = sliced[chosenIndex];
586
+ const displayId = String(selected.displayId || "");
587
+ if (displayId) {
588
+ await viewNoteInteractive(api, cfg.workspaceId, displayId);
589
+ }
590
+ return;
591
+ }
592
+ const rows = sliced.map((n) => [
593
+ String(n.displayId || ""),
594
+ truncate(String(n.title || "(untitled)"), 50),
595
+ n.updatedAt ? timeAgo(n.updatedAt) : "",
596
+ ]);
597
+ outputTable(["ID", "TITLE", "UPDATED"], rows);
598
+ if (notesList.length > sliced.length) {
599
+ console.log(`\nShowing ${sliced.length} of ${notesList.length} — raise --limit to see more.`);
600
+ }
601
+ }
602
+ /**
603
+ * Interactive note viewer: fetch note, then let user choose TLDR or Full Content.
604
+ * TLDR is pre-selected so double-enter from list goes straight to summary.
605
+ */
606
+ async function viewNoteInteractive(api, workspaceId, displayId) {
607
+ const noteData = await fetchNoteData(api, workspaceId, displayId);
608
+ if (!noteData) {
609
+ console.log("Could not fetch note.");
610
+ return;
611
+ }
612
+ const { note, latestVersion } = noteData;
613
+ // Print note header
614
+ console.log(`\n${note.displayId || note._id || note.id}`);
615
+ console.log(`Title: ${note.title || "(untitled)"}`);
616
+ console.log(`Type: ${note.type || "Note"}`);
617
+ if (note.tags && Array.isArray(note.tags) && note.tags.length > 0) {
618
+ console.log(`Tags: ${note.tags.join(", ")}`);
619
+ }
620
+ if (note.updatedAt) {
621
+ console.log(`Updated: ${timeAgo(note.updatedAt)}`);
622
+ }
623
+ const hasSummary = latestVersion?.searchContent || latestVersion?.summary;
624
+ const markdown = (latestVersion?.content || note.content);
625
+ if (!hasSummary && !markdown) {
626
+ console.log("\n(no content)");
627
+ return;
628
+ }
629
+ const viewMode = await select({
630
+ message: "View:",
631
+ choices: [
632
+ { name: "TLDR", value: "tldr" },
633
+ { name: "Full Content", value: "full" },
634
+ ],
635
+ });
636
+ if (viewMode === "tldr") {
637
+ if (latestVersion?.summary) {
638
+ console.log(`\n${latestVersion.summary}`);
639
+ }
640
+ else if (markdown) {
641
+ // No AI summary — show first ~8 lines of markdown as preview
642
+ const lines = markdown.split("\n").filter(Boolean);
643
+ console.log(`\n${lines.slice(0, 8).join("\n")}${lines.length > 8 ? "\n\n..." : ""}`);
644
+ }
645
+ else {
646
+ console.log("\n(no summary available)");
647
+ }
648
+ }
649
+ else {
650
+ if (markdown) {
651
+ console.log(`\n${markdown}`);
652
+ }
653
+ }
654
+ }
655
+ /**
656
+ * Fetch note data (metadata + latest version) by display ID.
657
+ */
658
+ async function fetchNoteData(api, workspaceId, displayId) {
659
+ try {
660
+ const notesList = await api.get("/api/notes", {
661
+ workspaceId,
662
+ exact_id: displayId,
663
+ format: "markdown",
664
+ });
665
+ const note = (Array.isArray(notesList) ? notesList : []).find((n) => n.displayId === displayId) || (Array.isArray(notesList) ? notesList[0] : null);
666
+ if (!note)
667
+ return null;
668
+ // latestVersion may be a nested object (cookie auth) or content may be top-level (CLI auth)
669
+ const lv = note.latestVersion || null;
670
+ return {
671
+ note,
672
+ latestVersion: lv || (note.content ? { content: note.content } : null),
673
+ };
674
+ }
675
+ catch {
676
+ return null;
677
+ }
678
+ }
679
+ function printNote(note, version) {
680
+ console.log(`\n${note.displayId || note._id || note.id}`);
681
+ console.log(`Title: ${note.title || "(untitled)"}`);
682
+ console.log(`Type: ${note.type || "Note"}`);
683
+ if (note.tags && Array.isArray(note.tags) && note.tags.length > 0) {
684
+ console.log(`Tags: ${note.tags.join(", ")}`);
685
+ }
686
+ if (note.isPinned)
687
+ console.log("Pinned: yes");
688
+ if (note.isArchived) {
689
+ const archivedAt = note.archivedAt;
690
+ console.log(`Archived: ${archivedAt ? timeAgo(archivedAt) : "yes"}`);
691
+ }
692
+ if (note.updatedAt) {
693
+ console.log(`Updated: ${timeAgo(note.updatedAt)}`);
694
+ }
695
+ // Content arrives as a markdown string (server renders TipTap → markdown for ?format=markdown).
696
+ const markdown = (version?.content || note.latestVersion?.content || note.content);
697
+ if (typeof markdown === "string" && markdown.length > 0) {
698
+ console.log(`\n${markdown}`);
699
+ return;
700
+ }
701
+ const searchContent = note.latestVersion?.searchContent || version?.searchContent;
702
+ if (searchContent) {
703
+ console.log(`\n${searchContent}`);
704
+ }
705
+ }
706
+ //# sourceMappingURL=notes.js.map