feat(cli): add interactive update command for skill and config updates (#10)

Existing users who update the npm package often forget to update the agent
skill files, leaving their AI agent working with stale workflow guidance
(sizing rules, color palettes, anti-patterns). The new `update` subcommand
solves this by detecting and updating all skill installations automatically.

🔧 Interactive update wizard:
- Scan all agent directories (Cursor, Claude Code, Codex CLI) across global
  and local scopes for existing excalidraw-skill installations
- Batch-update found installations with a single Y/n confirmation
- Offer to install the skill for detected agents that don't have it yet
- Optionally re-apply MCP config (defaults to no for non-breaking updates)

📝 Documentation:
- Rewrite README "Updating" section to lead with the interactive command
- Add collapsible example session showing the update flow
- Retain manual update methods as fallback for each installation path

🎯 Ensures skill files stay in sync with MCP tools across releases,
preventing the subtle drift where new tool capabilities exist but the
agent's workflow guidance doesn't reference them.
This commit is contained in:
Sanjib Devnath
2026-03-13 22:37:58 +05:30
committed by GitHub
parent c16e1f4cc3
commit 86abe92fa2
4 changed files with 282 additions and 1 deletions
+114
View File
@@ -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
```
<details>
<summary>Example session</summary>
```
$ 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.
```
</details>
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.
+1
View File
@@ -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",
+6 -1
View File
@@ -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`);
}
+161
View File
@@ -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<void> {
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<void> {