slyplan-mcp 1.4.2 → 1.5.1

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/cli.js CHANGED
@@ -3,11 +3,11 @@ import * as fs from 'fs';
3
3
  import * as path from 'path';
4
4
  import * as http from 'http';
5
5
  import * as readline from 'readline';
6
- import { createClient } from '@supabase/supabase-js';
7
6
  import { exec } from 'child_process';
7
+ import { createClient } from '@supabase/supabase-js';
8
8
  const SUPABASE_URL = 'https://omfzpkwtuzucwwxmyuqt.supabase.co';
9
9
  const SUPABASE_ANON_KEY = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Im9tZnpwa3d0dXp1Y3d3eG15dXF0Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzA5MjMwNDIsImV4cCI6MjA4NjQ5OTA0Mn0.KXGoUez7M45RtFM9qR7mjzGX6UhlaRE-gggAJxSkIHY';
10
- const SLYPLAN_URL = 'https://slyplan.com';
10
+ const SLYPLAN_ORIGIN = 'https://slyplan.com';
11
11
  // Smart transcript-aware hook: checks if set_project + add_to_work_mode have been called.
12
12
  // Silent when OK, reminds only when needed.
13
13
  const PRE_HOOK_FILE_CONTENT = `#!/usr/bin/env node
@@ -218,136 +218,66 @@ You MUST keep SlyPlan updated. This is not optional. Follow these rules:
218
218
  function log(msg) {
219
219
  process.stderr.write(msg + '\n');
220
220
  }
221
- function promptUser(question, hidden = false) {
221
+ function promptUser(question) {
222
222
  return new Promise((resolve) => {
223
- if (hidden && process.stdin.isTTY) {
224
- // Password masking via raw mode
225
- process.stderr.write(question);
226
- const stdin = process.stdin;
227
- stdin.setRawMode(true);
228
- stdin.resume();
229
- stdin.setEncoding('utf8');
230
- let password = '';
231
- const onData = (ch) => {
232
- const c = ch.toString();
233
- if (c === '\n' || c === '\r' || c === '\u0004') {
234
- stdin.setRawMode(false);
235
- stdin.pause();
236
- stdin.removeListener('data', onData);
237
- process.stderr.write('\n');
238
- resolve(password);
239
- }
240
- else if (c === '\u0003') {
241
- // Ctrl+C
242
- stdin.setRawMode(false);
243
- process.exit(1);
244
- }
245
- else if (c === '\u007f' || c === '\b') {
246
- // Backspace
247
- if (password.length > 0) {
248
- password = password.slice(0, -1);
249
- process.stderr.write('\b \b');
250
- }
251
- }
252
- else {
253
- password += c;
254
- process.stderr.write('*');
255
- }
256
- };
257
- stdin.on('data', onData);
258
- }
259
- else {
260
- // Non-TTY or visible input
261
- const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
262
- rl.question(question, (answer) => {
263
- rl.close();
264
- resolve(answer.trim());
265
- });
266
- }
267
- });
268
- }
269
- async function validateCredentials(email, password) {
270
- const client = createClient(SUPABASE_URL, SUPABASE_ANON_KEY, {
271
- auth: { autoRefreshToken: false, persistSession: false },
223
+ const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
224
+ rl.question(question, (answer) => {
225
+ rl.close();
226
+ resolve(answer.trim());
227
+ });
272
228
  });
273
- const { data, error } = await client.auth.signInWithPassword({ email, password });
274
- if (error)
275
- return { ok: false, error: error.message };
276
- return { ok: true, userEmail: data.user?.email ?? email, refreshToken: data.session?.refresh_token };
277
229
  }
278
- function readJsonFile(filePath) {
279
- try {
280
- const content = fs.readFileSync(filePath, 'utf8');
281
- return JSON.parse(content);
282
- }
283
- catch {
284
- return null;
285
- }
286
- }
287
- function writeJsonFile(filePath, data) {
288
- fs.mkdirSync(path.dirname(filePath), { recursive: true });
289
- fs.writeFileSync(filePath, JSON.stringify(data, null, 2) + '\n', 'utf8');
290
- }
291
- // --- Browser Auth ---
292
230
  function openBrowser(url) {
293
231
  const platform = process.platform;
294
- const cmd = platform === 'win32' ? 'start ""'
295
- : platform === 'darwin' ? 'open'
296
- : 'xdg-open';
297
- exec(`${cmd} "${url}"`);
232
+ const cmd = platform === 'win32' ? `start "" "${url}"`
233
+ : platform === 'darwin' ? `open "${url}"`
234
+ : `xdg-open "${url}"`;
235
+ exec(cmd, () => {
236
+ // Ignore errors — user can always open manually
237
+ });
298
238
  }
299
239
  const SUCCESS_HTML = `<!DOCTYPE html>
300
- <html lang="en">
301
- <head><meta charset="utf-8"><title>SlyPlan - Authorized</title>
240
+ <html>
241
+ <head><title>SlyPlan Authorized</title>
302
242
  <style>
303
- body { margin: 0; min-height: 100vh; display: flex; align-items: center; justify-content: center;
304
- background: #09090b; color: #f4f4f5; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; }
305
- .card { text-align: center; padding: 3rem; }
306
- .check { width: 64px; height: 64px; margin: 0 auto 1.5rem; color: #22c55e; }
307
- h1 { font-size: 1.5rem; margin: 0 0 0.5rem; }
308
- p { color: #a1a1aa; font-size: 0.875rem; margin: 0; }
309
- </style>
310
- </head>
311
- <body>
312
- <div class="card">
313
- <svg class="check" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
314
- <path stroke-linecap="round" stroke-linejoin="round" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
315
- </svg>
243
+ body { background: #09090b; color: #e4e4e7; font-family: system-ui, sans-serif;
244
+ display: flex; align-items: center; justify-content: center; min-height: 100vh; margin: 0; }
245
+ .card { text-align: center; padding: 3rem; border-radius: 12px;
246
+ background: #18181b; border: 1px solid #27272a; max-width: 360px; }
247
+ .check { color: #22c55e; font-size: 3rem; margin-bottom: 1rem; }
248
+ h1 { font-size: 1.25rem; margin: 0 0 0.5rem; }
249
+ p { color: #71717a; font-size: 0.875rem; margin: 0; }
250
+ </style></head>
251
+ <body><div class="card">
252
+ <div class="check">&#10003;</div>
316
253
  <h1>Authorized!</h1>
317
254
  <p>You can close this tab and return to your terminal.</p>
318
- </div>
319
- </body>
320
- </html>`;
255
+ </div></body></html>`;
321
256
  const ERROR_HTML = (msg) => `<!DOCTYPE html>
322
- <html lang="en">
323
- <head><meta charset="utf-8"><title>SlyPlan - Error</title>
257
+ <html>
258
+ <head><title>SlyPlan Error</title>
324
259
  <style>
325
- body { margin: 0; min-height: 100vh; display: flex; align-items: center; justify-content: center;
326
- background: #09090b; color: #f4f4f5; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; }
327
- .card { text-align: center; padding: 3rem; }
328
- .icon { width: 64px; height: 64px; margin: 0 auto 1.5rem; color: #ef4444; }
329
- h1 { font-size: 1.5rem; margin: 0 0 0.5rem; }
330
- p { color: #a1a1aa; font-size: 0.875rem; margin: 0; }
331
- </style>
332
- </head>
333
- <body>
334
- <div class="card">
335
- <svg class="icon" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
336
- <path stroke-linecap="round" stroke-linejoin="round" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L4.082 16.5c-.77.833.192 2.5 1.732 2.5z"/>
337
- </svg>
260
+ body { background: #09090b; color: #e4e4e7; font-family: system-ui, sans-serif;
261
+ display: flex; align-items: center; justify-content: center; min-height: 100vh; margin: 0; }
262
+ .card { text-align: center; padding: 3rem; border-radius: 12px;
263
+ background: #18181b; border: 1px solid #27272a; max-width: 360px; }
264
+ .icon { color: #ef4444; font-size: 3rem; margin-bottom: 1rem; }
265
+ h1 { font-size: 1.25rem; margin: 0 0 0.5rem; }
266
+ p { color: #71717a; font-size: 0.875rem; margin: 0; }
267
+ </style></head>
268
+ <body><div class="card">
269
+ <div class="icon">&#10007;</div>
338
270
  <h1>Authorization Failed</h1>
339
271
  <p>${msg}</p>
340
- </div>
341
- </body>
342
- </html>`;
272
+ </div></body></html>`;
343
273
  function startLocalAuthServer() {
344
274
  return new Promise((resolve, reject) => {
345
275
  const server = http.createServer((req, res) => {
346
276
  const url = new URL(req.url ?? '/', `http://localhost`);
347
277
  if (url.pathname === '/callback') {
348
278
  const refreshToken = url.searchParams.get('refresh_token');
349
- const email = url.searchParams.get('email') ?? '';
350
- if (refreshToken) {
279
+ const email = url.searchParams.get('email');
280
+ if (refreshToken && email) {
351
281
  res.writeHead(200, { 'Content-Type': 'text/html' });
352
282
  res.end(SUCCESS_HTML);
353
283
  server.close();
@@ -355,42 +285,65 @@ function startLocalAuthServer() {
355
285
  }
356
286
  else {
357
287
  res.writeHead(400, { 'Content-Type': 'text/html' });
358
- res.end(ERROR_HTML('Missing refresh token in callback.'));
288
+ res.end(ERROR_HTML('Missing token or email in callback.'));
359
289
  server.close();
360
- reject(new Error('Missing refresh token in callback'));
290
+ reject(new Error('Missing token or email in callback'));
361
291
  }
362
292
  }
363
293
  else {
364
- res.writeHead(404, { 'Content-Type': 'text/plain' });
365
- res.end('Not found');
294
+ res.writeHead(404);
295
+ res.end();
366
296
  }
367
297
  });
368
- // Listen on random port
369
298
  server.listen(0, '127.0.0.1', () => {
370
299
  const addr = server.address();
371
300
  if (!addr || typeof addr === 'string') {
372
- reject(new Error('Failed to start local server'));
301
+ reject(new Error('Failed to get server address'));
373
302
  return;
374
303
  }
375
304
  const port = addr.port;
376
305
  const callbackUrl = `http://localhost:${port}/callback`;
377
- const authUrl = `${SLYPLAN_URL}/cli/auth?port=${port}&callback=${encodeURIComponent(callbackUrl)}`;
378
- log(` Opening browser...`);
379
- log(` If it doesn't open, visit:`);
380
- log(` ${authUrl}`);
306
+ const authUrl = `${SLYPLAN_ORIGIN}/cli/auth?port=${port}&callback=${encodeURIComponent(callbackUrl)}`;
307
+ log(` Opening browser to authorize...`);
308
+ log(` If it doesn't open, visit: ${authUrl}`);
381
309
  log('');
382
310
  openBrowser(authUrl);
383
311
  });
384
312
  // Timeout after 120 seconds
385
313
  setTimeout(() => {
386
314
  server.close();
387
- reject(new Error('Authorization timed out after 120 seconds. Please try again.'));
315
+ reject(new Error('Authorization timed out (120s). Try again.'));
388
316
  }, 120000);
389
317
  });
390
318
  }
319
+ async function validateCredentials(email, password) {
320
+ const client = createClient(SUPABASE_URL, SUPABASE_ANON_KEY, {
321
+ auth: { autoRefreshToken: false, persistSession: false },
322
+ });
323
+ const { data, error } = await client.auth.signInWithPassword({ email, password });
324
+ if (error)
325
+ return { ok: false, error: error.message };
326
+ return {
327
+ ok: true,
328
+ userEmail: data.user?.email ?? email,
329
+ refreshToken: data.session?.refresh_token,
330
+ };
331
+ }
332
+ function readJsonFile(filePath) {
333
+ try {
334
+ const content = fs.readFileSync(filePath, 'utf8');
335
+ return JSON.parse(content);
336
+ }
337
+ catch {
338
+ return null;
339
+ }
340
+ }
341
+ function writeJsonFile(filePath, data) {
342
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
343
+ fs.writeFileSync(filePath, JSON.stringify(data, null, 2) + '\n', 'utf8');
344
+ }
391
345
  // --- Settings Merge ---
392
346
  // MCP server config goes in .mcp.json (Claude Code reads servers from here)
393
- // Hooks go in .claude/settings.json
394
347
  function mergeMcpJson(existing, refreshToken) {
395
348
  const result = { ...existing };
396
349
  if (!result.mcpServers)
@@ -404,8 +357,15 @@ function mergeMcpJson(existing, refreshToken) {
404
357
  };
405
358
  return result;
406
359
  }
360
+ // Hooks go in .claude/settings.json
407
361
  function mergeSettings(existing) {
408
362
  const result = { ...existing };
363
+ // Remove any stale MCP server config (moved to .mcp.json)
364
+ if (result.mcpServers?.slyplan) {
365
+ delete result.mcpServers.slyplan;
366
+ if (Object.keys(result.mcpServers).length === 0)
367
+ delete result.mcpServers;
368
+ }
409
369
  // Hook config
410
370
  if (!result.hooks)
411
371
  result.hooks = {};
@@ -460,8 +420,8 @@ function removeFromMcpJson(existing) {
460
420
  }
461
421
  function removeFromSettings(existing) {
462
422
  const result = { ...existing };
463
- // Remove MCP server (legacy — was previously in settings.json)
464
- if (result.mcpServers) {
423
+ // Remove any stale MCP server config
424
+ if (result.mcpServers?.slyplan) {
465
425
  delete result.mcpServers.slyplan;
466
426
  if (Object.keys(result.mcpServers).length === 0)
467
427
  delete result.mcpServers;
@@ -486,8 +446,8 @@ async function runSetup() {
486
446
  const cwd = process.cwd();
487
447
  const claudeDir = path.join(cwd, '.claude');
488
448
  const hooksDir = path.join(claudeDir, 'hooks');
489
- const settingsPath = path.join(claudeDir, 'settings.json');
490
449
  const mcpJsonPath = path.join(cwd, '.mcp.json');
450
+ const settingsPath = path.join(claudeDir, 'settings.json');
491
451
  const postHookFilePath = path.join(hooksDir, 'slyplan-post-tool-sync.cjs');
492
452
  const preHookFilePath = path.join(hooksDir, 'slyplan-pre-tool-sync.cjs');
493
453
  const claudeMdPath = path.join(cwd, 'CLAUDE.md');
@@ -496,7 +456,7 @@ async function runSetup() {
496
456
  log(' ==============================');
497
457
  log(` Setting up in: ${cwd}`);
498
458
  log('');
499
- // Parse flags
459
+ // Parse flags (--email + --password for CI/scripted use)
500
460
  const args = process.argv.slice(3);
501
461
  let flagEmail = '';
502
462
  let flagPassword = '';
@@ -509,7 +469,7 @@ async function runSetup() {
509
469
  let refreshToken = '';
510
470
  let userEmail = '';
511
471
  if (flagEmail && flagPassword) {
512
- // CI/scripted mode: validate credentials and extract refresh token
472
+ // CI mode: validate credentials and extract refresh token
513
473
  process.stderr.write(' Validating credentials... ');
514
474
  const result = await validateCredentials(flagEmail, flagPassword);
515
475
  if (!result.ok) {
@@ -526,8 +486,6 @@ async function runSetup() {
526
486
  }
527
487
  else {
528
488
  // Interactive mode: browser-based auth
529
- log(' Waiting for authorization in browser...');
530
- log('');
531
489
  try {
532
490
  const result = await startLocalAuthServer();
533
491
  refreshToken = result.refreshToken;
@@ -535,7 +493,6 @@ async function runSetup() {
535
493
  log(` Authorized as ${userEmail}`);
536
494
  }
537
495
  catch (err) {
538
- log('');
539
496
  log(` ${err.message}`);
540
497
  process.exit(1);
541
498
  }
@@ -547,15 +504,12 @@ async function runSetup() {
547
504
  log(' [+] Created .claude/hooks/slyplan-pre-tool-sync.cjs');
548
505
  fs.writeFileSync(postHookFilePath, POST_HOOK_FILE_CONTENT, 'utf8');
549
506
  log(' [+] Created .claude/hooks/slyplan-post-tool-sync.cjs');
550
- // 2a. Merge .mcp.json (MCP server config Claude Code reads servers from here)
551
- let existingMcp = {};
552
- const rawMcp = readJsonFile(mcpJsonPath);
553
- if (rawMcp)
554
- existingMcp = rawMcp;
507
+ // 2. Write MCP server config to .mcp.json
508
+ const existingMcp = readJsonFile(mcpJsonPath) ?? {};
555
509
  const mergedMcp = mergeMcpJson(existingMcp, refreshToken);
556
510
  writeJsonFile(mcpJsonPath, mergedMcp);
557
- log(' [+] Updated .mcp.json (MCP server)');
558
- // 2b. Merge .claude/settings.json (hooks)
511
+ log(' [+] Updated .mcp.json');
512
+ // 3. Write hooks to .claude/settings.json
559
513
  let existingSettings = {};
560
514
  const rawSettings = readJsonFile(settingsPath);
561
515
  if (rawSettings === null && fs.existsSync(settingsPath)) {
@@ -571,7 +525,7 @@ async function runSetup() {
571
525
  const mergedSettings = mergeSettings(existingSettings);
572
526
  writeJsonFile(settingsPath, mergedSettings);
573
527
  log(' [+] Updated .claude/settings.json (hooks)');
574
- // 3. Optional CLAUDE.md append
528
+ // 4. Optional CLAUDE.md append
575
529
  const appendAnswer = await promptUser(' Append SlyPlan sync instructions to CLAUDE.md? [Y/n]: ');
576
530
  if (appendAnswer.toLowerCase() !== 'n') {
577
531
  if (fs.existsSync(claudeMdPath)) {
@@ -606,8 +560,8 @@ async function runSetup() {
606
560
  async function runRemove() {
607
561
  const cwd = process.cwd();
608
562
  const claudeDir = path.join(cwd, '.claude');
609
- const settingsPath = path.join(claudeDir, 'settings.json');
610
563
  const mcpJsonPath = path.join(cwd, '.mcp.json');
564
+ const settingsPath = path.join(claudeDir, 'settings.json');
611
565
  const postHookFilePath = path.join(claudeDir, 'hooks', 'slyplan-post-tool-sync.cjs');
612
566
  const preHookFilePath = path.join(claudeDir, 'hooks', 'slyplan-pre-tool-sync.cjs');
613
567
  log('');
@@ -628,7 +582,7 @@ async function runRemove() {
628
582
  log(` [=] .claude/hooks/${name} not found — skipped`);
629
583
  }
630
584
  }
631
- // 2a. Clean .mcp.json (MCP server)
585
+ // 2. Clean .mcp.json
632
586
  const existingMcp = readJsonFile(mcpJsonPath);
633
587
  if (existingMcp) {
634
588
  const cleanedMcp = removeFromMcpJson(existingMcp);
@@ -641,7 +595,7 @@ async function runRemove() {
641
595
  log(' [-] Removed SlyPlan from .mcp.json');
642
596
  }
643
597
  }
644
- // 2b. Clean settings.json (hooks + legacy MCP server)
598
+ // 3. Clean settings.json
645
599
  const existing = readJsonFile(settingsPath);
646
600
  if (existing) {
647
601
  const cleaned = removeFromSettings(existing);
package/dist/supabase.js CHANGED
@@ -7,51 +7,34 @@ export const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY, {
7
7
  auth: { autoRefreshToken: true, persistSession: false },
8
8
  });
9
9
  let userId = null;
10
- function findMcpJsonPath() {
11
- // Walk up from cwd looking for .mcp.json
12
- let dir = process.cwd();
13
- while (true) {
14
- const candidate = path.join(dir, '.mcp.json');
15
- if (fs.existsSync(candidate))
16
- return candidate;
17
- const parent = path.dirname(dir);
18
- if (parent === dir)
19
- break;
20
- dir = parent;
21
- }
22
- return null;
23
- }
24
10
  function persistNewRefreshToken(newToken) {
11
+ const mcpJsonPath = path.join(process.cwd(), '.mcp.json');
25
12
  try {
26
- const mcpPath = findMcpJsonPath();
27
- if (!mcpPath)
28
- return;
29
- const raw = fs.readFileSync(mcpPath, 'utf8');
30
- const config = JSON.parse(raw);
31
- const env = config?.mcpServers?.slyplan?.env;
32
- if (!env)
33
- return;
34
- env.SLYPLAN_REFRESH_TOKEN = newToken;
35
- fs.writeFileSync(mcpPath, JSON.stringify(config, null, 2) + '\n', 'utf8');
13
+ const content = fs.readFileSync(mcpJsonPath, 'utf8');
14
+ const config = JSON.parse(content);
15
+ if (config?.mcpServers?.slyplan?.env) {
16
+ config.mcpServers.slyplan.env.SLYPLAN_REFRESH_TOKEN = newToken;
17
+ fs.writeFileSync(mcpJsonPath, JSON.stringify(config, null, 2) + '\n', 'utf8');
18
+ }
36
19
  }
37
20
  catch {
38
- // Silenttoken rotation is best-effort
21
+ // Best-effortdon't crash if file is missing/malformed
39
22
  }
40
23
  }
41
24
  export async function authenticate() {
42
25
  const refreshToken = process.env.SLYPLAN_REFRESH_TOKEN;
43
26
  const email = process.env.SLYPLAN_EMAIL;
44
27
  const password = process.env.SLYPLAN_PASSWORD;
45
- // Prefer refresh token (browser auth flow)
28
+ // Prefer refresh token (new browser-based flow)
46
29
  if (refreshToken) {
47
30
  const { data, error } = await supabase.auth.refreshSession({ refresh_token: refreshToken });
48
31
  if (error) {
49
32
  console.error(`Token refresh failed: ${error.message}`);
50
- console.error('Re-run "npx slyplan-mcp setup" to re-authorize.');
33
+ console.error('Run "npx slyplan-mcp setup" to re-authenticate.');
51
34
  process.exit(1);
52
35
  }
53
36
  userId = data.user.id;
54
- // Persist rotated token for next startup
37
+ // Self-healing: persist rotated token
55
38
  if (data.session?.refresh_token && data.session.refresh_token !== refreshToken) {
56
39
  persistNewRefreshToken(data.session.refresh_token);
57
40
  }
@@ -67,8 +50,8 @@ export async function authenticate() {
67
50
  userId = data.user.id;
68
51
  return;
69
52
  }
70
- console.error('Missing SLYPLAN_REFRESH_TOKEN (or legacy SLYPLAN_EMAIL + SLYPLAN_PASSWORD).');
71
- console.error('Run "npx slyplan-mcp setup" to authorize.');
53
+ console.error('Missing SLYPLAN_REFRESH_TOKEN (or SLYPLAN_EMAIL + SLYPLAN_PASSWORD).');
54
+ console.error('Run "npx slyplan-mcp setup" to configure authentication.');
72
55
  process.exit(1);
73
56
  }
74
57
  export function getUserId() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "slyplan-mcp",
3
- "version": "1.4.2",
3
+ "version": "1.5.1",
4
4
  "description": "MCP server for Slyplan — visual project management via Claude",
5
5
  "type": "module",
6
6
  "bin": {