@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 +6 -0
- package/dist/prompts/index.js +259 -0
- package/dist/resources/index.js +93 -0
- package/dist/tools/batch-update.js +91 -30
- package/dist/tools/create-graph.js +43 -22
- package/dist/tools/create.js +36 -5
- package/dist/tools/index.js +112 -63
- package/dist/tools/size-calc.js +1 -1
- package/dist/tools/todo.js +171 -0
- package/dist/tools/update.js +133 -18
- package/package.json +2 -2
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
|
-
|
|
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(['
|
|
24
|
+
.enum(['markdown', 'code', 'url'])
|
|
17
25
|
.optional()
|
|
18
|
-
.default('
|
|
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
|
|
56
|
-
const
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
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:
|
|
76
|
-
y:
|
|
77
|
-
width:
|
|
78
|
-
height:
|
|
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(
|
|
89
|
-
y: pixelsToUnits(
|
|
90
|
-
width: pixelsToUnits(
|
|
91
|
-
height: pixelsToUnits(
|
|
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
|
}
|