slyplan-mcp 1.5.2 → 1.6.3
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/commands/sly/sort-nodes.md +129 -0
- package/commands/sly/sort-tasks.md +90 -0
- package/dist/cli.js +150 -42
- package/dist/db.d.ts +2 -2
- package/dist/db.js +26 -13
- package/dist/index.js +21 -5
- package/dist/supabase.js +34 -2
- package/package.json +3 -2
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Reorganize and sort all nodes in the project tree for clean structure
|
|
3
|
+
argument-hint: "[project-name]"
|
|
4
|
+
allowed-tools:
|
|
5
|
+
- mcp__slyplan__list_projects
|
|
6
|
+
- mcp__slyplan__set_project
|
|
7
|
+
- mcp__slyplan__get_tree
|
|
8
|
+
- mcp__slyplan__get_node
|
|
9
|
+
- mcp__slyplan__update_node
|
|
10
|
+
- mcp__slyplan__delete_node
|
|
11
|
+
- mcp__slyplan__move_node
|
|
12
|
+
- mcp__slyplan__add_node
|
|
13
|
+
- mcp__slyplan__search
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
You are a tree reorganization agent for SlyPlan. Your job is to analyze the entire project tree for "$ARGUMENTS" and reorganize nodes so the structure is logical, clean, and well-grouped. Follow each step in order.
|
|
17
|
+
|
|
18
|
+
Initialize these counters at the start:
|
|
19
|
+
- moved = 0
|
|
20
|
+
- merged = 0
|
|
21
|
+
- created = 0
|
|
22
|
+
- deleted = 0
|
|
23
|
+
|
|
24
|
+
## Step 1: Resolve Project
|
|
25
|
+
|
|
26
|
+
1. Call `list_projects` to get all available projects.
|
|
27
|
+
2. Find the project whose name best matches "$ARGUMENTS" (case-insensitive, fuzzy — partial match is acceptable).
|
|
28
|
+
3. If no reasonable match is found, output: **"No project found matching '$ARGUMENTS'"** and stop.
|
|
29
|
+
4. Call `set_project` with the matched project's ID.
|
|
30
|
+
|
|
31
|
+
## Step 2: Fetch and Analyze Tree
|
|
32
|
+
|
|
33
|
+
1. Call `get_tree` **once** to get the complete project hierarchy.
|
|
34
|
+
2. Analyze the entire structure in your working memory.
|
|
35
|
+
3. Identify problems:
|
|
36
|
+
- **Misplaced nodes:** Plans that should be under a different phase/category.
|
|
37
|
+
- **Duplicates:** Nodes with very similar titles that should be merged.
|
|
38
|
+
- **Orphaned nodes:** Nodes that don't logically belong where they are.
|
|
39
|
+
- **Missing groups:** Related nodes scattered across the tree that should be grouped.
|
|
40
|
+
- **Wrong types:** Nodes whose type doesn't match their role (e.g., a plan that's really a phase).
|
|
41
|
+
- **Empty containers:** Categories or phases with no children.
|
|
42
|
+
4. Plan ALL changes before executing any MCP calls.
|
|
43
|
+
|
|
44
|
+
## Step 3: Fix Hierarchy
|
|
45
|
+
|
|
46
|
+
The expected hierarchy is: **project > category > phase > plan**
|
|
47
|
+
|
|
48
|
+
Same-type nesting is allowed (category under category, phase under phase) for logical grouping.
|
|
49
|
+
|
|
50
|
+
Fix violations:
|
|
51
|
+
- **Plan directly under project:** Wrap in appropriate category > phase.
|
|
52
|
+
- **Plan directly under category:** Wrap in appropriate phase.
|
|
53
|
+
- **Phase directly under project:** Wrap in appropriate category.
|
|
54
|
+
|
|
55
|
+
Group multiple violations under shared wrappers where logical.
|
|
56
|
+
|
|
57
|
+
## Step 4: Group Related Nodes
|
|
58
|
+
|
|
59
|
+
Look for nodes that logically belong together but are scattered:
|
|
60
|
+
|
|
61
|
+
1. **Identify clusters** of related nodes (by title, description, or domain).
|
|
62
|
+
2. **Create grouping nodes** (categories or phases) to hold them if none exist.
|
|
63
|
+
3. **Move nodes** into their logical groups using `move_node`.
|
|
64
|
+
4. Choose grouping names that are clear and descriptive.
|
|
65
|
+
|
|
66
|
+
## Step 5: Merge Duplicates
|
|
67
|
+
|
|
68
|
+
For sibling nodes with very similar titles:
|
|
69
|
+
|
|
70
|
+
1. **Exact match** (case-insensitive): definite duplicate.
|
|
71
|
+
2. **One title contains the other**: likely duplicate.
|
|
72
|
+
3. **Same key words in different order**: probable duplicate.
|
|
73
|
+
|
|
74
|
+
When merging A into B (B survives — pick the one with more children/progress):
|
|
75
|
+
1. Move ALL children of A under B using `move_node`.
|
|
76
|
+
2. Combine descriptions if A has unique content.
|
|
77
|
+
3. Set B's status to the more advanced of the two.
|
|
78
|
+
4. Set B's progress to `max(A.progress, B.progress)`.
|
|
79
|
+
5. Delete A (now childless) using `delete_node`.
|
|
80
|
+
|
|
81
|
+
When in doubt, do NOT merge.
|
|
82
|
+
|
|
83
|
+
## Step 6: Cleanup
|
|
84
|
+
|
|
85
|
+
1. Delete categories and phases with zero children.
|
|
86
|
+
2. Flatten single-child wrappers where the wrapper title matches the child.
|
|
87
|
+
3. Do NOT delete project nodes.
|
|
88
|
+
|
|
89
|
+
## CRITICAL SAFETY RULE
|
|
90
|
+
|
|
91
|
+
**Before calling `delete_node` on ANY node, you MUST first `move_node` ALL of its children to their new parent.**
|
|
92
|
+
|
|
93
|
+
The database uses `ON DELETE CASCADE` on `parent_id`. Deleting a parent node **permanently destroys ALL descendants**. There is no undo.
|
|
94
|
+
|
|
95
|
+
## Behavioral Rules
|
|
96
|
+
|
|
97
|
+
- Do NOT rewrite existing node titles unless merging duplicates or fixing obvious typos.
|
|
98
|
+
- Do NOT change node statuses or progress unless merging.
|
|
99
|
+
- Fetch `get_tree` only ONCE (Step 2). Do not call it again.
|
|
100
|
+
- Plan all changes mentally before executing any MCP calls.
|
|
101
|
+
- Execute in order: hierarchy fixes → grouping → merging → cleanup.
|
|
102
|
+
- Process silently — no verbose progress commentary during execution.
|
|
103
|
+
|
|
104
|
+
## Step 7: Report
|
|
105
|
+
|
|
106
|
+
After all operations, output a clean summary:
|
|
107
|
+
|
|
108
|
+
```
|
|
109
|
+
Sort complete for "[Project Name]"
|
|
110
|
+
|
|
111
|
+
Moved: X nodes
|
|
112
|
+
Merged: Y duplicate nodes
|
|
113
|
+
Created: Z grouping nodes
|
|
114
|
+
Deleted: W empty containers
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Then show the key changes:
|
|
118
|
+
```
|
|
119
|
+
Changes:
|
|
120
|
+
- Moved "Node" from "Old Parent" → "New Parent"
|
|
121
|
+
- Merged "Duplicate" into "Survivor"
|
|
122
|
+
- Created "New Category" to group related nodes
|
|
123
|
+
- Deleted empty "Old Phase"
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
If nothing was changed, output:
|
|
127
|
+
```
|
|
128
|
+
No changes needed for "[Project Name]" — tree structure is already clean.
|
|
129
|
+
```
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Sort "Sort Later" tasks into the correct places in the project tree
|
|
3
|
+
argument-hint: "[project-name]"
|
|
4
|
+
allowed-tools:
|
|
5
|
+
- mcp__slyplan__list_projects
|
|
6
|
+
- mcp__slyplan__set_project
|
|
7
|
+
- mcp__slyplan__get_tree
|
|
8
|
+
- mcp__slyplan__get_node
|
|
9
|
+
- mcp__slyplan__update_node
|
|
10
|
+
- mcp__slyplan__delete_node
|
|
11
|
+
- mcp__slyplan__move_node
|
|
12
|
+
- mcp__slyplan__add_node
|
|
13
|
+
- mcp__slyplan__search
|
|
14
|
+
- mcp__supabase__execute_sql
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
You are a task sorting agent for SlyPlan. Your job is to take all unsorted tasks from the "Sort Later" list (stored in project metadata) and create them as proper nodes in the correct places in the project tree for "$ARGUMENTS". Follow each step in order.
|
|
18
|
+
|
|
19
|
+
## Step 1: Resolve Project
|
|
20
|
+
|
|
21
|
+
1. Call `list_projects` to get all available projects.
|
|
22
|
+
2. Find the project whose name best matches "$ARGUMENTS" (case-insensitive, fuzzy — partial match is acceptable).
|
|
23
|
+
3. If no reasonable match is found, output: **"No project found matching '$ARGUMENTS'"** and stop.
|
|
24
|
+
4. Call `set_project` with the matched project's ID.
|
|
25
|
+
|
|
26
|
+
## Step 2: Find Sort Later Tasks
|
|
27
|
+
|
|
28
|
+
1. Call `get_node` on the project to read its metadata.
|
|
29
|
+
2. Look for `metadata.sortLaterTasks` — this is an array of tasks with `{ id, title, description, children: [{ id, title, description }] }`.
|
|
30
|
+
3. If the array is empty or doesn't exist, output: **"No Sort Later tasks found — nothing to sort."** and stop.
|
|
31
|
+
4. Note down all the tasks — their titles, descriptions, and children.
|
|
32
|
+
|
|
33
|
+
## Step 3: Understand the Tree
|
|
34
|
+
|
|
35
|
+
1. Call `get_tree` **once** to get the complete project hierarchy.
|
|
36
|
+
2. Analyze the existing structure: categories, phases, and plans.
|
|
37
|
+
3. For each Sort Later task, determine the best placement:
|
|
38
|
+
- Search for existing categories and phases that logically match the task.
|
|
39
|
+
- Consider the task's title and description to understand its intent.
|
|
40
|
+
4. Plan ALL placements before executing any.
|
|
41
|
+
|
|
42
|
+
## Step 4: Create Nodes for Each Task
|
|
43
|
+
|
|
44
|
+
For each Sort Later task:
|
|
45
|
+
|
|
46
|
+
1. **If an existing category > phase fits:** Create the task as a `plan` node under that phase using `add_node`.
|
|
47
|
+
2. **If a category exists but no fitting phase:** Create a new `phase` with `add_node`, then create the task under it.
|
|
48
|
+
3. **If no relevant category exists:** Create a new `category` with `add_node`, then a `phase` under it, then the task.
|
|
49
|
+
4. **If a task could itself be a category or phase:** Create it with that type and restructure accordingly.
|
|
50
|
+
5. **If a task has children:** Create the parent first, then create each child as a `plan` node under it.
|
|
51
|
+
6. **If a task makes absolutely no sense:** Ask the user what they want to do with it before creating or discarding it.
|
|
52
|
+
|
|
53
|
+
Use your intelligence to group things logically. Related tasks should be near each other.
|
|
54
|
+
|
|
55
|
+
## Step 5: Clear Sort Later List
|
|
56
|
+
|
|
57
|
+
After all tasks are created as nodes, clear the Sort Later list from project metadata:
|
|
58
|
+
|
|
59
|
+
Use `execute_sql` to clear the sortLaterTasks from the project metadata:
|
|
60
|
+
```sql
|
|
61
|
+
UPDATE nodes SET metadata = metadata - 'sortLaterTasks' WHERE id = '<project-id>';
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Behavioral Rules
|
|
65
|
+
|
|
66
|
+
- Do NOT modify existing nodes — only create new ones for the Sort Later tasks.
|
|
67
|
+
- Ask the user about ambiguous tasks rather than guessing wrong.
|
|
68
|
+
- Fetch `get_tree` only ONCE (Step 3). Work from the snapshot.
|
|
69
|
+
- Plan all changes before executing MCP calls.
|
|
70
|
+
- Process silently — no verbose progress commentary during execution.
|
|
71
|
+
|
|
72
|
+
## Step 6: Report
|
|
73
|
+
|
|
74
|
+
After all operations, output a clean summary:
|
|
75
|
+
|
|
76
|
+
```
|
|
77
|
+
Sort complete for "[Project Name]"
|
|
78
|
+
|
|
79
|
+
Sorted: X tasks
|
|
80
|
+
Created: Y new categories/phases
|
|
81
|
+
Skipped: Z tasks (asked user)
|
|
82
|
+
Sort Later list: [cleared | X tasks remaining]
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Then list where each task was placed:
|
|
86
|
+
```
|
|
87
|
+
- "Task title" → Category > Phase
|
|
88
|
+
- "Task title" → Category > Phase (new)
|
|
89
|
+
- "Task title" → Skipped (asked user)
|
|
90
|
+
```
|
package/dist/cli.js
CHANGED
|
@@ -49,7 +49,9 @@ process.stdin.on('end', () => {
|
|
|
49
49
|
if (block.type !== 'tool_use') continue;
|
|
50
50
|
const name = block.name || '';
|
|
51
51
|
if (name.includes('set_project')) hasSetProject = true;
|
|
52
|
+
// Track add/remove — last action wins
|
|
52
53
|
if (name.includes('add_to_work_mode')) hasWorkModeNode = true;
|
|
54
|
+
if (name.includes('remove_from_work_mode')) hasWorkModeNode = false;
|
|
53
55
|
}
|
|
54
56
|
}
|
|
55
57
|
} catch {}
|
|
@@ -72,7 +74,7 @@ process.stdin.on('end', () => {
|
|
|
72
74
|
suppressOutput: true,
|
|
73
75
|
hookSpecificOutput: {
|
|
74
76
|
hookEventName: "PreToolCall",
|
|
75
|
-
additionalContext: "
|
|
77
|
+
additionalContext: "BLOCKED: No node in work mode. Call search + add_to_work_mode before doing any work. This is not optional."
|
|
76
78
|
}
|
|
77
79
|
});
|
|
78
80
|
process.stdout.write(output);
|
|
@@ -85,11 +87,13 @@ process.stdin.on('end', () => {
|
|
|
85
87
|
}
|
|
86
88
|
});
|
|
87
89
|
`;
|
|
88
|
-
// Smart transcript-aware hook: counts file changes
|
|
89
|
-
// Silent when synced
|
|
90
|
+
// Smart transcript-aware hook: counts file changes SINCE last SlyPlan sync.
|
|
91
|
+
// Silent when synced. Fires after 3+ unsynced file changes or a git commit.
|
|
90
92
|
const POST_HOOK_FILE_CONTENT = `#!/usr/bin/env node
|
|
91
93
|
const fs = require('fs');
|
|
92
94
|
|
|
95
|
+
const BATCH_THRESHOLD = 3;
|
|
96
|
+
|
|
93
97
|
let input = '';
|
|
94
98
|
process.stdin.setEncoding('utf8');
|
|
95
99
|
process.stdin.on('data', (chunk) => { input += chunk; });
|
|
@@ -108,9 +112,9 @@ process.stdin.on('end', () => {
|
|
|
108
112
|
const FILE_TOOLS = ['Write', 'Edit', 'NotebookEdit'];
|
|
109
113
|
const SLYPLAN_SYNC_TOOLS = ['update_node', 'add_node', 'add_to_work_mode'];
|
|
110
114
|
|
|
111
|
-
let lastFileChangeIdx = -1;
|
|
112
115
|
let lastSlyplanSyncIdx = -1;
|
|
113
|
-
let
|
|
116
|
+
let fileChangesSinceSync = 0;
|
|
117
|
+
let hasGitCommitSinceSync = false;
|
|
114
118
|
|
|
115
119
|
for (let i = 0; i < lines.length; i++) {
|
|
116
120
|
try {
|
|
@@ -120,36 +124,62 @@ process.stdin.on('end', () => {
|
|
|
120
124
|
if (block.type !== 'tool_use') continue;
|
|
121
125
|
const name = block.name || '';
|
|
122
126
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
127
|
+
// Track SlyPlan syncs — resets the counter
|
|
128
|
+
for (const st of SLYPLAN_SYNC_TOOLS) {
|
|
129
|
+
if (name.includes(st)) {
|
|
130
|
+
lastSlyplanSyncIdx = i;
|
|
131
|
+
fileChangesSinceSync = 0;
|
|
132
|
+
hasGitCommitSinceSync = false;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Count file changes since last sync
|
|
137
|
+
if (FILE_TOOLS.includes(name) && i > lastSlyplanSyncIdx) {
|
|
138
|
+
fileChangesSinceSync++;
|
|
126
139
|
}
|
|
140
|
+
|
|
141
|
+
// Detect git commit since last sync
|
|
127
142
|
if (name === 'Bash' && block.input && typeof block.input.command === 'string') {
|
|
128
143
|
const cmd = block.input.command;
|
|
129
|
-
if (cmd.includes('git commit')
|
|
130
|
-
|
|
131
|
-
fileChangeCount++;
|
|
144
|
+
if (cmd.includes('git commit') && i > lastSlyplanSyncIdx) {
|
|
145
|
+
hasGitCommitSinceSync = true;
|
|
132
146
|
}
|
|
133
147
|
}
|
|
134
|
-
for (const st of SLYPLAN_SYNC_TOOLS) {
|
|
135
|
-
if (name.includes(st)) lastSlyplanSyncIdx = i;
|
|
136
|
-
}
|
|
137
148
|
}
|
|
138
149
|
}
|
|
139
150
|
} catch {}
|
|
140
151
|
}
|
|
141
152
|
|
|
142
|
-
|
|
143
|
-
if (
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
+
// No changes since last sync — stay silent
|
|
154
|
+
if (fileChangesSinceSync === 0 && !hasGitCommitSinceSync) { process.exit(0); }
|
|
155
|
+
|
|
156
|
+
// Fire on git commit
|
|
157
|
+
if (hasGitCommitSinceSync) {
|
|
158
|
+
const output = JSON.stringify({
|
|
159
|
+
suppressOutput: true,
|
|
160
|
+
hookSpecificOutput: {
|
|
161
|
+
hookEventName: "PostToolUse",
|
|
162
|
+
additionalContext: "SYNC NOW: Git commit detected \\u2014 update SlyPlan node with progress before continuing."
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
process.stdout.write(output);
|
|
166
|
+
process.exit(0);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Fire when batch threshold reached
|
|
170
|
+
if (fileChangesSinceSync >= BATCH_THRESHOLD) {
|
|
171
|
+
const output = JSON.stringify({
|
|
172
|
+
suppressOutput: true,
|
|
173
|
+
hookSpecificOutput: {
|
|
174
|
+
hookEventName: "PostToolUse",
|
|
175
|
+
additionalContext: "SYNC NOW: " + fileChangesSinceSync + " file change(s) since last sync. Call update_node with current progress."
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
process.stdout.write(output);
|
|
179
|
+
process.exit(0);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Under threshold — stay silent
|
|
153
183
|
process.exit(0);
|
|
154
184
|
} catch (e) {
|
|
155
185
|
process.exit(0);
|
|
@@ -162,7 +192,7 @@ const CLAUDEMD_SECTION = `
|
|
|
162
192
|
Hook messages containing **"BLOCKED"** or **"SYNC NOW"** are HARD REQUIREMENTS, not suggestions.
|
|
163
193
|
|
|
164
194
|
- **BLOCKED:** You MUST stop what you're doing and follow the instruction BEFORE doing anything else. No exceptions — regardless of whether you're mid-workflow, running a GSD command, or in a multi-step task.
|
|
165
|
-
- **SYNC NOW:** You MUST call \`update_node\` with updated progress BEFORE continuing with the next tool call. Max
|
|
195
|
+
- **SYNC NOW:** You MUST call \`update_node\` with updated progress BEFORE continuing with the next tool call. Max 3 file changes between each sync.
|
|
166
196
|
- Hooks are transcript-aware — they only fire when sync is actually missing. If you see a message, it means you have NOT done the work.
|
|
167
197
|
- Ignoring hooks because you're "following a recipe" or "in the middle of something" is NEVER a valid reason.
|
|
168
198
|
|
|
@@ -175,17 +205,30 @@ You MUST keep SlyPlan updated. This is not optional. Follow these rules:
|
|
|
175
205
|
- Match the project based on the cwd folder name or user context.
|
|
176
206
|
- If ambiguous, ask the user. Cache the project ID for the rest of the session.
|
|
177
207
|
|
|
178
|
-
### 2. Find
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
208
|
+
### 2. Find the right place in the tree BEFORE creating nodes
|
|
209
|
+
Before creating any node, you MUST understand where it belongs:
|
|
210
|
+
1. Call \`get_tree\` with \`max_depth: 2\` to see the project's top-level structure (categories and their immediate children).
|
|
211
|
+
2. Identify the most relevant category or phase for your work.
|
|
212
|
+
3. Call \`get_node\` on that parent with \`max_depth: 2\` to see its full children (2 levels deep).
|
|
213
|
+
4. Now decide: create your node under the best-fitting parent.
|
|
214
|
+
5. If nothing in step 1 was relevant, create a new category.
|
|
215
|
+
|
|
216
|
+
Once you've found or created the right node:
|
|
217
|
+
- Call \`add_to_work_mode\` on it BEFORE starting with Edit/Write/Bash.
|
|
182
218
|
- **You must NEVER start work without an active work mode node.**
|
|
183
219
|
|
|
184
220
|
### 3. Update progress AS YOU GO
|
|
185
|
-
- After each meaningful change: call \`update_node\` with updated \`progress\` (0-100).
|
|
186
|
-
-
|
|
221
|
+
- After each meaningful change: call \`update_node\` with updated \`progress\` (0-100) and a \`status_log\` entry.
|
|
222
|
+
- The \`status_log\` parameter appends a log entry to the node — use it to describe what was just done.
|
|
223
|
+
- Keep the node's \`description\` clean and static. Progress notes go in \`status_log\`.
|
|
187
224
|
- Skip for trivial changes (typos, config tweaks).
|
|
188
225
|
|
|
226
|
+
**Writing good descriptions:**
|
|
227
|
+
- Explain what this node does and WHY it exists — not just a title repeat.
|
|
228
|
+
- Show how it fits in the parent's flow (e.g. "Handles auth token refresh so the API layer can retry failed requests transparently").
|
|
229
|
+
- Keep it concise: 1-3 sentences. Code references are fine when they add clarity (e.g. "Refactors \`useElkLayout.ts\` to support per-project circular layout"), but don't dump implementation details.
|
|
230
|
+
- A non-developer should roughly understand the purpose from the description.
|
|
231
|
+
|
|
189
232
|
### 4. NEVER mark anything as done without asking
|
|
190
233
|
- Use \`AskUserQuestion\` with choices: "Yes, mark as done" / "Not yet" / "Skip".
|
|
191
234
|
- **No exceptions.** Auto-marking done is forbidden.
|
|
@@ -200,17 +243,28 @@ You MUST keep SlyPlan updated. This is not optional. Follow these rules:
|
|
|
200
243
|
### Node hierarchy
|
|
201
244
|
\`project\` > \`category\` > \`phase\` > \`plan\`
|
|
202
245
|
|
|
246
|
+
Organize nodes logically for readability. Use subcategories/subphases when it helps, but no hard limit on children count — use good judgement.
|
|
247
|
+
|
|
248
|
+
**Node creation rules:**
|
|
249
|
+
- Always place new nodes under existing logical parents — search first, don't create top-level orphans.
|
|
250
|
+
- Split plans into 2-3 children when work touches multiple components/steps. Don't oversplit single changes.
|
|
251
|
+
- NEVER create a new node to fix/debug an existing one. Update the original node instead (status, description, progress). Debugging is part of the original work, not a separate task.
|
|
252
|
+
|
|
203
253
|
### Performance rules
|
|
204
|
-
- NEVER use \`get_tree\`
|
|
254
|
+
- NEVER use \`get_tree\` without \`max_depth\`. Always pass \`max_depth: 2\` or lower.
|
|
255
|
+
- Use \`search\` for finding specific nodes by name.
|
|
256
|
+
- Use \`get_node\` with \`max_depth: 2\` to explore a specific subtree.
|
|
205
257
|
|
|
206
258
|
### MCP tools
|
|
207
259
|
| Tool | Usage |
|
|
208
260
|
|------|-------|
|
|
209
261
|
| \`list_projects\` | Find projects (session start) |
|
|
210
262
|
| \`set_project\` | Select active project |
|
|
263
|
+
| \`get_tree\` | See project structure (ALWAYS use \`max_depth: 2\`) |
|
|
264
|
+
| \`get_node\` | Inspect a node + its children (use \`max_depth: 2\`) |
|
|
211
265
|
| \`search\` | Find nodes by name/content |
|
|
212
266
|
| \`add_node\` | Create new node (project/category/phase/plan) |
|
|
213
|
-
| \`update_node\` | Update status, progress, description |
|
|
267
|
+
| \`update_node\` | Update status, progress, description, \`status_log\` (appends log entry) |
|
|
214
268
|
| \`add_to_work_mode\` | Mark node as active work |
|
|
215
269
|
| \`remove_from_work_mode\` | Remove from active work |
|
|
216
270
|
`;
|
|
@@ -344,16 +398,21 @@ function writeJsonFile(filePath, data) {
|
|
|
344
398
|
}
|
|
345
399
|
// --- Settings Merge ---
|
|
346
400
|
// MCP server config goes in .mcp.json (Claude Code reads servers from here)
|
|
347
|
-
function mergeMcpJson(existing,
|
|
401
|
+
function mergeMcpJson(existing, auth) {
|
|
348
402
|
const result = { ...existing };
|
|
349
403
|
if (!result.mcpServers)
|
|
350
404
|
result.mcpServers = {};
|
|
405
|
+
const env = {};
|
|
406
|
+
if (auth.apiKey) {
|
|
407
|
+
env.SLYPLAN_API_KEY = auth.apiKey;
|
|
408
|
+
}
|
|
409
|
+
else if (auth.refreshToken) {
|
|
410
|
+
env.SLYPLAN_REFRESH_TOKEN = auth.refreshToken;
|
|
411
|
+
}
|
|
351
412
|
result.mcpServers.slyplan = {
|
|
352
413
|
command: 'npx',
|
|
353
414
|
args: ['-y', 'slyplan-mcp@latest'],
|
|
354
|
-
env
|
|
355
|
-
SLYPLAN_REFRESH_TOKEN: refreshToken,
|
|
356
|
-
},
|
|
415
|
+
env,
|
|
357
416
|
};
|
|
358
417
|
return result;
|
|
359
418
|
}
|
|
@@ -498,6 +557,26 @@ async function runSetup() {
|
|
|
498
557
|
}
|
|
499
558
|
}
|
|
500
559
|
log('');
|
|
560
|
+
// Generate permanent API key (recommended over refresh tokens)
|
|
561
|
+
let apiKey = '';
|
|
562
|
+
const client = createClient(SUPABASE_URL, SUPABASE_ANON_KEY, {
|
|
563
|
+
auth: { autoRefreshToken: false, persistSession: false },
|
|
564
|
+
});
|
|
565
|
+
await client.auth.refreshSession({ refresh_token: refreshToken });
|
|
566
|
+
const keyAnswer = await promptUser(' Generate a permanent API key? (recommended) [Y/n]: ');
|
|
567
|
+
if (keyAnswer.toLowerCase() !== 'n') {
|
|
568
|
+
const keyName = await promptUser(' Key name [Claude Code]: ');
|
|
569
|
+
const name = keyName.trim() || 'Claude Code';
|
|
570
|
+
const { data, error } = await client.rpc('create_api_key', { key_name: name });
|
|
571
|
+
if (!error && data?.key) {
|
|
572
|
+
apiKey = data.key;
|
|
573
|
+
log(` [+] API key created: ${data.key_prefix}••••`);
|
|
574
|
+
}
|
|
575
|
+
else {
|
|
576
|
+
log(` [!] Could not create API key: ${error?.message || 'unknown error'}`);
|
|
577
|
+
log(' Falling back to refresh token.');
|
|
578
|
+
}
|
|
579
|
+
}
|
|
501
580
|
// 1. Create hook files
|
|
502
581
|
fs.mkdirSync(hooksDir, { recursive: true });
|
|
503
582
|
fs.writeFileSync(preHookFilePath, PRE_HOOK_FILE_CONTENT, 'utf8');
|
|
@@ -506,9 +585,12 @@ async function runSetup() {
|
|
|
506
585
|
log(' [+] Created .claude/hooks/slyplan-post-tool-sync.cjs');
|
|
507
586
|
// 2. Write MCP server config to .mcp.json
|
|
508
587
|
const existingMcp = readJsonFile(mcpJsonPath) ?? {};
|
|
509
|
-
const
|
|
588
|
+
const auth = apiKey
|
|
589
|
+
? { apiKey }
|
|
590
|
+
: { refreshToken };
|
|
591
|
+
const mergedMcp = mergeMcpJson(existingMcp, auth);
|
|
510
592
|
writeJsonFile(mcpJsonPath, mergedMcp);
|
|
511
|
-
log(
|
|
593
|
+
log(` [+] Updated .mcp.json (${apiKey ? 'API key' : 'refresh token'})`);
|
|
512
594
|
// 3. Write hooks to .claude/settings.json
|
|
513
595
|
let existingSettings = {};
|
|
514
596
|
const rawSettings = readJsonFile(settingsPath);
|
|
@@ -525,7 +607,18 @@ async function runSetup() {
|
|
|
525
607
|
const mergedSettings = mergeSettings(existingSettings);
|
|
526
608
|
writeJsonFile(settingsPath, mergedSettings);
|
|
527
609
|
log(' [+] Updated .claude/settings.json (hooks)');
|
|
528
|
-
// 4.
|
|
610
|
+
// 4. Install slash commands (.claude/commands/sly/)
|
|
611
|
+
const commandsSrcDir = path.resolve(path.dirname(new URL(import.meta.url).pathname.replace(/^\/([A-Z]:)/, '$1')), '..', 'commands', 'sly');
|
|
612
|
+
const commandsDestDir = path.join(claudeDir, 'commands', 'sly');
|
|
613
|
+
if (fs.existsSync(commandsSrcDir)) {
|
|
614
|
+
fs.mkdirSync(commandsDestDir, { recursive: true });
|
|
615
|
+
const commandFiles = fs.readdirSync(commandsSrcDir).filter(f => f.endsWith('.md'));
|
|
616
|
+
for (const file of commandFiles) {
|
|
617
|
+
fs.copyFileSync(path.join(commandsSrcDir, file), path.join(commandsDestDir, file));
|
|
618
|
+
}
|
|
619
|
+
log(` [+] Installed ${commandFiles.length} slash commands: ${commandFiles.map(f => '/sly:' + f.replace('.md', '')).join(', ')}`);
|
|
620
|
+
}
|
|
621
|
+
// 5. Optional CLAUDE.md append
|
|
529
622
|
const appendAnswer = await promptUser(' Append SlyPlan sync instructions to CLAUDE.md? [Y/n]: ');
|
|
530
623
|
if (appendAnswer.toLowerCase() !== 'n') {
|
|
531
624
|
if (fs.existsSync(claudeMdPath)) {
|
|
@@ -595,7 +688,22 @@ async function runRemove() {
|
|
|
595
688
|
log(' [-] Removed SlyPlan from .mcp.json');
|
|
596
689
|
}
|
|
597
690
|
}
|
|
598
|
-
// 3.
|
|
691
|
+
// 3. Remove slash commands
|
|
692
|
+
const commandsDir = path.join(claudeDir, 'commands', 'sly');
|
|
693
|
+
if (fs.existsSync(commandsDir)) {
|
|
694
|
+
const files = fs.readdirSync(commandsDir).filter(f => f.endsWith('.md'));
|
|
695
|
+
for (const file of files) {
|
|
696
|
+
fs.unlinkSync(path.join(commandsDir, file));
|
|
697
|
+
}
|
|
698
|
+
// Remove empty dirs
|
|
699
|
+
try {
|
|
700
|
+
fs.rmdirSync(commandsDir);
|
|
701
|
+
fs.rmdirSync(path.join(claudeDir, 'commands'));
|
|
702
|
+
}
|
|
703
|
+
catch { }
|
|
704
|
+
log(` [-] Removed ${files.length} slash commands`);
|
|
705
|
+
}
|
|
706
|
+
// 4. Clean settings.json
|
|
599
707
|
const existing = readJsonFile(settingsPath);
|
|
600
708
|
if (existing) {
|
|
601
709
|
const cleaned = removeFromSettings(existing);
|
package/dist/db.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { TreeNode, Link, NodeDependency } from './types.js';
|
|
2
|
-
export declare function getTree(rootId?: string | null): Promise<TreeNode[]>;
|
|
3
|
-
export declare function getNode(id: string): Promise<TreeNode | null>;
|
|
2
|
+
export declare function getTree(rootId?: string | null, maxDepth?: number): Promise<TreeNode[]>;
|
|
3
|
+
export declare function getNode(id: string, maxDepth?: number): Promise<TreeNode | null>;
|
|
4
4
|
export declare function insertNode(data: {
|
|
5
5
|
parentId: string | null;
|
|
6
6
|
type: string;
|
package/dist/db.js
CHANGED
|
@@ -29,7 +29,7 @@ function dbLinkToLink(row) {
|
|
|
29
29
|
};
|
|
30
30
|
}
|
|
31
31
|
// --- Node CRUD ---
|
|
32
|
-
export async function getTree(rootId) {
|
|
32
|
+
export async function getTree(rootId, maxDepth) {
|
|
33
33
|
const { data: allNodes, error: nodesErr } = await supabase
|
|
34
34
|
.from('nodes')
|
|
35
35
|
.select('*')
|
|
@@ -56,10 +56,12 @@ export async function getTree(rootId) {
|
|
|
56
56
|
arr.push(node);
|
|
57
57
|
childrenMap.set(parentKey, arr);
|
|
58
58
|
}
|
|
59
|
-
function buildSubtree(parentId) {
|
|
59
|
+
function buildSubtree(parentId, depth) {
|
|
60
60
|
const children = childrenMap.get(parentId) || [];
|
|
61
61
|
return children.map(node => {
|
|
62
|
-
const subtreeChildren =
|
|
62
|
+
const subtreeChildren = (maxDepth !== undefined && depth >= maxDepth)
|
|
63
|
+
? []
|
|
64
|
+
: buildSubtree(node.id, depth + 1);
|
|
63
65
|
const links = linksByNode.get(node.id) || [];
|
|
64
66
|
let progress = node.progress;
|
|
65
67
|
if (subtreeChildren.length > 0) {
|
|
@@ -73,16 +75,16 @@ export async function getTree(rootId) {
|
|
|
73
75
|
const rootNode = nodeMap.get(rootId);
|
|
74
76
|
if (!rootNode)
|
|
75
77
|
return [];
|
|
76
|
-
const children = buildSubtree(rootId);
|
|
78
|
+
const children = buildSubtree(rootId, 1);
|
|
77
79
|
let progress = rootNode.progress;
|
|
78
80
|
if (children.length > 0) {
|
|
79
81
|
progress = Math.round(children.reduce((sum, c) => sum + c.progress, 0) / children.length);
|
|
80
82
|
}
|
|
81
83
|
return [dbNodeToTree({ ...rootNode, progress }, linksByNode.get(rootId) || [], children)];
|
|
82
84
|
}
|
|
83
|
-
return buildSubtree(null);
|
|
85
|
+
return buildSubtree(null, 0);
|
|
84
86
|
}
|
|
85
|
-
export async function getNode(id) {
|
|
87
|
+
export async function getNode(id, maxDepth = 1) {
|
|
86
88
|
const { data: row, error } = await supabase
|
|
87
89
|
.from('nodes')
|
|
88
90
|
.select('*')
|
|
@@ -95,10 +97,18 @@ export async function getNode(id) {
|
|
|
95
97
|
.select('*')
|
|
96
98
|
.eq('node_id', id);
|
|
97
99
|
const links = (linkRows || []).map(dbLinkToLink);
|
|
100
|
+
const children = maxDepth > 0 ? await getChildrenRecursive(id, maxDepth, 1) : [];
|
|
101
|
+
let progress = row.progress;
|
|
102
|
+
if (children.length > 0) {
|
|
103
|
+
progress = Math.round(children.reduce((sum, c) => sum + c.progress, 0) / children.length);
|
|
104
|
+
}
|
|
105
|
+
return dbNodeToTree({ ...row, progress }, links, children);
|
|
106
|
+
}
|
|
107
|
+
async function getChildrenRecursive(parentId, maxDepth, currentDepth) {
|
|
98
108
|
const { data: childRows } = await supabase
|
|
99
109
|
.from('nodes')
|
|
100
110
|
.select('*')
|
|
101
|
-
.eq('parent_id',
|
|
111
|
+
.eq('parent_id', parentId)
|
|
102
112
|
.order('sort_order', { ascending: true });
|
|
103
113
|
const children = [];
|
|
104
114
|
for (const child of (childRows || [])) {
|
|
@@ -106,13 +116,16 @@ export async function getNode(id) {
|
|
|
106
116
|
.from('links')
|
|
107
117
|
.select('*')
|
|
108
118
|
.eq('node_id', child.id);
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
119
|
+
const grandchildren = currentDepth < maxDepth
|
|
120
|
+
? await getChildrenRecursive(child.id, maxDepth, currentDepth + 1)
|
|
121
|
+
: [];
|
|
122
|
+
let progress = child.progress;
|
|
123
|
+
if (grandchildren.length > 0) {
|
|
124
|
+
progress = Math.round(grandchildren.reduce((sum, c) => sum + c.progress, 0) / grandchildren.length);
|
|
125
|
+
}
|
|
126
|
+
children.push(dbNodeToTree({ ...child, progress }, (childLinkRows || []).map(dbLinkToLink), grandchildren));
|
|
114
127
|
}
|
|
115
|
-
return
|
|
128
|
+
return children;
|
|
116
129
|
}
|
|
117
130
|
export async function insertNode(data) {
|
|
118
131
|
const id = uuid();
|
package/dist/index.js
CHANGED
|
@@ -56,14 +56,20 @@ server.tool('set_project', 'Select which project to work in. All subsequent oper
|
|
|
56
56
|
content: [{ type: 'text', text: `Active project set to: "${node.title}"\n\nAll add_node, get_tree and search operations will now apply to this project.\nUse get_tree to see the project structure.` }],
|
|
57
57
|
};
|
|
58
58
|
});
|
|
59
|
-
server.tool('get_tree', 'Get the project tree. Shows the active project if set, or the full tree.
|
|
59
|
+
server.tool('get_tree', 'Get the project tree. Shows the active project if set, or the full tree. Use max_depth to limit how deep the tree goes.', {
|
|
60
|
+
root_id: z.string().optional().describe('Node ID to use as root. Omit for active project / full tree.'),
|
|
61
|
+
max_depth: z.number().min(0).optional().describe('Max depth of children to include (0 = root only, 1 = direct children, 2 = grandchildren). Omit for full tree.'),
|
|
62
|
+
}, async ({ root_id, max_depth }) => {
|
|
60
63
|
const effectiveRoot = root_id ?? activeProjectId ?? null;
|
|
61
|
-
const tree = await getTree(effectiveRoot);
|
|
64
|
+
const tree = await getTree(effectiveRoot, max_depth);
|
|
62
65
|
const ctx = activeProjectId ? `Project: ${await getActiveProjectName()}\n\n` : '';
|
|
63
66
|
return { content: [{ type: 'text', text: `${ctx}${JSON.stringify(tree, null, 2)}` }] };
|
|
64
67
|
});
|
|
65
|
-
server.tool('get_node', 'Get details about a single node
|
|
66
|
-
|
|
68
|
+
server.tool('get_node', 'Get details about a single node and its children', {
|
|
69
|
+
id: z.string().describe('Node ID to fetch'),
|
|
70
|
+
max_depth: z.number().min(0).optional().describe('How many levels of children to include (0 = node only, 1 = direct children, 2 = grandchildren). Default: 1.'),
|
|
71
|
+
}, async ({ id, max_depth }) => {
|
|
72
|
+
const node = await getNode(id, max_depth ?? 1);
|
|
67
73
|
if (!node)
|
|
68
74
|
return { content: [{ type: 'text', text: 'Node not found' }], isError: true };
|
|
69
75
|
return { content: [{ type: 'text', text: JSON.stringify(node, null, 2) }] };
|
|
@@ -97,7 +103,17 @@ server.tool('update_node', 'Update fields on an existing node', {
|
|
|
97
103
|
status: z.enum(['not_started', 'in_progress', 'blocked', 'done']).optional().describe('New status'),
|
|
98
104
|
progress: z.number().min(0).max(100).optional().describe('New progress (0-100)'),
|
|
99
105
|
metadata: z.record(z.unknown()).optional().describe('New metadata'),
|
|
100
|
-
|
|
106
|
+
status_log: z.string().optional().describe('Append a status log entry (stored in metadata.statusLog array)'),
|
|
107
|
+
}, async ({ id, status_log, ...updates }) => {
|
|
108
|
+
if (status_log) {
|
|
109
|
+
const current = await getNode(id);
|
|
110
|
+
if (!current)
|
|
111
|
+
return { content: [{ type: 'text', text: 'Node not found' }], isError: true };
|
|
112
|
+
const meta = (current.metadata || {});
|
|
113
|
+
const log = Array.isArray(meta.statusLog) ? [...meta.statusLog] : [];
|
|
114
|
+
log.push(status_log);
|
|
115
|
+
updates.metadata = { ...meta, ...(updates.metadata || {}), statusLog: log };
|
|
116
|
+
}
|
|
101
117
|
const node = await updateNode(id, updates);
|
|
102
118
|
if (!node)
|
|
103
119
|
return { content: [{ type: 'text', text: 'Node not found' }], isError: true };
|
package/dist/supabase.js
CHANGED
|
@@ -33,10 +33,42 @@ supabase.auth.onAuthStateChange((event, session) => {
|
|
|
33
33
|
}
|
|
34
34
|
});
|
|
35
35
|
export async function authenticate() {
|
|
36
|
+
const apiKey = process.env.SLYPLAN_API_KEY;
|
|
36
37
|
const refreshToken = process.env.SLYPLAN_REFRESH_TOKEN;
|
|
37
38
|
const email = process.env.SLYPLAN_EMAIL;
|
|
38
39
|
const password = process.env.SLYPLAN_PASSWORD;
|
|
39
|
-
//
|
|
40
|
+
// 1. Preferred: permanent API key (never expires)
|
|
41
|
+
// Calls edge function to exchange API key → Supabase session tokens
|
|
42
|
+
if (apiKey) {
|
|
43
|
+
try {
|
|
44
|
+
const res = await fetch(`${SUPABASE_URL}/functions/v1/exchange-api-key`, {
|
|
45
|
+
method: 'POST',
|
|
46
|
+
headers: { 'Content-Type': 'application/json' },
|
|
47
|
+
body: JSON.stringify({ api_key: apiKey }),
|
|
48
|
+
});
|
|
49
|
+
if (!res.ok) {
|
|
50
|
+
const err = await res.json().catch(() => ({ error: res.statusText }));
|
|
51
|
+
console.error(`API key authentication failed: ${err.error || res.statusText}`);
|
|
52
|
+
process.exit(1);
|
|
53
|
+
}
|
|
54
|
+
const tokens = await res.json();
|
|
55
|
+
const { error: sessionError } = await supabase.auth.setSession({
|
|
56
|
+
access_token: tokens.access_token,
|
|
57
|
+
refresh_token: tokens.refresh_token,
|
|
58
|
+
});
|
|
59
|
+
if (sessionError) {
|
|
60
|
+
console.error(`Failed to set session from API key: ${sessionError.message}`);
|
|
61
|
+
process.exit(1);
|
|
62
|
+
}
|
|
63
|
+
userId = tokens.user_id;
|
|
64
|
+
}
|
|
65
|
+
catch (err) {
|
|
66
|
+
console.error(`API key authentication failed: ${err.message}`);
|
|
67
|
+
process.exit(1);
|
|
68
|
+
}
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
// 2. Refresh token (browser-based flow)
|
|
40
72
|
if (refreshToken) {
|
|
41
73
|
lastPersistedToken = refreshToken;
|
|
42
74
|
const { data, error } = await supabase.auth.refreshSession({ refresh_token: refreshToken });
|
|
@@ -62,7 +94,7 @@ export async function authenticate() {
|
|
|
62
94
|
userId = data.user.id;
|
|
63
95
|
return;
|
|
64
96
|
}
|
|
65
|
-
console.error('Missing SLYPLAN_REFRESH_TOKEN
|
|
97
|
+
console.error('Missing SLYPLAN_API_KEY, SLYPLAN_REFRESH_TOKEN, or SLYPLAN_EMAIL + SLYPLAN_PASSWORD.');
|
|
66
98
|
console.error('Run "npx slyplan-mcp setup" to configure authentication.');
|
|
67
99
|
process.exit(1);
|
|
68
100
|
}
|
package/package.json
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "slyplan-mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.3",
|
|
4
4
|
"description": "MCP server for Slyplan — visual project management via Claude",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"slyplan-mcp": "dist/index.js"
|
|
8
8
|
},
|
|
9
9
|
"files": [
|
|
10
|
-
"dist"
|
|
10
|
+
"dist",
|
|
11
|
+
"commands"
|
|
11
12
|
],
|
|
12
13
|
"scripts": {
|
|
13
14
|
"build": "tsc",
|