@mentagen/mcp 0.6.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -2,6 +2,8 @@
2
2
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
3
3
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
4
4
  import { createMentagenClient } from './client/helene.js';
5
+ import { registerPrompts } from './prompts/index.js';
6
+ import { registerResources } from './resources/index.js';
5
7
  import { registerTools } from './tools/index.js';
6
8
  import { loadConfig } from './utils/config.js';
7
9
  async function main() {
@@ -13,8 +15,12 @@ async function main() {
13
15
  }, {
14
16
  capabilities: {
15
17
  tools: {},
18
+ resources: {},
19
+ prompts: {},
16
20
  },
17
21
  });
22
+ registerResources(mcpServer);
23
+ registerPrompts(mcpServer);
18
24
  registerTools(mcpServer.server, client, config);
19
25
  const transport = new StdioServerTransport();
20
26
  await mcpServer.connect(transport);
@@ -0,0 +1,259 @@
1
+ /**
2
+ * Comprehensive guide for AI agents on using Mentagen nodes effectively.
3
+ */
4
+ const NODE_USAGE_GUIDE = `# Mentagen Node Usage Guide
5
+
6
+ ## Node Types Overview
7
+
8
+ Mentagen supports several node types, each designed for specific use cases:
9
+
10
+ ### Markdown Nodes (Recommended for Most Content)
11
+ - **Type**: \`markdown\`
12
+ - **Default Color**: cyan
13
+ - **Best For**: Documentation, notes, articles, structured content
14
+ - **Features**:
15
+ - Full GitHub Flavored Markdown (GFM) support
16
+ - **Todos/Checklists**: Only markdown nodes support the todos feature
17
+ - Rich formatting with headers, lists, code blocks, tables
18
+ - **When to Use**: Default choice for most text content
19
+
20
+ ### Code Nodes
21
+ - **Type**: \`code\`
22
+ - **Default Color**: indigo
23
+ - **Best For**: Source code, scripts, configuration files
24
+ - **Features**:
25
+ - Syntax highlighting for many languages
26
+ - Monospace font optimized for code
27
+ - **No todo support** - use markdown nodes for task lists
28
+ - **When to Use**: When content is primarily source code
29
+ - **Naming Convention**: Include file extension (e.g., "utils.ts", "config.json")
30
+
31
+ ### Text Nodes
32
+ - **Type**: \`text\`
33
+ - **Default Color**: none (neutral)
34
+ - **Best For**: Simple labels, titles, short notes
35
+ - **When to Use**: Brief content that doesn't need formatting
36
+
37
+ ### URL Nodes
38
+ - **Type**: \`url\`
39
+ - **Default Color**: blue
40
+ - **Best For**: Bookmarks, external references, documentation links
41
+ - **When to Use**: Storing links to external resources
42
+
43
+ ## Todo/Checklist Support
44
+
45
+ ### CRITICAL: Use Native Todos, NOT Markdown Checkboxes
46
+
47
+ **ALWAYS use the native todo API** (\`mentagen_add_todo\`, \`mentagen_toggle_todo\`, etc.) instead of writing markdown checkboxes (\`- [ ]\`) in the content field.
48
+
49
+ **Why native todos are better:**
50
+ - Proper tracking with \`mentagen_list_todos\` showing completion stats
51
+ - Toggle completion with \`mentagen_toggle_todo\` without rewriting content
52
+ - Todos render as interactive checkboxes in the Mentagen UI
53
+ - Each todo has a unique ID for precise manipulation
54
+
55
+ **Wrong approach (don't do this):**
56
+ \`\`\`
57
+ mentagen_update_node({
58
+ content: "- [ ] Task 1\\n- [ ] Task 2" // NO! This is just text
59
+ })
60
+ \`\`\`
61
+
62
+ **Correct approach:**
63
+ \`\`\`
64
+ mentagen_add_todo({ text: "Task 1" })
65
+ mentagen_add_todo({ text: "Task 2" })
66
+ \`\`\`
67
+
68
+ ### Node Granularity
69
+
70
+ Create **separate nodes for distinct task lists** rather than one giant node:
71
+
72
+ - **Good**: "Sprint 1 Tasks" node, "Sprint 2 Tasks" node, "Backlog" node
73
+ - **Bad**: One "All Tasks" node with 50 todos
74
+
75
+ Each focused node should have:
76
+ - A clear, descriptive name (the node title)
77
+ - Related todos that belong together
78
+ - Optional markdown content for context/notes
79
+
80
+ ### Supported Node Types
81
+
82
+ **Only markdown and text nodes support todos.** Code nodes do NOT support todos.
83
+
84
+ ### Todo Operations
85
+ - \`mentagen_add_todo\`: Add individual tasks (auto-generates UUID and order)
86
+ - \`mentagen_toggle_todo\`: Mark tasks complete/incomplete
87
+ - \`mentagen_list_todos\`: View all todos with completion statistics
88
+ - \`mentagen_update_node\` with \`todos\` array: Replace all todos at once
89
+
90
+ ## Node Positioning
91
+
92
+ ### Grid System
93
+ - 1 grid unit = 16 pixels
94
+ - Positions snap to grid automatically
95
+ - Use \`mentagen_find_position\` to find non-colliding positions
96
+
97
+ ### Spacing Guidelines
98
+ - **Connected nodes** (with edges): Use gap of 6+ units (96px+)
99
+ - **Unrelated nodes**: Gap of 1 unit (16px) is sufficient
100
+ - Nodes auto-size based on their title, not content
101
+
102
+ ## Common Patterns
103
+
104
+ ### Creating a Task Board
105
+ 1. Create a markdown node for the task list
106
+ 2. Add todos using \`mentagen_add_todo\`
107
+ 3. Use \`mentagen_toggle_todo\` to track completion
108
+
109
+ ### Documentation Structure
110
+ 1. Use markdown nodes for content sections
111
+ 2. Connect related nodes with edges
112
+ 3. Use consistent colors for categories
113
+
114
+ ### Code Organization
115
+ 1. Use code nodes for source files
116
+ 2. Name nodes with file extensions
117
+ 3. Group related code with edges
118
+ `;
119
+ /**
120
+ * Guide specifically for creating and managing todo lists.
121
+ */
122
+ const TODO_GUIDE = `# Creating Checklists with Todos
123
+
124
+ ## IMPORTANT: Native Todos vs Markdown Checkboxes
125
+
126
+ **ALWAYS use native todos** via the todo API tools. Do NOT write markdown checkboxes in content.
127
+
128
+ | Approach | Method | Result |
129
+ |----------|--------|--------|
130
+ | **CORRECT** | \`mentagen_add_todo({ text: "Task" })\` | Interactive checkbox, trackable |
131
+ | **WRONG** | \`content: "- [ ] Task"\` | Just text, not trackable |
132
+
133
+ Native todos provide:
134
+ - Completion tracking via \`mentagen_list_todos\`
135
+ - Toggle without rewriting content
136
+ - Interactive checkboxes in UI
137
+ - Unique IDs for each todo
138
+
139
+ ## Node Granularity Best Practices
140
+
141
+ Create **focused nodes for related tasks**, not one giant task dump:
142
+
143
+ **Good structure:**
144
+ - "Feature A - Tasks" (5 todos)
145
+ - "Feature B - Tasks" (3 todos)
146
+ - "Bug Fixes" (4 todos)
147
+
148
+ **Bad structure:**
149
+ - "All Project Tasks" (50 todos in one node)
150
+
151
+ Each task node should:
152
+ - Have a clear, descriptive title
153
+ - Contain only related todos
154
+ - Optionally include context in the markdown content field
155
+
156
+ ## Quick Start
157
+
158
+ ### Option A: Create node with initial todos (recommended for bulk)
159
+
160
+ \`\`\`
161
+ mentagen_create_node({
162
+ boardId: "...",
163
+ name: "Sprint 1 Tasks",
164
+ type: "markdown",
165
+ content: "Tasks for the first sprint milestone.",
166
+ todos: [
167
+ { text: "Implement auth" },
168
+ { text: "Write tests" },
169
+ { text: "Update docs" }
170
+ ]
171
+ })
172
+ \`\`\`
173
+
174
+ Note: Just provide "text" for each todo. The id and order are auto-generated.
175
+
176
+ ### Option B: Create node then add todos one by one
177
+
178
+ 1. **Create a markdown node** for your task list:
179
+ \`\`\`
180
+ mentagen_create_node({
181
+ boardId: "...",
182
+ name: "Sprint 1 Tasks",
183
+ type: "markdown",
184
+ content: "Tasks for the first sprint milestone."
185
+ })
186
+ \`\`\`
187
+
188
+ 2. **Add todos using the API** (not markdown syntax):
189
+ \`\`\`
190
+ mentagen_add_todo({ boardId: "...", nodeId: "...", text: "Implement auth" })
191
+ mentagen_add_todo({ boardId: "...", nodeId: "...", text: "Write tests" })
192
+ mentagen_add_todo({ boardId: "...", nodeId: "...", text: "Update docs" })
193
+ \`\`\`
194
+
195
+ 3. **Check progress**:
196
+ \`\`\`
197
+ mentagen_list_todos({ boardId: "...", nodeId: "..." })
198
+ \`\`\`
199
+ Returns: \`{ summary: { total: 3, completed: 0, remaining: 3 } }\`
200
+
201
+ 4. **Mark complete**:
202
+ \`\`\`
203
+ mentagen_toggle_todo({ boardId: "...", nodeId: "...", todoId: "..." })
204
+ \`\`\`
205
+
206
+ ## Supported Node Types
207
+
208
+ - **Markdown nodes**: Full todo support ✓
209
+ - **Text nodes**: Full todo support ✓
210
+ - **Code nodes**: NO todo support ✗
211
+
212
+ ## Bulk Operations
213
+
214
+ To set all todos at once (useful for importing):
215
+ \`\`\`
216
+ mentagen_update_node({
217
+ boardId: "...",
218
+ nodeId: "...",
219
+ todos: [
220
+ { id: "uuid-1", text: "Task 1", completed: false, order: 0 },
221
+ { id: "uuid-2", text: "Task 2", completed: true, order: 1 }
222
+ ]
223
+ })
224
+ \`\`\`
225
+ `;
226
+ export function registerPrompts(mcpServer) {
227
+ // Enable prompts capability
228
+ mcpServer.server.registerCapabilities({
229
+ prompts: {
230
+ listChanged: true,
231
+ },
232
+ });
233
+ mcpServer.registerPrompt('node-usage-guide', {
234
+ description: 'Comprehensive guide for using Mentagen nodes effectively, including node types, todos, and best practices.',
235
+ }, async () => ({
236
+ messages: [
237
+ {
238
+ role: 'user',
239
+ content: {
240
+ type: 'text',
241
+ text: NODE_USAGE_GUIDE,
242
+ },
243
+ },
244
+ ],
245
+ }));
246
+ mcpServer.registerPrompt('todo-checklist-guide', {
247
+ description: 'Step-by-step guide for creating and managing todo lists/checklists on Mentagen nodes.',
248
+ }, async () => ({
249
+ messages: [
250
+ {
251
+ role: 'user',
252
+ content: {
253
+ type: 'text',
254
+ text: TODO_GUIDE,
255
+ },
256
+ },
257
+ ],
258
+ }));
259
+ }
@@ -0,0 +1,93 @@
1
+ const NODE_COLORS = [
2
+ { name: 'amber', rgb: 'rgb(253, 230, 138)' },
3
+ { name: 'blue', rgb: 'rgb(0, 120, 255)' },
4
+ { name: 'cyan', rgb: 'rgb(0, 231, 255)' },
5
+ { name: 'emerald', rgb: 'rgb(0, 230, 118)' },
6
+ { name: 'fuchsia', rgb: 'rgb(233, 30, 99)' },
7
+ { name: 'green', rgb: 'rgb(52, 211, 153)' },
8
+ { name: 'indigo', rgb: 'rgb(72, 77, 201)' },
9
+ { name: 'lime', rgb: 'rgb(209, 250, 229)' },
10
+ { name: 'orange', rgb: 'rgb(252, 165, 0)' },
11
+ { name: 'pink', rgb: 'rgb(244, 143, 177)' },
12
+ { name: 'purple', rgb: 'rgb(156, 39, 176)' },
13
+ { name: 'red', rgb: 'rgb(239, 68, 68)' },
14
+ { name: 'rose', rgb: 'rgb(255, 204, 203)' },
15
+ { name: 'sky', rgb: 'rgb(0, 184, 230)' },
16
+ { name: 'slate', rgb: 'rgb(107, 114, 128)' },
17
+ { name: 'teal', rgb: 'rgb(0, 210, 183)' },
18
+ { name: 'violet', rgb: 'rgb(123, 31, 162)' },
19
+ { name: 'yellow', rgb: 'rgb(255, 251, 235)' },
20
+ ];
21
+ /**
22
+ * Supported node types with their default colors.
23
+ *
24
+ * Keep in sync with: src/client/features/boards/inner-board-context-menu.tsx
25
+ */
26
+ const NODE_TYPES = [
27
+ {
28
+ type: 'text',
29
+ defaultColor: null,
30
+ description: 'Simple text node with a title and optional rich text content.',
31
+ usage: 'Use for short notes, labels, headings, or any content that needs a title.',
32
+ fields: {
33
+ name: 'The node title (required)',
34
+ content: 'Optional HTML content',
35
+ },
36
+ },
37
+ {
38
+ type: 'markdown',
39
+ defaultColor: 'cyan',
40
+ description: 'Markdown node for longer-form content with full Markdown syntax support.',
41
+ usage: 'Use for documentation, articles, formatted notes, or any content that benefits from Markdown formatting.',
42
+ fields: {
43
+ name: 'The node title (required)',
44
+ content: 'Markdown content (supports full GFM syntax)',
45
+ },
46
+ },
47
+ {
48
+ type: 'code',
49
+ defaultColor: 'indigo',
50
+ description: 'Code node for storing and displaying source code with syntax highlighting.',
51
+ usage: 'Use for code snippets, scripts, configuration files. Supports multiple programming languages.',
52
+ fields: {
53
+ name: 'The node title (required)',
54
+ content: 'Source code content',
55
+ },
56
+ },
57
+ {
58
+ type: 'url',
59
+ defaultColor: 'blue',
60
+ description: 'Bookmark node that stores and displays a link to an external URL.',
61
+ usage: 'Use for saving references to external resources, articles, documentation, or any web link.',
62
+ fields: {
63
+ name: 'The link title (required)',
64
+ url: 'The full URL including protocol (e.g., https://example.com)',
65
+ },
66
+ },
67
+ ];
68
+ export function registerResources(mcpServer) {
69
+ mcpServer.resource('Node Colors', 'mentagen://colors', {
70
+ description: 'Available colors for nodes. Use the color name when creating or updating nodes.',
71
+ mimeType: 'application/json',
72
+ }, async () => ({
73
+ contents: [
74
+ {
75
+ uri: 'mentagen://colors',
76
+ mimeType: 'application/json',
77
+ text: JSON.stringify({ colors: NODE_COLORS }, null, 2),
78
+ },
79
+ ],
80
+ }));
81
+ mcpServer.resource('Node Types', 'mentagen://node-types', {
82
+ description: 'Supported node types with descriptions and usage guidance. Specify the type parameter when creating nodes.',
83
+ mimeType: 'application/json',
84
+ }, async () => ({
85
+ contents: [
86
+ {
87
+ uri: 'mentagen://node-types',
88
+ mimeType: 'application/json',
89
+ text: JSON.stringify({ nodeTypes: NODE_TYPES }, null, 2),
90
+ },
91
+ ],
92
+ }));
93
+ }
@@ -2,15 +2,32 @@ import { z } from 'zod';
2
2
  import { formatError } from '../utils/errors.js';
3
3
  import { unitsToPixels } from '../utils/units.js';
4
4
  import { snapToGrid } from './grid-calc.js';
5
+ import { validateTypeChange } from './update.js';
6
+ const todoItemSchema = z.object({
7
+ id: z.string().describe('Unique identifier (UUID v4)'),
8
+ text: z.string().describe('Todo text content'),
9
+ completed: z.boolean().describe('Completion status'),
10
+ order: z.number().describe('Sort order (0-based)'),
11
+ });
5
12
  const nodeUpdateSchema = z.object({
6
13
  nodeId: z.string().describe('The node ID to update'),
14
+ type: z
15
+ .enum(['markdown', 'code', 'url'])
16
+ .optional()
17
+ .describe('Change node type. Safe conversions: markdown ↔ code. Content is auto-migrated.'),
7
18
  name: z.string().optional().describe('New name for the node'),
8
19
  content: z.string().optional().describe('New content for the node'),
20
+ code: z.string().optional().describe('Code content for code-type nodes'),
21
+ url: z.string().optional().describe('URL for url-type nodes'),
9
22
  color: z.string().optional().describe('Node color'),
10
23
  x: z.number().optional().describe('X position in grid units'),
11
24
  y: z.number().optional().describe('Y position in grid units'),
12
25
  width: z.number().optional().describe('Node width in grid units'),
13
26
  height: z.number().optional().describe('Node height in grid units'),
27
+ todos: z
28
+ .array(todoItemSchema)
29
+ .optional()
30
+ .describe('Replace all todos on this markdown node. Only markdown/text nodes support todos.'),
14
31
  });
15
32
  const edgeUpdateSchema = z.object({
16
33
  edgeId: z.string().describe('The edge ID to update'),
@@ -25,6 +42,79 @@ export const batchUpdateSchema = z.object({
25
42
  nodes: z.array(nodeUpdateSchema).optional().describe('Array of node updates'),
26
43
  edges: z.array(edgeUpdateSchema).optional().describe('Array of edge updates'),
27
44
  });
45
+ const POSITION_FIELDS = ['x', 'y', 'width', 'height'];
46
+ /**
47
+ * Process type change and return migration fields or error.
48
+ */
49
+ async function processNodeTypeChange(client, boardId, nodeId, targetType, data) {
50
+ try {
51
+ const currentNode = await client.getNode({ boardId, nodeId });
52
+ if (targetType === currentNode.type) {
53
+ return { success: true, fields: {} };
54
+ }
55
+ const result = validateTypeChange(currentNode.type, targetType, currentNode, {
56
+ ...data,
57
+ type: targetType,
58
+ boardId,
59
+ nodeId,
60
+ autoSize: true,
61
+ });
62
+ if (!result.valid) {
63
+ return { success: false, error: result.error };
64
+ }
65
+ return { success: true, fields: result.fields };
66
+ }
67
+ catch (error) {
68
+ return { success: false, error: formatError(error) };
69
+ }
70
+ }
71
+ /**
72
+ * Convert data fields to update format with pixel conversion.
73
+ */
74
+ function buildNodeUpdateData(data, migrationFields) {
75
+ const cleanData = {
76
+ ...migrationFields,
77
+ };
78
+ for (const [key, value] of Object.entries(data)) {
79
+ if (value === undefined)
80
+ continue;
81
+ if (POSITION_FIELDS.includes(key)) {
82
+ cleanData[key] = snapToGrid(unitsToPixels(value));
83
+ }
84
+ else if (key === 'todos') {
85
+ cleanData[key] = value;
86
+ }
87
+ else {
88
+ cleanData[key] = value;
89
+ }
90
+ }
91
+ return cleanData;
92
+ }
93
+ /**
94
+ * Process a single node update.
95
+ */
96
+ async function processNodeUpdate(client, boardId, update) {
97
+ const { nodeId, type: targetType, ...data } = update;
98
+ let migrationFields = {};
99
+ if (targetType !== undefined) {
100
+ const typeResult = await processNodeTypeChange(client, boardId, nodeId, targetType, data);
101
+ if (!typeResult.success) {
102
+ return { id: nodeId, success: false, error: typeResult.error };
103
+ }
104
+ migrationFields = typeResult.fields;
105
+ }
106
+ const cleanData = buildNodeUpdateData(data, migrationFields);
107
+ if (Object.keys(cleanData).length === 0) {
108
+ return { id: nodeId, success: false, error: 'No fields to update' };
109
+ }
110
+ try {
111
+ await client.updateNode({ boardId, nodeId, data: cleanData });
112
+ return { id: nodeId, success: true };
113
+ }
114
+ catch (error) {
115
+ return { id: nodeId, success: false, error: formatError(error) };
116
+ }
117
+ }
28
118
  export async function handleBatchUpdate(client, params) {
29
119
  const nodeUpdates = params.nodes ?? [];
30
120
  const edgeUpdates = params.edges ?? [];
@@ -43,36 +133,7 @@ export async function handleBatchUpdate(client, params) {
43
133
  nodes: [],
44
134
  edges: [],
45
135
  };
46
- // Process node updates in parallel
47
- const nodePromises = nodeUpdates.map(async (update) => {
48
- const { nodeId, ...data } = update;
49
- // Filter out undefined values and convert position fields from units to pixels
50
- const cleanData = {};
51
- for (const [key, value] of Object.entries(data)) {
52
- if (value !== undefined) {
53
- if (key === 'x' || key === 'y' || key === 'width' || key === 'height') {
54
- cleanData[key] = snapToGrid(unitsToPixels(value));
55
- }
56
- else {
57
- cleanData[key] = value;
58
- }
59
- }
60
- }
61
- if (Object.keys(cleanData).length === 0) {
62
- return { id: nodeId, success: false, error: 'No fields to update' };
63
- }
64
- try {
65
- await client.updateNode({
66
- boardId: params.boardId,
67
- nodeId,
68
- data: cleanData,
69
- });
70
- return { id: nodeId, success: true };
71
- }
72
- catch (error) {
73
- return { id: nodeId, success: false, error: formatError(error) };
74
- }
75
- });
136
+ const nodePromises = nodeUpdates.map(update => processNodeUpdate(client, params.boardId, update));
76
137
  // Process edge updates in parallel
77
138
  const edgePromises = edgeUpdates.map(async (update) => {
78
139
  const { edgeId, direction, label } = update;
@@ -1,9 +1,17 @@
1
1
  import { z } from 'zod';
2
2
  import { formatError } from '../utils/errors.js';
3
3
  import { pixelsToUnits, unitsToPixels } from '../utils/units.js';
4
- import { applyExtension, generateId } from './create.js';
4
+ import { applyExtension, generateId, normalizeTodos } from './create.js';
5
5
  import { snapToGrid } from './grid-calc.js';
6
6
  import { calculateNodeSize } from './size-calc.js';
7
+ const todoInputSchema = z.object({
8
+ text: z.string().describe('Todo text content'),
9
+ completed: z
10
+ .boolean()
11
+ .optional()
12
+ .default(false)
13
+ .describe('Completion status (default: false)'),
14
+ });
7
15
  const nodeInputSchema = z.object({
8
16
  tempId: z.string().describe('Temporary ID for referencing in edges'),
9
17
  name: z.string().describe('The node title/name'),
@@ -13,15 +21,19 @@ const nodeInputSchema = z.object({
13
21
  .describe('File extension to append to name (e.g., ".ts", ".py"). Only applies to code nodes. Defaults to .js.'),
14
22
  content: z.string().optional().describe('Node content'),
15
23
  type: z
16
- .enum(['text', 'markdown', 'code', 'url'])
24
+ .enum(['markdown', 'code', 'url'])
17
25
  .optional()
18
- .default('text')
26
+ .default('markdown')
19
27
  .describe('Node type'),
20
28
  color: z.string().optional().describe('Node color'),
21
29
  x: z.number().optional().describe('X position in grid units'),
22
30
  y: z.number().optional().describe('Y position in grid units'),
23
31
  width: z.number().optional().describe('Width in grid units'),
24
32
  height: z.number().optional().describe('Height in grid units'),
33
+ todos: z
34
+ .array(todoInputSchema)
35
+ .optional()
36
+ .describe('Initial todos for the node. Only supported on markdown/text nodes. Only text is required; id and order are auto-generated.'),
25
37
  });
26
38
  const edgeInputSchema = z.object({
27
39
  source: z.string().describe('Source node tempId'),
@@ -46,24 +58,32 @@ const DEFAULT_NODE_COLORS = {
46
58
  code: 'indigo',
47
59
  url: 'blue',
48
60
  };
61
+ /** Calculate snapped pixel dimensions for a node. */
62
+ function calcNodeDimensions(nodeInput, autoSize) {
63
+ const DEFAULT_POS = 6;
64
+ return {
65
+ x: snapToGrid(unitsToPixels(nodeInput.x ?? DEFAULT_POS)),
66
+ y: snapToGrid(unitsToPixels(nodeInput.y ?? DEFAULT_POS)),
67
+ width: snapToGrid(nodeInput.width !== undefined
68
+ ? unitsToPixels(nodeInput.width)
69
+ : autoSize.width),
70
+ height: snapToGrid(nodeInput.height !== undefined
71
+ ? unitsToPixels(nodeInput.height)
72
+ : autoSize.height),
73
+ };
74
+ }
49
75
  async function createNode(client, boardId, nodeInput, idMap) {
50
76
  const realId = generateId();
51
77
  const type = nodeInput.type ?? 'text';
52
78
  const finalName = applyExtension(nodeInput.name, type, nodeInput.extension);
53
79
  const color = nodeInput.color ?? DEFAULT_NODE_COLORS[type];
54
80
  const autoSize = calculateNodeSize(finalName, type);
55
- const xPixels = snapToGrid(nodeInput.x !== undefined ? unitsToPixels(nodeInput.x) : unitsToPixels(6));
56
- const yPixels = snapToGrid(nodeInput.y !== undefined ? unitsToPixels(nodeInput.y) : unitsToPixels(6));
57
- const widthPixels = snapToGrid(nodeInput.width !== undefined
58
- ? unitsToPixels(nodeInput.width)
59
- : autoSize.width);
60
- const heightPixels = snapToGrid(nodeInput.height !== undefined
61
- ? unitsToPixels(nodeInput.height)
62
- : autoSize.height);
81
+ const dims = calcNodeDimensions(nodeInput, autoSize);
82
+ const isCodeNode = type === 'code';
83
+ const nodeContent = nodeInput.content ?? '';
84
+ // Only pass todos for non-code nodes
85
+ const todos = !isCodeNode && nodeInput.todos ? normalizeTodos(nodeInput.todos) : undefined;
63
86
  try {
64
- // For code nodes, store content in the `code` field
65
- const isCodeNode = type === 'code';
66
- const nodeContent = nodeInput.content ?? '';
67
87
  const node = await client.addNode({
68
88
  _id: realId,
69
89
  boardId,
@@ -72,10 +92,11 @@ async function createNode(client, boardId, nodeInput, idMap) {
72
92
  ...(isCodeNode ? { code: nodeContent } : { content: nodeContent }),
73
93
  type,
74
94
  color,
75
- x: xPixels,
76
- y: yPixels,
77
- width: widthPixels,
78
- height: heightPixels,
95
+ x: dims.x,
96
+ y: dims.y,
97
+ width: dims.width,
98
+ height: dims.height,
99
+ ...(todos && { todos }),
79
100
  },
80
101
  });
81
102
  idMap[nodeInput.tempId] = node._id;
@@ -85,10 +106,10 @@ async function createNode(client, boardId, nodeInput, idMap) {
85
106
  tempId: nodeInput.tempId,
86
107
  id: node._id,
87
108
  name: node.name,
88
- x: pixelsToUnits(xPixels),
89
- y: pixelsToUnits(yPixels),
90
- width: pixelsToUnits(widthPixels),
91
- height: pixelsToUnits(heightPixels),
109
+ x: pixelsToUnits(dims.x),
110
+ y: pixelsToUnits(dims.y),
111
+ width: pixelsToUnits(dims.width),
112
+ height: pixelsToUnits(dims.height),
92
113
  },
93
114
  };
94
115
  }