diff --git a/README.md b/README.md index b21f6a0..e4fc9d5 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,7 @@ Click the workspace badge to switch between isolated canvases — each workspace - [Quick Start](#quick-start) - [Configuration](#configuration) - [Verify Installation](#verify-installation) +- [Updating](#updating) - [How We Differ from the Official Excalidraw MCP](#how-we-differ-from-the-official-excalidraw-mcp) - [What Changed From Upstream](#what-changed-from-upstream) - [Architecture](#architecture) @@ -280,6 +281,119 @@ open http://localhost:3000 If the health check fails, see [Troubleshooting](#troubleshooting). +## Updating + +Already installed a previous version? The interactive update wizard is the easiest way to update everything — MCP server **and** agent skills — in one go. + +### Interactive Update (recommended) + +```bash +npx @sanjibdevnath/mcp-excalidraw-local@latest update +``` + +
+Example session + +``` +$ npx @sanjibdevnath/mcp-excalidraw-local@latest update + + Excalidraw MCP — Update v1.2.0 + + [1/2] Skill Update + + Found 2 existing skill installation(s): + [1] Cursor (global) — ~/.cursor/skills/excalidraw-skill + [2] Claude Code (global) — ~/.claude/skills/excalidraw-skill + + Update all 2 installation(s) to v1.2.0? [Y/n]: Y + ✔ Updated Cursor (global) — ~/.cursor/skills/excalidraw-skill + ✔ Updated Claude Code (global) — ~/.claude/skills/excalidraw-skill + + 2/2 skill(s) updated. + + [2/2] MCP Configuration + Re-apply MCP server config? (overwrites existing entry) [y/N]: N + MCP config unchanged. + + Update complete! Restart your MCP client to pick up changes. +``` +
+ +The update wizard: +1. **Finds all existing skill installations** across Cursor, Claude Code, and Codex CLI (both global and local scopes) +2. **Updates them in-place** with the latest skill files (SKILL.md, cheatsheet, geometric-thinking reference, helper scripts) +3. **Offers to install** the skill for any detected agent that doesn't have it yet +4. **Optionally re-applies MCP config** if needed + +> **Why this matters:** The agent skill contains workflow guidance, sizing rules, color palettes, and anti-patterns that evolve alongside the MCP tools. Updating the MCP server without updating the skill means your AI agent is working with stale instructions. + +### Manual Update by Installation Method + +If you prefer to update manually, follow the steps for your installation method, then restart your MCP client. + +#### npx users + +If your MCP config uses `npx -y @sanjibdevnath/mcp-excalidraw-local`, npx caches the package locally and won't automatically fetch new versions. + +**Option A — Clear the cache (one-time):** +```bash +npm cache clean --force +``` +Then restart your MCP client. npx will download the latest version on next launch. + +**Option B — Pin to `@latest` in your MCP config (permanent fix):** + +Update the `args` in your MCP config to include `@latest`: +```json +{ + "mcpServers": { + "excalidraw-canvas": { + "command": "npx", + "args": ["-y", "@sanjibdevnath/mcp-excalidraw-local@latest"], + "env": { "CANVAS_PORT": "3000" } + } + } +} +``` + +This ensures npx always checks for the newest published version. + +#### From-source users + +```bash +cd mcp-excalidraw-local +git pull origin main +npm install +npm run build +``` + +#### Docker users + +```bash +docker pull sanjibdevnath/mcp-excalidraw-local:latest +docker pull sanjibdevnath/mcp-excalidraw-local-canvas:latest +``` + +Then recreate your containers (`docker compose up -d` or `docker run` again). + +#### Updating the agent skill manually + +If you skipped the interactive update, copy the skill files yourself: +```bash +cp -R skills/excalidraw-skill ~/.cursor/skills/excalidraw-skill +cp -R skills/excalidraw-skill ~/.claude/skills/excalidraw-skill +``` + +### Verify the update + +```bash +# Check the running version +curl -s http://localhost:3000/health + +# Or check the installed package version +npx @sanjibdevnath/mcp-excalidraw-local --version +``` + ## How We Differ from the Official Excalidraw MCP Excalidraw now has an [official MCP](https://github.com/excalidraw/excalidraw-mcp) — it's great for quick, prompt-to-diagram generation rendered inline in chat. We solve a different problem. diff --git a/package.json b/package.json index 37bd5b4..4a23a60 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "production": "npm run build && npm run canvas", "prepublishOnly": "npm run build", "setup": "node dist/index.js setup", + "update": "node dist/index.js update", "type-check": "npx tsc --noEmit", "test": "vitest run", "test:watch": "vitest", diff --git a/src/index.ts b/src/index.ts index 791ab53..36f6f80 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2727,12 +2727,17 @@ if (isMainModule()) { process.stderr.write(`Setup failed: ${(error as Error).message}\n`); process.exit(1); }); + } else if (arg === 'update') { + import('./setup.js').then(m => m.runUpdate()).catch(error => { + process.stderr.write(`Update failed: ${(error as Error).message}\n`); + process.exit(1); + }); } else if (arg === '--help' || arg === '-h' || arg === '--version' || arg === '-v') { const pkgPath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', 'package.json'); try { const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8')); if (arg === '--help' || arg === '-h') { - process.stdout.write(`${pkg.name} v${pkg.version}\n\nUsage:\n mcp-excalidraw-local Start MCP server (stdio transport)\n mcp-excalidraw-local setup Interactive setup wizard\n mcp-excalidraw-local --help Show this help\n mcp-excalidraw-local --version Show version\n`); + process.stdout.write(`${pkg.name} v${pkg.version}\n\nUsage:\n mcp-excalidraw-local Start MCP server (stdio transport)\n mcp-excalidraw-local setup Interactive setup wizard\n mcp-excalidraw-local update Update agent skills and MCP config\n mcp-excalidraw-local --help Show this help\n mcp-excalidraw-local --version Show version\n`); } else { process.stdout.write(`${pkg.version}\n`); } diff --git a/src/setup.ts b/src/setup.ts index 4457eec..643f142 100644 --- a/src/setup.ts +++ b/src/setup.ts @@ -346,6 +346,167 @@ function printManualConfig(): void { `); } +// ── Update ─────────────────────────────────────────────────── + +interface SkillInstallation { + agent: AgentDef; + scope: 'global' | 'local'; + path: string; + exists: boolean; +} + +function getPackageVersion(): string { + try { + const pkgPath = path.resolve(__dirname, '..', 'package.json'); + const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8')); + return pkg.version ?? 'unknown'; + } catch { + return 'unknown'; + } +} + +function findExistingSkillInstalls(): SkillInstallation[] { + const agents = getAgents(); + const installs: SkillInstallation[] = []; + for (const agent of agents) { + for (const scope of ['global', 'local'] as const) { + const skillDir = path.join(agent.skillBasePaths[scope], 'excalidraw-skill'); + installs.push({ + agent, + scope, + path: skillDir, + exists: fs.existsSync(path.join(skillDir, 'SKILL.md')), + }); + } + } + return installs; +} + +export async function runUpdate(): Promise { + if (!process.stdin.isTTY) { + process.stderr.write('Error: Update requires an interactive terminal.\n'); + process.exit(1); + } + + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, + }); + + const version = getPackageVersion(); + process.stdout.write(`\n ${BOLD}Excalidraw MCP — Update${RESET} ${DIM}v${version}${RESET}\n`); + + try { + // ── Phase 1: Detect existing skill installations ────────── + heading('1/2', 'Skill Update'); + + const allInstalls = findExistingSkillInstalls(); + const existing = allInstalls.filter(i => i.exists); + const missing = allInstalls.filter(i => !i.exists); + const detectedAgents = detectInstalledAgents(); + + const skillSource = path.resolve(__dirname, '..', 'skills', 'excalidraw-skill'); + if (!fs.existsSync(skillSource)) { + fail(`Skill source not found at ${skillSource}`); + fail('This can happen with corrupted installs. Try: npx @sanjibdevnath/mcp-excalidraw-local@latest setup'); + rl.close(); + return; + } + + if (existing.length > 0) { + process.stdout.write(`\n Found ${CYAN}${existing.length}${RESET} existing skill installation(s):\n`); + existing.forEach((inst, i) => { + const label = `${inst.agent.name} (${inst.scope})`; + process.stdout.write(` ${CYAN}[${i + 1}]${RESET} ${label} — ${DIM}${inst.path}${RESET}\n`); + }); + + const doUpdate = await confirm(rl, `\n Update all ${existing.length} installation(s) to v${version}?`); + if (doUpdate) { + let updated = 0; + for (const inst of existing) { + try { + copyDirSync(skillSource, inst.path); + ok(`Updated ${inst.agent.name} (${inst.scope}) — ${inst.path}`); + updated++; + } catch (err) { + fail(`Failed to update ${inst.path}: ${(err as Error).message}`); + } + } + process.stdout.write(`\n ${GREEN}${updated}/${existing.length}${RESET} skill(s) updated.\n`); + } else { + info(`${DIM}Skipped skill update.${RESET}`); + } + } else { + info('No existing skill installations found.'); + } + + // Offer to install for detected agents that don't have the skill + const agentsWithoutSkill = detectedAgents.filter(agent => + !existing.some(inst => inst.agent.name === agent.name), + ); + + if (agentsWithoutSkill.length > 0) { + process.stdout.write(`\n Agents without the skill:\n`); + agentsWithoutSkill.forEach((a, i) => { + process.stdout.write(` ${YELLOW}[${i + 1}]${RESET} ${a.name}\n`); + }); + + const doInstall = await confirm(rl, 'Install the skill for these agents?'); + if (doInstall) { + for (const agent of agentsWithoutSkill) { + const scopeAnswer = await ask(rl, `\n ${agent.name} — scope? [G]lobal / [l]ocal: `); + const scope = scopeAnswer.trim().toLowerCase() === 'l' ? 'local' : 'global'; + const destDir = path.join(agent.skillBasePaths[scope], 'excalidraw-skill'); + + try { + fs.mkdirSync(destDir, { recursive: true }); + copyDirSync(skillSource, destDir); + ok(`Installed to ${destDir}`); + } catch (err) { + fail(`Failed to install to ${destDir}: ${(err as Error).message}`); + } + } + } + } + + // ── Phase 2: MCP config check ──────────────────────────── + heading('2/2', 'MCP Configuration'); + + const wantConfig = await confirm(rl, 'Re-apply MCP server config? (overwrites existing entry)', false); + if (wantConfig) { + for (const agent of detectedAgents) { + if (agent.mcpConfigType === 'json-file' && agent.mcpConfigPath) { + const doIt = await confirm(rl, `${agent.name} — update ${agent.mcpConfigPath}?`); + if (doIt) { + try { + mergeJsonConfig(agent.mcpConfigPath); + ok(`Updated 'excalidraw-canvas' in ${agent.mcpConfigPath}`); + } catch (err) { + fail(`Failed: ${(err as Error).message}`); + } + } + } else if (agent.mcpConfigType === 'cli-command' && agent.mcpCliCommand) { + const doIt = await confirm(rl, `${agent.name} — re-register via CLI?`); + if (doIt) { + try { + execSync(agent.mcpCliCommand, { stdio: 'inherit' }); + ok(`Re-registered 'excalidraw-canvas' via ${agent.name} CLI`); + } catch (err) { + fail(`CLI registration failed: ${(err as Error).message}`); + } + } + } + } + } else { + info(`${DIM}MCP config unchanged.${RESET}`); + } + + process.stdout.write(`\n ${GREEN}${BOLD}Update complete!${RESET} Restart your MCP client to pick up changes.\n\n`); + } finally { + rl.close(); + } +} + // ── Main ───────────────────────────────────────────────────── export async function runSetup(): Promise {