slyplan-mcp 1.7.0 → 1.7.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/cli.js +61 -90
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -11,7 +11,7 @@ const SUPABASE_URL = 'https://omfzpkwtuzucwwxmyuqt.supabase.co';
11
11
  const SUPABASE_ANON_KEY = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Im9tZnpwa3d0dXp1Y3d3eG15dXF0Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzA5MjMwNDIsImV4cCI6MjA4NjQ5OTA0Mn0.KXGoUez7M45RtFM9qR7mjzGX6UhlaRE-gggAJxSkIHY';
12
12
  const SLYPLAN_ORIGIN = 'https://slyplan.com';
13
13
  // Smart transcript-aware hook: checks if set_project + add_to_work_mode have been called.
14
- // Silent when OK, reminds only when needed. Also shows update notification.
14
+ // Optimized: uses string search instead of parsing every JSON line.
15
15
  const PRE_HOOK_FILE_CONTENT = `#!/usr/bin/env node
16
16
  const fs = require('fs');
17
17
  const path = require('path');
@@ -25,63 +25,41 @@ process.stdin.on('end', () => {
25
25
  const data = JSON.parse(input);
26
26
 
27
27
  if (!data.transcript_path) {
28
- const output = JSON.stringify({
29
- suppressOutput: true,
28
+ process.stdout.write(JSON.stringify({
30
29
  hookSpecificOutput: {
31
30
  hookEventName: "PreToolCall",
32
31
  additionalContext: "BLOCKED: Call list_projects + set_project before doing any work. This is not optional."
33
32
  }
34
- });
35
- process.stdout.write(output);
33
+ }));
36
34
  process.exit(0);
37
35
  }
38
36
 
39
37
  const transcriptPath = data.transcript_path.replace(/^~/, process.env.HOME || process.env.USERPROFILE || '');
40
38
  if (!fs.existsSync(transcriptPath)) { process.exit(0); }
41
39
 
40
+ // Fast: read file as string and use indexOf instead of parsing every JSON line
42
41
  const content = fs.readFileSync(transcriptPath, 'utf8');
43
- const lines = content.split('\\n').filter(Boolean);
44
42
 
45
- let hasSetProject = false;
46
- let hasWorkModeNode = false;
47
-
48
- for (const line of lines) {
49
- try {
50
- const entry = JSON.parse(line);
51
- if (entry.type === 'assistant' && entry.message && Array.isArray(entry.message.content)) {
52
- for (const block of entry.message.content) {
53
- if (block.type !== 'tool_use') continue;
54
- const name = block.name || '';
55
- if (name.includes('set_project')) hasSetProject = true;
56
- // Track add/remove \\u2014 last action wins
57
- if (name.includes('add_to_work_mode')) hasWorkModeNode = true;
58
- if (name.includes('remove_from_work_mode')) hasWorkModeNode = false;
59
- }
60
- }
61
- } catch {}
62
- }
63
-
64
- if (!hasSetProject) {
65
- const output = JSON.stringify({
66
- suppressOutput: true,
43
+ if (!content.includes('set_project')) {
44
+ process.stdout.write(JSON.stringify({
67
45
  hookSpecificOutput: {
68
46
  hookEventName: "PreToolCall",
69
47
  additionalContext: "BLOCKED: Call list_projects + set_project before doing any work. This is not optional."
70
48
  }
71
- });
72
- process.stdout.write(output);
49
+ }));
73
50
  process.exit(0);
74
51
  }
75
52
 
76
- if (!hasWorkModeNode) {
77
- const output = JSON.stringify({
78
- suppressOutput: true,
53
+ // For work mode, we need to check the LAST occurrence wins (add vs remove)
54
+ const lastAdd = content.lastIndexOf('add_to_work_mode');
55
+ const lastRemove = content.lastIndexOf('remove_from_work_mode');
56
+ if (lastAdd < 0 || (lastRemove > lastAdd)) {
57
+ process.stdout.write(JSON.stringify({
79
58
  hookSpecificOutput: {
80
59
  hookEventName: "PreToolCall",
81
60
  additionalContext: "BLOCKED: No node in work mode. Call search + add_to_work_mode before doing any work. This is not optional."
82
61
  }
83
- });
84
- process.stdout.write(output);
62
+ }));
85
63
  process.exit(0);
86
64
  }
87
65
 
@@ -91,14 +69,12 @@ process.stdin.on('end', () => {
91
69
  if (fs.existsSync(cacheFile)) {
92
70
  const cache = JSON.parse(fs.readFileSync(cacheFile, 'utf8'));
93
71
  if (cache.update_available) {
94
- const output = JSON.stringify({
95
- suppressOutput: true,
72
+ process.stdout.write(JSON.stringify({
96
73
  hookSpecificOutput: {
97
74
  hookEventName: "PreToolCall",
98
- additionalContext: "\\u26A0\\uFE0F SlyPlan update available: " + cache.installed + " \\u2192 " + cache.latest + ". Run /sly:update to update."
75
+ additionalContext: "SlyPlan update available: " + cache.installed + " -> " + cache.latest + ". Run /sly:update to update."
99
76
  }
100
- });
101
- process.stdout.write(output);
77
+ }));
102
78
  process.exit(0);
103
79
  }
104
80
  }
@@ -111,11 +87,12 @@ process.stdin.on('end', () => {
111
87
  });
112
88
  `;
113
89
  // Smart transcript-aware hook: counts file changes SINCE last SlyPlan sync.
114
- // Silent when synced. Fires after 3+ unsynced file changes or a git commit.
90
+ // Optimized: reads only the tail of the transcript to find recent activity.
115
91
  const POST_HOOK_FILE_CONTENT = `#!/usr/bin/env node
116
92
  const fs = require('fs');
117
93
 
118
94
  const BATCH_THRESHOLD = 3;
95
+ const TAIL_BYTES = 32768; // Read last 32KB — enough for recent tool calls
119
96
 
120
97
  let input = '';
121
98
  process.stdin.setEncoding('utf8');
@@ -129,80 +106,74 @@ process.stdin.on('end', () => {
129
106
  const transcriptPath = data.transcript_path.replace(/^~/, process.env.HOME || process.env.USERPROFILE || '');
130
107
  if (!fs.existsSync(transcriptPath)) { process.exit(0); }
131
108
 
132
- const content = fs.readFileSync(transcriptPath, 'utf8');
133
- const lines = content.split('\\n').filter(Boolean);
109
+ // Read only the tail of the file for performance
110
+ const stat = fs.statSync(transcriptPath);
111
+ const fd = fs.openSync(transcriptPath, 'r');
112
+ const readStart = Math.max(0, stat.size - TAIL_BYTES);
113
+ const buf = Buffer.alloc(Math.min(stat.size, TAIL_BYTES));
114
+ fs.readSync(fd, buf, 0, buf.length, readStart);
115
+ fs.closeSync(fd);
116
+ const tail = buf.toString('utf8');
134
117
 
135
- const FILE_TOOLS = ['Write', 'Edit', 'NotebookEdit'];
136
- const SLYPLAN_SYNC_TOOLS = ['update_node', 'add_node', 'add_to_work_mode'];
118
+ // If we're reading a partial file, skip the first (possibly incomplete) line
119
+ const lines = tail.split('\\n').filter(Boolean);
120
+ if (readStart > 0 && lines.length > 0) lines.shift();
137
121
 
138
- let lastSlyplanSyncIdx = -1;
139
- let fileChangesSinceSync = 0;
140
- let hasGitCommitSinceSync = false;
122
+ let lastSyncIdx = -1;
123
+ let fileChanges = 0;
124
+ let hasCommit = false;
141
125
 
142
126
  for (let i = 0; i < lines.length; i++) {
127
+ // Fast string checks before expensive JSON.parse
128
+ const line = lines[i];
129
+ if (!line.includes('tool_use')) continue;
130
+
143
131
  try {
144
- const entry = JSON.parse(lines[i]);
145
- if (entry.type === 'assistant' && entry.message && Array.isArray(entry.message.content)) {
146
- for (const block of entry.message.content) {
147
- if (block.type !== 'tool_use') continue;
148
- const name = block.name || '';
149
-
150
- // Track SlyPlan syncs resets the counter
151
- for (const st of SLYPLAN_SYNC_TOOLS) {
152
- if (name.includes(st)) {
153
- lastSlyplanSyncIdx = i;
154
- fileChangesSinceSync = 0;
155
- hasGitCommitSinceSync = false;
156
- }
157
- }
132
+ const entry = JSON.parse(line);
133
+ if (entry.type !== 'assistant' || !entry.message || !Array.isArray(entry.message.content)) continue;
134
+ for (const block of entry.message.content) {
135
+ if (block.type !== 'tool_use') continue;
136
+ const name = block.name || '';
137
+
138
+ if (name.includes('update_node') || name.includes('add_node') || name.includes('add_to_work_mode')) {
139
+ lastSyncIdx = i;
140
+ fileChanges = 0;
141
+ hasCommit = false;
142
+ }
158
143
 
159
- // Count file changes since last sync
160
- if (FILE_TOOLS.includes(name) && i > lastSlyplanSyncIdx) {
161
- fileChangesSinceSync++;
162
- }
144
+ if ((name === 'Write' || name === 'Edit' || name === 'NotebookEdit') && i > lastSyncIdx) {
145
+ fileChanges++;
146
+ }
163
147
 
164
- // Detect git commit since last sync
165
- if (name === 'Bash' && block.input && typeof block.input.command === 'string') {
166
- const cmd = block.input.command;
167
- if (cmd.includes('git commit') && i > lastSlyplanSyncIdx) {
168
- hasGitCommitSinceSync = true;
169
- }
170
- }
148
+ if (name === 'Bash' && line.includes('git commit') && i > lastSyncIdx) {
149
+ hasCommit = true;
171
150
  }
172
151
  }
173
152
  } catch {}
174
153
  }
175
154
 
176
- // No changes since last sync stay silent
177
- if (fileChangesSinceSync === 0 && !hasGitCommitSinceSync) { process.exit(0); }
155
+ if (fileChanges === 0 && !hasCommit) { process.exit(0); }
178
156
 
179
- // Fire on git commit
180
- if (hasGitCommitSinceSync) {
181
- const output = JSON.stringify({
182
- suppressOutput: true,
157
+ if (hasCommit) {
158
+ process.stdout.write(JSON.stringify({
183
159
  hookSpecificOutput: {
184
160
  hookEventName: "PostToolUse",
185
- additionalContext: "SYNC NOW: Git commit detected \\u2014 update SlyPlan node with progress before continuing."
161
+ additionalContext: "SYNC NOW: Git commit detected. Call update_node with current progress."
186
162
  }
187
- });
188
- process.stdout.write(output);
163
+ }));
189
164
  process.exit(0);
190
165
  }
191
166
 
192
- // Fire when batch threshold reached
193
- if (fileChangesSinceSync >= BATCH_THRESHOLD) {
194
- const output = JSON.stringify({
195
- suppressOutput: true,
167
+ if (fileChanges >= BATCH_THRESHOLD) {
168
+ process.stdout.write(JSON.stringify({
196
169
  hookSpecificOutput: {
197
170
  hookEventName: "PostToolUse",
198
- additionalContext: "SYNC NOW: " + fileChangesSinceSync + " file change(s) since last sync. Call update_node with current progress."
171
+ additionalContext: "SYNC NOW: " + fileChanges + " file change(s) since last sync. Call update_node with current progress."
199
172
  }
200
- });
201
- process.stdout.write(output);
173
+ }));
202
174
  process.exit(0);
203
175
  }
204
176
 
205
- // Under threshold — stay silent
206
177
  process.exit(0);
207
178
  } catch (e) {
208
179
  process.exit(0);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "slyplan-mcp",
3
- "version": "1.7.0",
3
+ "version": "1.7.2",
4
4
  "description": "MCP server for Slyplan — visual project management via Claude",
5
5
  "type": "module",
6
6
  "bin": {