@creator-notes/cnotes 0.26.0 → 0.30.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -9,8 +9,8 @@
9
9
  * At runtime this file lives at <package>/dist/lib/install-skill.js, so the
10
10
  * skill source is two directories up.
11
11
  */
12
- import { copyFileSync, cpSync, existsSync, mkdirSync, readFileSync, rmSync } from "fs";
13
- import { dirname, resolve } from "path";
12
+ import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync } from "fs";
13
+ import { dirname, relative, resolve } from "path";
14
14
  import { homedir } from "os";
15
15
  import { fileURLToPath } from "url";
16
16
  export const SKILL_TARGETS = ["claude", "codex"];
@@ -66,6 +66,32 @@ function filesAreIdentical(a, b) {
66
66
  return false;
67
67
  }
68
68
  }
69
+ /** Every file path under `dir`, relative to it (recursive). */
70
+ function listFilesRecursive(dir, base = dir) {
71
+ const out = [];
72
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
73
+ const full = resolve(dir, entry.name);
74
+ if (entry.isDirectory())
75
+ out.push(...listFilesRecursive(full, base));
76
+ else
77
+ out.push(relative(base, full));
78
+ }
79
+ return out;
80
+ }
81
+ /**
82
+ * True when every bundled file under `sourceDir` (SKILL.md plus any references/)
83
+ * exists byte-identical under `destDir`. The skill folder is a handful of small
84
+ * files, so this per-invocation compare stays cheap — and unlike a SKILL.md-only
85
+ * check it also notices a changed reference and triggers a refresh.
86
+ */
87
+ function skillTreeUpToDate(sourceDir, destDir) {
88
+ for (const rel of listFilesRecursive(sourceDir)) {
89
+ if (!filesAreIdentical(resolve(sourceDir, rel), resolve(destDir, rel))) {
90
+ return false;
91
+ }
92
+ }
93
+ return true;
94
+ }
69
95
  export const SKILL_CATALOG = [
70
96
  {
71
97
  name: "cnotes",
@@ -108,8 +134,7 @@ export function installSkillFolder(name, target) {
108
134
  throw new Error(`Bundled skill "${name}" not found at ${source}`);
109
135
  }
110
136
  const dest = skillFolderDest(name, target);
111
- const upToDate = existsSync(dest) &&
112
- filesAreIdentical(resolve(source, "SKILL.md"), resolve(dest, "SKILL.md"));
137
+ const upToDate = existsSync(dest) && skillTreeUpToDate(source, dest);
113
138
  if (!upToDate) {
114
139
  mkdirSync(dirname(dest), { recursive: true });
115
140
  cpSync(source, dest, { recursive: true });
@@ -143,20 +168,27 @@ export function refreshInstalledSkills() {
143
168
  return results;
144
169
  }
145
170
  /**
146
- * Copy the bundled skill into a target agent's skills dir.
147
- * Caller is responsible for any prompting / confirmation.
171
+ * Copy the bundled skill *folder* (SKILL.md plus its `references/`) into a
172
+ * target agent's skills dir. Caller is responsible for any prompting.
173
+ *
174
+ * The whole folder is copied — not just SKILL.md — so progressively-disclosed
175
+ * references (e.g. `references/humanizer.md`, loaded only for writing tasks) ship
176
+ * alongside the skill. `destPath` still points at the SKILL.md so callers'
177
+ * messages read the same.
148
178
  */
149
179
  export function installSkill(target = "claude") {
150
- const source = getBundledSkillPath();
180
+ const sourceFile = getBundledSkillPath();
151
181
  const dest = getSkillDestPath(target);
152
- if (!existsSync(source)) {
153
- throw new Error(`Bundled skill not found at ${source}`);
182
+ const sourceDir = dirname(sourceFile);
183
+ const destDir = dirname(dest);
184
+ if (!existsSync(sourceFile)) {
185
+ throw new Error(`Bundled skill not found at ${sourceFile}`);
154
186
  }
155
- if (existsSync(dest) && filesAreIdentical(source, dest)) {
187
+ if (existsSync(dest) && skillTreeUpToDate(sourceDir, destDir)) {
156
188
  return { target, installed: false, destPath: dest, alreadyUpToDate: true };
157
189
  }
158
- mkdirSync(dirname(dest), { recursive: true });
159
- copyFileSync(source, dest);
190
+ mkdirSync(destDir, { recursive: true });
191
+ cpSync(sourceDir, destDir, { recursive: true }); // SKILL.md + references/
160
192
  removeLegacySkill(target); // drop the old `cn` skill once the new one is in place
161
193
  return { target, installed: true, destPath: dest };
162
194
  }
@@ -1 +1 @@
1
- {"version":3,"file":"install-skill.js","sourceRoot":"","sources":["../../src/lib/install-skill.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"install-skill.js","sourceRoot":"","sources":["../../src/lib/install-skill.ts"],"names":[],"mappings":"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"}
@@ -101,7 +101,7 @@ async function withErrorHandling(fn) {
101
101
  // ---------------------------------------------------------------------------
102
102
  const server = new McpServer({
103
103
  name: "cnotes-mcp",
104
- version: "0.3.0",
104
+ version: "0.4.0",
105
105
  });
106
106
  // ---------------------------------------------------------------------------
107
107
  // Resources
@@ -233,7 +233,7 @@ const NoteCreateItem = z.object({
233
233
  markdown: z.string().describe("Note content as markdown (the first # h1 heading becomes the title)"),
234
234
  tags: z.array(z.string()).optional().describe("Tags for this note"),
235
235
  });
236
- server.tool("cnotes_notes_create", "Create one or more interlinked notes in a single atomic transaction. Before authoring, read the target type's rubric (cnotes_types_show, or the validationPrompt field from cnotes_types_list) and write to it so the note lands well-formed. Pass `notes` as a single item object or an array of items. Title is derived from the # h1 heading in each note's markdown. Relationship link syntax inside markdown: use [@key: Title](relationship:references) to cross-reference another note in the same batch (the @key is replaced with the new note's display ID after creation), or [NOTE-12: Title](relationship:references) to reference an existing note. ALWAYS include the `: Title` part — the title-less form `[NOTE-12](relationship:references)` saves a chip with `title: null` that renders as \"Untitled\" in the UI. The relationship name is free-form (e.g. references, supports, triggers); use `references` if unsure. The response echoes each used type's rubric under `rubrics` — revise in-loop if a note falls short. If validation fails, the response includes a structured `details.items` array pinpointing which item (by index + key) is malformed and why.", {
236
+ server.tool("cnotes_notes_create", "Create one or more interlinked notes in a single atomic transaction. FIRST read the target type's rubric (cnotes_types_show) and author to it. Pass `notes` as one item object or an array; each note's title is its first # h1 heading. Cross-reference in markdown with [@key: Title](relationship:type) (same-batch — @key resolves to the new display ID) or [NOTE-12: Title](relationship:references) (existing note); ALWAYS include the ': Title' part, and the relationship name is free-form (use 'references' if unsure). The response echoes each type's rubric under `rubrics`; on validation failure, `details.items` pinpoints the bad item by index + key.", {
237
237
  notes: z
238
238
  .union([NoteCreateItem, z.array(NoteCreateItem).min(1)])
239
239
  .describe("Either a single item object or a non-empty array of items."),
@@ -280,7 +280,7 @@ server.tool("cnotes_notes_retype", "Change a single note's type. Atomic operatio
280
280
  // ---------------------------------------------------------------------------
281
281
  // Search tools
282
282
  // ---------------------------------------------------------------------------
283
- server.tool("cnotes_search", "Unified search across NOTES and CANVASES in one call — returns two separately-ranked sections (notes, canvases), so you never miss a relevant canvas by searching only notes. Default (type omitted or 'all') returns both. Set type to the reserved 'Canvas' for canvases only, or to a note supertag (e.g. 'Decision') for notes of that type only. Note rows carry score (higher=better), type, snippet; canvas rows carry score, matchedBy, noteCount, themes, snippet, and a deep link. Prefer this when you don't already know whether the answer is a note or a canvas.", {
283
+ server.tool("cnotes_search", "Unified search across NOTES and CANVASES in one call — returns two separately-ranked sections (notes, canvases), so you never miss a canvas by searching only notes. Default (type omitted or 'all') returns both; set type to 'Canvas' for canvases only, or a note supertag (e.g. 'Decision') for notes of that type. Prefer this when you don't already know whether the answer is a note or a canvas.", {
284
284
  query: z.string().describe("Search query"),
285
285
  type: z.string().optional().describe("Scope: omit or 'all' for both, 'Canvas' for canvases only, or a note supertag (e.g. 'Decision') for notes of that type"),
286
286
  limit: z.number().optional().default(10).describe("Max results per section"),
@@ -302,7 +302,7 @@ server.tool("cnotes_search_semantic", "Semantic (vector) search across NOTES onl
302
302
  limit: limit ?? 10,
303
303
  ...(canvasId ? { canvasId } : {}),
304
304
  })));
305
- server.tool("cnotes_canvas_search", "Semantic + lexical search across CANVASES only. Finds a canvas by name, topic, or meaning — blends lexical match (name / themes / digest) with semantic similarity over canvas-digest embeddings, so a query that shares no words with a canvas can still surface it. Use this to locate the right canvas before reading it; for a single call that also covers notes, use cnotes_search. Pass '*' or 'all' to enumerate every canvas. Each result carries id, displayId, title, matchedBy (lexical|semantic|both), relevance (0..1), purpose, themes, noteCount, snippet, and a deep link.", {
305
+ server.tool("cnotes_canvas_search", "Semantic + lexical search across CANVASES only — find a canvas by name, topic, or meaning (blends lexical name/theme/digest match with semantic similarity, so a query sharing no words with a canvas can still surface it). Use it to locate the right canvas before reading it; for one call that also covers notes, use cnotes_search. Pass '*' or 'all' to enumerate every canvas.", {
306
306
  query: z.string().describe("Search query (use '*' or 'all' to list every canvas)"),
307
307
  limit: z.number().optional().default(10).describe("Max results"),
308
308
  }, async ({ query, limit }) => withErrorHandling(() => api.get("/api/canvas/search", {
@@ -373,28 +373,25 @@ server.tool("cnotes_canvas_add_node", "[Deprecated: use cnotes_canvas_bulk_add]
373
373
  action: "bulkAddNodes",
374
374
  items: [{ type: "note", noteId, positionX: x, positionY: y }],
375
375
  })));
376
- server.tool("cnotes_canvas_add_text", "[Deprecated: use cnotes_canvas_bulk_add] Add a text annotation node to a canvas.", {
376
+ // The canvas has ONE free-form text element, called "Text", and it is a `richtext`
377
+ // node on the wire. The old `text` node kind (the "Heading") is LEGACY: existing ones
378
+ // still render / move / delete, but nothing creates them any more — a Text element with
379
+ // no background tint and `# Foo` inside IS a heading.
380
+ const TEXT_ELEMENT_CONTENT_DESC = "Text content as markdown (headings, emphasis, images all work — a heading is just `# Title`). Plain markdown ONLY — do NOT use [NOTE-X](relationship:...) or [[...]] mention syntax here (it corrupts the card, which renders blank); reference notes as plain display IDs (\"see RISK-64\"). Keep it card-sized (≤ ~3 sentences): text elements are unversioned, unsearchable presentation furniture — knowledge worth keeping belongs in a typed note.";
381
+ const textElementSchema = {
377
382
  canvasId: z.string().describe("Canvas ID"),
378
- text: z.string().describe("Text content"),
379
- size: z.enum(["heading", "paragraph"]).optional().default("heading").describe("Text size"),
380
- color: z.enum(["normal", "muted", "highlighted"]).optional().describe("Color variant"),
381
- x: z.number().optional().default(100).describe("X position"),
382
- y: z.number().optional().default(100).describe("Y position"),
383
- }, async ({ canvasId, text, size, color, x, y }) => withErrorHandling(() => api.post(`/api/canvas/${canvasId}`, {
384
- action: "bulkAddNodes",
385
- items: [{ type: "text", content: text, fontSize: size === "paragraph" ? 18 : 32, colorVariant: color, positionX: x, positionY: y }],
386
- })));
387
- server.tool("cnotes_canvas_add_richtext", "[Deprecated: use cnotes_canvas_bulk_add] Add a richtext annotation node to a canvas.", {
388
- canvasId: z.string().describe("Canvas ID"),
389
- content: z.string().describe("Richtext content as markdown. Plain markdown ONLY — do NOT use [NOTE-X](relationship:...) or [[...]] mention syntax here (it corrupts the card, which renders blank); reference notes as plain display IDs (\"see RISK-64\"). Keep it sticky-sized (≤ ~3 sentences): richtext is unversioned, unsearchable presentation furniture — knowledge worth keeping belongs in a typed note."),
383
+ content: z.string().describe(TEXT_ELEMENT_CONTENT_DESC),
390
384
  size: z.enum(["small", "medium", "large"]).optional().default("small").describe("Display size"),
391
- color: z.string().optional().describe("Background color hex"),
385
+ color: z.string().optional().describe("Background tint color hex. Default is NO background — omit unless the tint encodes a role used consistently on this canvas."),
392
386
  x: z.number().optional().default(100).describe("X position"),
393
387
  y: z.number().optional().default(100).describe("Y position"),
394
- }, async ({ canvasId, content, size, color, x, y }) => withErrorHandling(() => api.post(`/api/canvas/${canvasId}`, {
388
+ };
389
+ const addTextElement = ({ canvasId, content, size, color, x, y }) => withErrorHandling(() => api.post(`/api/canvas/${canvasId}`, {
395
390
  action: "bulkAddNodes",
396
391
  items: [{ type: "richtext", content, size, colorHex: color, positionX: x, positionY: y }],
397
- })));
392
+ }));
393
+ server.tool("cnotes_canvas_add_text", "[Deprecated: use cnotes_canvas_bulk_add] Add a Text element to a canvas — free-form rich text placed directly on the canvas (markdown: headings, emphasis, images), with an optional background tint (none by default). A heading is just content '# Title'.", textElementSchema, addTextElement);
394
+ server.tool("cnotes_canvas_add_richtext", "[Deprecated: use cnotes_canvas_add_text or cnotes_canvas_bulk_add] Alias of cnotes_canvas_add_text — adds the same Text element (richtext) with the same arguments.", textElementSchema, addTextElement);
398
395
  server.tool("cnotes_canvas_add_list", "[Deprecated: use cnotes_canvas_bulk_add] Add a list/grid node grouping notes on a canvas. The description is the only label; its first paragraph derives the searchable title.", {
399
396
  canvasId: z.string().describe("Canvas ID"),
400
397
  description: z.string().min(1).describe("Rich-text description (markdown). Required. First paragraph derives the searchable title."),
@@ -423,7 +420,7 @@ server.tool("cnotes_canvas_add_link", "[Deprecated: use cnotes_canvas_bulk_add]
423
420
  items: [{ type: "canvas", linkedCanvasId: targetCanvasId, positionX: x, positionY: y }],
424
421
  })));
425
422
  // --- Primary bulk tool ---
426
- server.tool("cnotes_canvas_bulk_add", "Add one or more items to a canvas. Use this for ALL canvas additions — single or multiple. Supports all node types: note, text, richtext, list, canvas (link to another canvas), section (named region frame), and edge. ALWAYS prefer this over repeated single-item calls. Items are processed in order so edges can reference nodes created earlier in the same batch via sourceItemIndex/targetItemIndex.\n\nELEMENT CHOICE — notes are the knowledge tier (typed, versioned, searchable, the only content agents read back in full); everything else is presentation furniture. Default any claim/fact/decision/finding worth keeping to a typed NOTE. Use text for one-line section labels, richtext only for sticky-sized framing (orientation banners, emphasis quoting a note, image tiles — never the sole home of a claim), list for grouping member notes (description = one-line frame; N distinct things = N notes), canvas for portals, edge for note-to-note relationships (notes only — text/richtext endpoints fail; mirror the relationship as a mention in the note bodies).", {
423
+ server.tool("cnotes_canvas_bulk_add", "Add one or more items to a canvas. Use this for ALL canvas additions — single or multiple. Supported item types: note, richtext (the Text element), list, canvas (link to another canvas), section (named region frame), and edge. ALWAYS prefer this over repeated single-item calls. Items are processed in order so edges can reference nodes created earlier in the same batch via sourceItemIndex/targetItemIndex.\n\nELEMENT CHOICE — notes are the knowledge tier (typed, versioned, searchable, the only content agents read back in full); everything else is presentation furniture. Default any claim/fact/decision/finding worth keeping to a typed NOTE. Use richtext (the Text element) for card-sized framing — orientation banners, a markdown heading used as a label ('# Q1 Goals', untinted), emphasis quoting a note, image tiles — never as the sole home of a claim; section for a named REGION of the canvas; list for grouping member notes (description = one-line frame; N distinct things = N notes); canvas for portals; edge for note-to-note relationships (notes only — richtext endpoints fail; mirror the relationship as a mention in the note bodies).", {
427
424
  canvasId: z.string().describe("Canvas ID"),
428
425
  notes: z.array(z.object({
429
426
  noteId: z.string().describe("Note display ID or Convex ID"),
@@ -438,20 +435,14 @@ server.tool("cnotes_canvas_bulk_add", "Add one or more items to a canvas. Use th
438
435
  y: z.number().describe("Y position"),
439
436
  }),
440
437
  z.object({
441
- type: z.literal("text"),
442
- content: z.string().describe("Text content"),
443
- x: z.number().describe("X position"),
444
- y: z.number().describe("Y position"),
445
- fontSize: z.number().optional().describe("Font size (default 18, h1=32, h2=24)"),
446
- colorVariant: z.enum(["normal", "muted", "highlighted"]).optional(),
447
- }),
448
- z.object({
438
+ // `richtext` IS the Text element. There is no `text` item type any more:
439
+ // the legacy `text` node (the old "Heading") can no longer be created.
449
440
  type: z.literal("richtext"),
450
- content: z.string().describe("Markdown content. Plain markdown ONLY — no [NOTE-X](relationship:...) / [[...]] mention syntax (corrupts the card); reference notes as plain display IDs. Sticky-sized (≤ ~3 sentences); knowledge worth keeping goes in a note."),
441
+ content: z.string().describe("Markdown content — this is the Text element (headings, emphasis, images; a heading is just '# Title'). Plain markdown ONLY — no [NOTE-X](relationship:...) / [[...]] mention syntax (corrupts the card); reference notes as plain display IDs. Card-sized (≤ ~3 sentences); knowledge worth keeping goes in a note."),
451
442
  x: z.number().describe("X position"),
452
443
  y: z.number().describe("Y position"),
453
444
  size: z.enum(["small", "medium", "large"]).optional().describe("Node size"),
454
- colorHex: z.string().optional().describe("Background color hex"),
445
+ colorHex: z.string().optional().describe("Background tint hex. Default is NO background — omit unless the tint encodes a role used consistently on this canvas."),
455
446
  }),
456
447
  z.object({
457
448
  type: z.literal("list"),
@@ -514,13 +505,13 @@ server.tool("cnotes_canvas_bulk_add", "Add one or more items to a canvas. Use th
514
505
  items: allItems,
515
506
  });
516
507
  }));
517
- server.tool("cnotes_canvas_place", "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 a single batch. PREFER THIS over cnotes_canvas_bulk_add whenever you can express the layout as stacks/grids/anchors — which is almost always. The agent should describe STRUCTURE (rows, columns, what's near what), not pixel coordinates.\n\nELEMENT CHOICE: default content to typed NOTES (the only versioned, searchable tier agents can read back); text = one-line labels, richtext = sticky-sized framing only, list = member notes behind a one-line frame, edges connect notes only.\n\nThe call is best-effort, NOT atomic: per-item failures are reported in the response (HTTP 207) but the items that succeeded stay on the canvas. Always inspect `items[].error` and `edges[].error` in the response.\n\nThe `doc` argument is a layout document:\n { root: <LayoutNode>, edges?: [{from, to, label?}], origin?: {x, y} }\n\n<LayoutNode> is one of:\n • { kind: 'stack', axis: 'vertical'|'horizontal', gap?: 'tight'|'medium'|'spacious'|<px>, align?: 'start'|'center'|'end', items: [<LayoutNode>...] }\n • { kind: 'grid', columns: <n>, gap?: ..., items: [<LayoutNode>...] }\n • { kind: 'anchor', to: '<displayId or canvas node id>', direction: 'right'|'left'|'above'|'below', gap?: ..., child: <LayoutNode> }\n • { kind: 'item', type: 'note'|'text'|'richtext'|'list'|'canvas', key?: '<name>', ...item-specific fields }\n\nLeaf item shapes (no x/y!):\n note: { kind:'item', type:'note', noteId:'NOTE-3', key?:'a' }\n text: { kind:'item', type:'text', content:'Hello', fontSize?:32, colorVariant?:'normal'|'muted'|'highlighted' } // one-line section labels only\n richtext: { kind:'item', type:'richtext', content:'# Markdown OK', size?:'small'|'medium'|'large', colorHex?:'<hex>' } // sticky-sized framing only; plain markdown, NO [NOTE-X](relationship:...) mention syntax (corrupts the card); knowledge worth keeping = a note\n list: { kind:'item', type:'list', description:'One-line frame — first paragraph becomes the title', noteIds?:['NOTE-1','NOTE-2'], viewMode?:'list'|'grid' } // description is REQUIRED and is a frame, never the content: N distinct things = N notes in noteIds\n canvas: { kind:'item', type:'canvas', linkedCanvasId:'<id>' }\n\nEdges reference endpoints by, in priority order: `@<key>` (any item with a `key` placed in the same call), the original noteId/displayId of a note placed in this call (e.g. 'NOTE-7'), or the displayId / convex id of a node already on the canvas.\n\nExample (a SWOT-like quadrant):\n {\n root: {\n kind:'stack', axis:'vertical', gap:'spacious',\n items:[\n { kind:'stack', axis:'horizontal', gap:'spacious', items:[\n { kind:'grid', columns:2, items:[\n { kind:'item', type:'note', noteId:'STR-1' },\n { kind:'item', type:'note', noteId:'STR-2' },\n ]},\n { kind:'grid', columns:2, items:[\n { kind:'item', type:'note', noteId:'WEAK-1' },\n ]},\n ]},\n { kind:'stack', axis:'horizontal', gap:'spacious', items:[ /* opportunities + threats */ ]},\n ],\n },\n }", {
508
+ server.tool("cnotes_canvas_place", "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 a single batch. PREFER THIS over cnotes_canvas_bulk_add whenever you can express the layout as stacks/grids/anchors — which is almost always. The agent should describe STRUCTURE (rows, columns, what's near what), not pixel coordinates.\n\nELEMENT CHOICE: default content to typed NOTES (the only versioned, searchable tier agents can read back); richtext = the Text element, card-sized framing only (a heading is just content '# Title', untinted), section = a named region frame, list = member notes behind a one-line frame, edges connect notes only.\n\nThe call is best-effort, NOT atomic: per-item failures are reported in the response (HTTP 207) but the items that succeeded stay on the canvas. Always inspect `items[].error` and `edges[].error` in the response.\n\nThe `doc` argument is a layout document:\n { root: <LayoutNode>, edges?: [{from, to, label?}], origin?: {x, y} }\n\n<LayoutNode> is one of:\n • { kind: 'stack', axis: 'vertical'|'horizontal', gap?: 'tight'|'medium'|'spacious'|<px>, align?: 'start'|'center'|'end', items: [<LayoutNode>...] }\n • { kind: 'grid', columns: <n>, gap?: ..., items: [<LayoutNode>...] }\n • { kind: 'anchor', to: '<displayId or canvas node id>', direction: 'right'|'left'|'above'|'below', gap?: ..., child: <LayoutNode> }\n • { kind: 'item', type: 'note'|'richtext'|'list'|'canvas'|'section', key?: '<name>', ...item-specific fields }\n\nLeaf item shapes (no x/y!):\n note: { kind:'item', type:'note', noteId:'NOTE-3', key?:'a' }\n richtext: { kind:'item', type:'richtext', content:'# Markdown OK', size?:'small'|'medium'|'large', colorHex?:'<hex>' } // the Text element: card-sized framing only; a heading is just content '# Title' with no colorHex; plain markdown, NO [NOTE-X](relationship:...) mention syntax (corrupts the card); knowledge worth keeping = a note\n list: { kind:'item', type:'list', description:'One-line frame — first paragraph becomes the title', noteIds?:['NOTE-1','NOTE-2'], viewMode?:'list'|'grid' } // description is REQUIRED and is a frame, never the content: N distinct things = N notes in noteIds\n canvas: { kind:'item', type:'canvas', linkedCanvasId:'<id>' }\n\nEdges reference endpoints by, in priority order: `@<key>` (any item with a `key` placed in the same call), the original noteId/displayId of a note placed in this call (e.g. 'NOTE-7'), or the displayId / convex id of a node already on the canvas.\n\nExample (a SWOT-like quadrant):\n {\n root: {\n kind:'stack', axis:'vertical', gap:'spacious',\n items:[\n { kind:'stack', axis:'horizontal', gap:'spacious', items:[\n { kind:'grid', columns:2, items:[\n { kind:'item', type:'note', noteId:'STR-1' },\n { kind:'item', type:'note', noteId:'STR-2' },\n ]},\n { kind:'grid', columns:2, items:[\n { kind:'item', type:'note', noteId:'WEAK-1' },\n ]},\n ]},\n { kind:'stack', axis:'horizontal', gap:'spacious', items:[ /* opportunities + threats */ ]},\n ],\n },\n }", {
518
509
  canvasId: z.string().describe("Canvas ID"),
519
510
  doc: z
520
511
  .object({
521
512
  root: z
522
513
  .unknown()
523
- .describe("Root LayoutNode. kind is one of: 'stack' (items flow along one axis, like CSS flexbox: {kind:'stack', axis:'vertical'|'horizontal', gap?, align?:'start'|'center'|'end', items:[LayoutNode...]}), 'grid' (N columns, items wrap to new rows: {kind:'grid', columns:<n>, gap?, items:[...]}), 'anchor' (place a sub-tree relative to an EXISTING canvas node: {kind:'anchor', to:'<displayId or node id>', direction:'right'|'left'|'above'|'below' — exactly these four, NOT top/bottom/up/down, gap?, child:LayoutNode}), 'section' (a named frame that sizes itself around its content: {kind:'section', name:'<title>', key?, child:LayoutNode}), or 'item' (a leaf — one per visible thing, NO x/y: {kind:'item', type:'note'|'text'|'richtext'|'list'|'canvas'|'section', key?, ...type-specific fields}). gap accepts a preset 'tight'|'medium'|'spacious' or a raw pixel number (default 'medium')."),
514
+ .describe("Root LayoutNode. kind is one of: 'stack' (items flow along one axis, like CSS flexbox: {kind:'stack', axis:'vertical'|'horizontal', gap?, align?:'start'|'center'|'end', items:[LayoutNode...]}), 'grid' (N columns, items wrap to new rows: {kind:'grid', columns:<n>, gap?, items:[...]}), 'anchor' (place a sub-tree relative to an EXISTING canvas node: {kind:'anchor', to:'<displayId or node id>', direction:'right'|'left'|'above'|'below' — exactly these four, NOT top/bottom/up/down, gap?, child:LayoutNode}), 'section' (a named frame that sizes itself around its content: {kind:'section', name:'<title>', key?, child:LayoutNode}), or 'item' (a leaf — one per visible thing, NO x/y: {kind:'item', type:'note'|'richtext'|'list'|'canvas'|'section', key?, ...type-specific fields}). gap accepts a preset 'tight'|'medium'|'spacious' or a raw pixel number (default 'medium')."),
524
515
  edges: z
525
516
  .array(z.unknown())
526
517
  .optional()
@@ -540,11 +531,46 @@ server.tool("cnotes_canvas_place", "Place items on a canvas using a DECLARATIVE
540
531
  action: "placeLayout",
541
532
  doc,
542
533
  })));
543
- server.tool("cnotes_canvas_bulk_move", "Move multiple nodes to new positions in one operation. More efficient than calling cnotes_canvas_move_node multiple times.", {
534
+ server.tool("cnotes_canvas_arrange", "Reflow EXISTING canvas nodes with the layout solver (no x/y math). Unlike cnotes_canvas_place (create-only), this MOVES nodes already on the canvas — mixed kinds welcome (note, canvas-link, richtext, list, legacy text). Prefer this over cnotes_canvas_bulk_move whenever you are reorganizing existing content into a grid/stack/section.\n\nLeaves in `doc` must be `{ kind:'item', type:'existing', nodeId:'<id or NOTE-n>', nodeType:'note'|'text'|'richtext'|'list'|'canvas' }`. Wrap them in a section container and pass `targetSectionId` (SECTION-<n>) to pack into that frame and auto-resize it in place — the SECTION-<n> id stays stable (never delete+re-add).\n\nConvenience shape for a section reflow:\n {\n targetSectionId: 'SECTION-90',\n autoResize: true,\n doc: {\n root: {\n kind: 'section', name: '<current name>', key: 'frame',\n child: {\n kind: 'grid', columns: 4, gap: 'medium',\n items: [\n { kind:'item', type:'existing', nodeId:'…', nodeType:'canvas' },\n { kind:'item', type:'existing', nodeId:'NOTE-3', nodeType:'note' }\n ]\n }\n }\n }\n }\n\nWhen targetSectionId is set and origin/placement are omitted, the server pins the layout to the frame's current origin (placement exact).", {
535
+ canvasId: z.string().describe("Canvas ID"),
536
+ doc: z
537
+ .object({
538
+ root: z.unknown().describe("Root LayoutNode. Same stack/grid/anchor/section primitives as place, but leaves must be type:'existing' (nodeId + nodeType). A section container sizes the target frame around the rearranged content."),
539
+ origin: z
540
+ .object({ x: z.number(), y: z.number() })
541
+ .optional()
542
+ .describe("Optional. Omit with targetSectionId to pin to the frame's current origin."),
543
+ placement: z
544
+ .enum(["auto", "append", "exact"])
545
+ .optional()
546
+ .describe("Defaults to exact (pinned) when targetSectionId is set and origin/placement are omitted."),
547
+ })
548
+ .passthrough()
549
+ .describe("Layout document with type:'existing' leaves."),
550
+ targetSectionId: z
551
+ .string()
552
+ .optional()
553
+ .describe("SECTION-<n> (or raw id) to pack into and auto-resize in place."),
554
+ autoResize: z
555
+ .boolean()
556
+ .optional()
557
+ .describe("Resize the target section to the solved frame bbox. Defaults true when targetSectionId is set."),
558
+ dryRun: z
559
+ .boolean()
560
+ .optional()
561
+ .describe("Solve and preview without moving anything."),
562
+ }, async ({ canvasId, doc, targetSectionId, autoResize, dryRun }) => withErrorHandling(() => api.post(`/api/canvas/${encodeURIComponent(canvasId)}?workspaceId=${encodeURIComponent(workspaceId)}`, {
563
+ action: "arrangeLayout",
564
+ doc,
565
+ ...(targetSectionId ? { targetSectionId } : {}),
566
+ ...(autoResize !== undefined ? { autoResize } : {}),
567
+ ...(dryRun ? { dryRun: true } : {}),
568
+ })));
569
+ server.tool("cnotes_canvas_bulk_move", "Move multiple nodes to new positions in one operation. More efficient than calling cnotes_canvas_move_node multiple times. Prefer cnotes_canvas_arrange when reorganizing into a grid/stack/section — that path measures cards and auto-resizes frames.", {
544
570
  canvasId: z.string().describe("Canvas ID"),
545
571
  moves: z.array(z.object({
546
572
  nodeId: z.string().describe("Canvas node ID"),
547
- nodeType: z.enum(["note", "text", "list", "canvas", "richtext", "section"]).optional().default("note").describe("Node type"),
573
+ nodeType: z.enum(["note", "text", "list", "canvas", "richtext", "section"]).optional().default("note").describe("Node type. 'text' is a LEGACY kind (old heading nodes) — still movable/deletable, but no longer creatable."),
548
574
  x: z.number().describe("New X position"),
549
575
  y: z.number().describe("New Y position"),
550
576
  })).describe("Array of node moves"),
@@ -617,10 +643,44 @@ server.tool("cnotes_canvas_update_edge", "Update an edge: change its label, togg
617
643
  body.reverse = reverse;
618
644
  return api.post(`/api/canvas/${canvasId}`, body);
619
645
  }));
646
+ server.tool("cnotes_canvas_update_section", "Resize, rename, move, or recolor an existing section frame IN PLACE, keeping its SECTION-<n> id. Use this to grow a frame around new content — never delete + re-add a section, which mints a NEW id and breaks every [[SECTION-<n>]] mention pointing at the old one. Only the fields you pass are changed.", {
647
+ canvasId: z.string().describe("Canvas ID"),
648
+ sectionId: z
649
+ .string()
650
+ .describe("Section to update — SECTION-<n> display ID or a raw node ID"),
651
+ name: z.string().optional().describe("New frame title"),
652
+ x: z.number().optional().describe("New X position (top-left)"),
653
+ y: z.number().optional().describe("New Y position (top-left)"),
654
+ width: z.number().optional().describe("New frame width in px"),
655
+ height: z.number().optional().describe("New frame height in px"),
656
+ colorHex: z
657
+ .string()
658
+ .optional()
659
+ .describe("New frame tint hex, or '' to reset to the default"),
660
+ }, async ({ canvasId, sectionId, name, x, y, width, height, colorHex }) => withErrorHandling(() => {
661
+ const body = {
662
+ action: "updateSectionNode",
663
+ nodeId: sectionId,
664
+ };
665
+ if (name !== undefined)
666
+ body.name = name;
667
+ if (x !== undefined)
668
+ body.positionX = x;
669
+ if (y !== undefined)
670
+ body.positionY = y;
671
+ if (width !== undefined)
672
+ body.width = width;
673
+ if (height !== undefined)
674
+ body.height = height;
675
+ if (colorHex !== undefined)
676
+ body.colorHex = colorHex;
677
+ // workspaceId lets the server resolve a CANVAS-<n> handle, not just a raw id.
678
+ return api.post(`/api/canvas/${encodeURIComponent(canvasId)}?workspaceId=${encodeURIComponent(workspaceId)}`, body);
679
+ }));
620
680
  server.tool("cnotes_canvas_move_node", "Move a node to a new position on a canvas.", {
621
681
  canvasId: z.string().describe("Canvas ID"),
622
682
  nodeId: z.string().describe("Canvas node ID"),
623
- nodeType: z.enum(["note", "text", "list", "canvas", "richtext", "section"]).optional().default("note").describe("Node type"),
683
+ nodeType: z.enum(["note", "text", "list", "canvas", "richtext", "section"]).optional().default("note").describe("Node type. 'text' is a LEGACY kind (old heading nodes) — still movable/deletable, but no longer creatable."),
624
684
  x: z.number().describe("New X position"),
625
685
  y: z.number().describe("New Y position"),
626
686
  }, async ({ canvasId, nodeId, nodeType, x, y }) => withErrorHandling(() => api.post(`/api/canvas/${canvasId}`, {
@@ -644,13 +704,13 @@ server.tool("cnotes_canvas_remove_edge", "Remove an edge from a canvas.", {
644
704
  action: "removeEdge",
645
705
  edgeId,
646
706
  })));
647
- server.tool("cnotes_canvas_digest", "Get the AI-generated digest of a canvas: narrative summary, key themes, and a per-note summary list covering every note on the canvas. Run this first when starting work on any canvas — it gives you full orientation in a single call, so you don't need to read each note individually. Each note entry has shape { displayId, title, type, summary }; when summary is null the AI summary hasn't generated yet. The notes list is always populated, even when digest status is 'none' or 'stale'.", {
707
+ server.tool("cnotes_canvas_digest", "Get the AI-generated digest of a canvas: narrative summary, key themes, and a per-note summary list. Run this FIRST when starting on any canvas — full orientation in one call, so you don't read each note individually. The notes list is always populated even when digest status is 'none' or 'stale' (a null per-note summary just means it hasn't generated yet).", {
648
708
  canvasId: z.string().describe("Canvas ID"),
649
709
  }, async ({ canvasId }) => withErrorHandling(() => api.get(`/api/canvas/${encodeURIComponent(canvasId)}/digest`, { workspaceId })));
650
710
  server.tool("cnotes_canvas_digest_generate", "Request digest (re)generation for a canvas. Use when digest status is 'none' or you want a fresh summary. The digest is generated async — poll cnotes_canvas_digest after ~60s.", {
651
711
  canvasId: z.string().describe("Canvas ID"),
652
712
  }, async ({ canvasId }) => withErrorHandling(() => api.post(`/api/canvas/${encodeURIComponent(canvasId)}/digest?workspaceId=${encodeURIComponent(workspaceId)}`, {})));
653
- server.tool("cnotes_canvas_activity", "Show recent activity on a canvas — edits, agent runs, gestures, restores — newest first. Use this to find out what changed and who changed it before making further edits, or to confirm a previous agent run actually applied. Returns { canvasName, operations: Array<{ _id, operationType, actorName, actorSource ('human'|'agent'|'import'|'api'), agentRunId, userPrompt, status ('in_progress'|'applied'|'reverted'|'failed'), eventCount, affectedEntityIds, summary, structuredSummary: { headline, summary, byCategory[], themes[] }, startedAt, completedAt }>}.", {
713
+ server.tool("cnotes_canvas_activity", "Show recent activity on a canvas — edits, agent runs, gestures, restores, newest first. Use it to see what changed and who changed it before further edits, or to confirm a previous agent run applied. Returns { canvasName, operations[] }, each operation carrying its type, actor (human/agent/import/api), status, prompt, summary, and timestamps.", {
654
714
  canvasId: z.string().describe("Canvas ID"),
655
715
  limit: z.number().int().min(1).max(200).optional().describe("Max entries (default 50, max 200)"),
656
716
  }, async ({ canvasId, limit }) => {
@@ -777,33 +837,6 @@ server.tool("cnotes_relationships_list", "List note relationships in the workspa
777
837
  displayId: noteDisplayId,
778
838
  })));
779
839
  // ---------------------------------------------------------------------------
780
- // Memory (Zep)
781
- // ---------------------------------------------------------------------------
782
- server.tool("cnotes_memory_query", "Search facts and entities in the knowledge graph by relevance.", {
783
- query: z.string().describe("Search query"),
784
- limit: z.number().optional().default(10).describe("Max results per category"),
785
- }, async ({ query, limit }) => withErrorHandling(() => api.post("/api/zep/query", {
786
- workspaceId,
787
- query,
788
- limit: limit ?? 10,
789
- })));
790
- server.tool("cnotes_memory_facts", "List all workspace facts from the knowledge graph.", {}, async () => withErrorHandling(async () => {
791
- const data = await api.post("/api/zep/query", {
792
- workspaceId,
793
- query: "all facts",
794
- limit: 1,
795
- });
796
- return data.allFacts;
797
- }));
798
- server.tool("cnotes_memory_entities", "List all workspace entities (people, topics, orgs) from the knowledge graph.", {}, async () => withErrorHandling(async () => {
799
- const data = await api.post("/api/zep/query", {
800
- workspaceId,
801
- query: "all entities",
802
- limit: 1,
803
- });
804
- return data.allEntities;
805
- }));
806
- // ---------------------------------------------------------------------------
807
840
  // Versions
808
841
  // ---------------------------------------------------------------------------
809
842
  server.tool("cnotes_versions_list", "List versions of a note.", {