@ammduncan/easel 0.2.0 → 0.2.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.
package/CHANGELOG.md ADDED
@@ -0,0 +1,48 @@
1
+ # Changelog
2
+
3
+ All notable changes to easel. This project adheres to [Semantic Versioning](https://semver.org/).
4
+
5
+ ## 0.2.2 — 2026-05-22
6
+
7
+ ### Fixed
8
+ - **MCP servers wired via `npx -y @ammduncan/easel` now boot the MCP server instead of printing CLI help.** When stdin isn't a TTY (i.e. the process is launched over a pipe by an MCP client), the bin transparently boots the MCP server. Interactive terminal use still shows help. Previously, Claude Desktop, Cursor, and Windsurf saw the CLI's help text on stdout and reported `Unexpected token 'e' is not valid JSON` for every line.
9
+
10
+ ### Added
11
+ - Explicit `easel mcp` subcommand that runs the stdio MCP server in the foreground (used internally by the no-TTY auto-detection; available as an explicit entry point for clients that want it).
12
+
13
+ ### Changed
14
+ - `dist/mcp.js` now exports `main()` and only auto-runs when invoked directly (not when imported), so the CLI can route to it without double-booting.
15
+
16
+ ## 0.2.1 — 2026-05-22
17
+
18
+ ### Added
19
+ - `easel setup --client cursor|claude-desktop|windsurf` writes the MCP entry into each client's config file, merging into any existing `mcpServers` map. Sibling top-level keys are preserved. (`src/client-setup.ts`)
20
+ - README leads with the npx install; per-client install commands documented; generic JSON snippet for any other MCP-speaking client.
21
+ - This `CHANGELOG.md`.
22
+
23
+ ### Notes
24
+ - The Claude Code setup path (bare `easel setup`) is unchanged.
25
+
26
+ ## 0.2.0 — 2026-05-22
27
+
28
+ ### Added
29
+ - Published to npm as [`@ammduncan/easel`](https://www.npmjs.com/package/@ammduncan/easel). Any MCP client can now install with `npx -y @ammduncan/easel` — no clone, no build.
30
+ - `EASEL_SESSION_ID` env var as the highest-priority override for session resolution.
31
+ - Synthetic PPID-derived session id as a final fallback when no Claude Code hook fired and no transcript exists — gives Cursor, Claude Desktop, Windsurf, and any other MCP client a stable session per chat with zero setup beyond the MCP entry.
32
+ - MCP-side auto-open: non-CC clients have no `SessionStart` hook to open the tab, so the MCP server now opens it on the first tool call when no hook file exists for this PPID. One-shot guard avoids re-opening if the user closes the tab. Claude Code behaviour unchanged.
33
+ - `LICENCE` file (MIT).
34
+
35
+ ### Changed
36
+ - `resolveClaudeSessionId()` is now total — always returns a string. Dead "no session id" error branches removed from `cli.ts` and `mcp.ts`.
37
+ - `package.json` reshaped for npm publishing: scoped name `@ammduncan/easel`, MIT licence, repo/homepage/bugs URLs, keywords for discovery, `engines.node >=20`, `files` whitelist (only bin/dist/scripts/skills/README/LICENCE in the tarball), `prepublishOnly` script, `publishConfig.access=public`.
38
+ - Package size: 47 kB tarball, 165 kB unpacked, 3 runtime deps.
39
+
40
+ ## 0.1.0 — internal baseline
41
+
42
+ The pre-publish baseline. Highlights from the unreleased history:
43
+
44
+ - Renamed project from `claude-display` to `easel`; data root migrated from `~/.claude-display` to `~/.easel` with one-shot `mv` on first start. Browser localStorage keys (`claude-display:*`) and postMessage events migrated to the `easel:*` namespace with a browser-side migration shim. Internal `__CLAUDE_DISPLAY__` window global renamed to `__EASEL__`. Env vars renamed (`CLAUDE_DISPLAY_PORT` → `EASEL_PORT` with legacy fallback).
45
+ - Dropped `jq` as a runtime dependency. Ported `scripts/easel-session-id.sh` to `scripts/easel-session-id.mjs` (Node stdlib only).
46
+ - Added `easel restart` command.
47
+ - MCP tool names finalised as `push`, `open`, `config`, `label` (invoked as `mcp__easel__*`).
48
+ - Skill folder canonicalised at `skills/using-easel/`.
package/README.md CHANGED
@@ -1,37 +1,68 @@
1
1
  # easel
2
2
 
3
- A live browser tab for every Claude Code session. Agents push HTML — explanations, mockups, diagrams, diffs, comparisons — to a scrolling feed you keep open in split-screen. No more wall-of-text in the terminal.
3
+ A live browser tab for every AI coding session. Agents push HTML — explanations, mockups, diagrams, diffs, comparisons — to a scrolling feed you keep open in split-screen. No more wall-of-text in the terminal.
4
4
 
5
5
  ```
6
- ┌──────────── Claude Code (left) ────────────┐ ┌────── easel (right) ──────────────┐
6
+ ┌──────────── agent (left) ──────────────────┐ ┌────── easel (right) ──────────────┐
7
7
  │ │ │ s/<session-id> • 3 pushes • live│
8
8
  │ > walk me through the new auth flow │ │ ───────────────────────────────── │
9
9
  │ │ │ #1 Auth flow overview │
10
- │ pushed to display ↗ — #1 │ │ ┌────────────────────────────────┐│
10
+ │ pushed to easel ↗ — #1 │ │ ┌────────────────────────────────┐│
11
11
  │ │ │ │ Three actors talk to each… ││
12
12
  │ > what could break? │ │ └────────────────────────────────┘│
13
13
  │ │ │ │
14
- │ pushed to display ↗ — #2 │ │ #2 Failure modes │
14
+ │ pushed to easel ↗ — #2 │ │ #2 Failure modes │
15
15
  └────────────────────────────────────────────┘ └────────────────────────────────────┘
16
16
  ```
17
17
 
18
+ Works with **Claude Code**, **Cursor**, **Claude Desktop**, **Windsurf**, and any other MCP-speaking client.
19
+
18
20
  ## Why
19
21
 
20
- Long markdown explanations bury what the agent is actually doing. Visual content (mockups, comparisons, diagrams) is even worse in a TTY. `easel` gives each chat session its own browser tab, and a single MCP tool — `push` — that the agent uses proactively. The terminal stays as a conversation log; the browser carries the substance.
22
+ Long markdown explanations bury what the agent is actually doing. Visual content (mockups, comparisons, diagrams) is even worse in a TTY. easel gives each chat session its own browser tab, and a single MCP tool — `push` — that the agent uses proactively. The terminal stays as a conversation log; the browser carries the substance.
21
23
 
22
24
  ## Install
23
25
 
24
- Requires Node 20+, `git`, and Claude Code.
26
+ Requires Node 20+.
27
+
28
+ ### Claude Code
29
+
30
+ ```bash
31
+ npx -y @ammduncan/easel setup
32
+ ```
33
+
34
+ That registers the MCP at user scope, installs the `using-easel` skill so the agent knows when to push, and adds the `SessionStart` hooks that resolve session IDs and auto-open the tab. Restart Claude Code and you're done.
35
+
36
+ ### Cursor / Claude Desktop / Windsurf
37
+
38
+ One command per client:
25
39
 
26
40
  ```bash
27
- curl -fsSL https://raw.githubusercontent.com/AmmDuncan/easel/main/scripts/install.sh | bash
41
+ npx -y @ammduncan/easel setup --client cursor
42
+ npx -y @ammduncan/easel setup --client claude-desktop
43
+ npx -y @ammduncan/easel setup --client windsurf
28
44
  ```
29
45
 
30
- The installer clones to `~/.local/share/easel` (override with `EASEL_DIR=…`), runs `npm install && npm run build`, then registers the MCP at user scope and adds two `SessionStart` hooks (session-id capture + auto-open tab). Idempotent safe to re-run to update.
46
+ Each writes the MCP entry to the client's config file (`~/.cursor/mcp.json`, `~/Library/Application Support/Claude/claude_desktop_config.json`, or `~/.codeium/windsurf/mcp_config.json`). Restart the client to load it.
31
47
 
32
- Restart Claude Code afterwards.
48
+ ### Any other MCP client
33
49
 
34
- ### Manual install
50
+ Drop this snippet into your client's MCP config:
51
+
52
+ ```json
53
+ {
54
+ "mcpServers": {
55
+ "easel": {
56
+ "command": "npx",
57
+ "args": ["-y", "@ammduncan/easel"]
58
+ }
59
+ }
60
+ }
61
+ ```
62
+
63
+ The MCP child mints its own session from its parent process ID — no hook required. Set `EASEL_SESSION_ID` in the client's `env` block if you want to pin a specific session.
64
+
65
+ ### From source (for contributors)
35
66
 
36
67
  ```bash
37
68
  git clone https://github.com/AmmDuncan/easel.git ~/work/tools/easel
@@ -56,20 +87,20 @@ Agents invoke them as `mcp__easel__push`, `mcp__easel__open`, etc.
56
87
  - **Presets**: `paper` (warm pitstop-style, amber accent — default), `aurora` (deep canvas + violet glow halos), `slate` (cool neutral, cyan accent)
57
88
  - **Themes**: light / dark, with sun-moon toggle in the topbar
58
89
  - **Density**: `carded` (bordered cards) or `flat` (no chrome, whitespace separates pushes)
59
- - Three swatches + density toggle live in the topbar; config persists in `~/.easel/config.json` and SSE-broadcasts across all open tabs
90
+ - Three swatches + density toggle live in the topbar; config persists in `~/.easel/config.json` and SSE-broadcasts across all open tabs.
60
91
 
61
92
  ## Sessions
62
93
 
63
- - Each Claude Code session gets its own URL: `localhost:7878/s/<session-id>`
64
- - Session IDs come from Claude Code itself (via the pitstop-style SessionStart hook)
65
- - Sessions auto-rename to `cwd-basename` by default; you can rename them via the click-to-edit label in the topbar, or the agent can via the `label` tool
66
- - Idle sessions (>24h since last push) are GC'd every 10 minutes
67
- - Up to 50 pushes per session; oldest evicted from disk first
68
- - Per-push delete (trash icon on each card) + per-session delete (hover any row in the switcher or index)
94
+ - Each chat session gets its own URL: `localhost:7878/s/<session-id>`
95
+ - Session IDs come from the agent client. Claude Code provides them via a SessionStart hook; other MCP clients fall through to a stable PPID-derived id for the MCP child. Override either via the `EASEL_SESSION_ID` env var.
96
+ - Sessions auto-rename to `cwd-basename` by default; you can rename them via the click-to-edit label in the topbar, or the agent can via the `label` tool.
97
+ - Idle sessions (>24h since last push) are GC'd every 10 minutes.
98
+ - Up to 50 pushes per session; oldest evicted from disk first.
99
+ - Per-push delete (trash icon on each card) + per-session delete (hover any row in the switcher or index).
69
100
 
70
101
  ## Tool surface
71
102
 
72
- The MCP exposes one server (`display`) with four tools. HTML is rendered in a sandboxed iframe (`sandbox="allow-scripts"`) with a baseline design system injected — off-white / charcoal, Inter, presentation-scale typography — so plain `<h1>/<h2>/<p>` markup looks right without extra CSS. Authors can also write a full `<!DOCTYPE html>` document and take ownership of styling.
103
+ The MCP exposes one server (`easel`) with four tools. HTML is rendered in a sandboxed iframe (`sandbox="allow-scripts"`) with a baseline design system injected — off-white / charcoal, Inter, presentation-scale typography — so plain `<h1>/<h2>/<p>` markup looks right without extra CSS. Authors can also write a full `<!DOCTYPE html>` document and take ownership of styling.
73
104
 
74
105
  Inside pushed HTML, semantic chips are available out of the box:
75
106
 
@@ -84,6 +115,20 @@ Inside pushed HTML, semantic chips are available out of the box:
84
115
 
85
116
  Each is themed for both light and dark with a soft outer glow.
86
117
 
118
+ ## CLI
119
+
120
+ ```
121
+ easel open ensure server is running, open this session's tab
122
+ easel url print this session's URL
123
+ easel config print / set { preset, theme, density }
124
+ easel setup Claude Code: hooks + MCP + skill
125
+ easel setup --client <name> register the MCP in another client (cursor, claude-desktop, windsurf)
126
+ easel restart kill + respawn the HTTP server (handy after a build)
127
+ easel update git pull + build + setup (clone installs only)
128
+ easel server run the HTTP server in the foreground (debug)
129
+ easel version
130
+ ```
131
+
87
132
  ## Files
88
133
 
89
134
  ```
@@ -93,10 +138,11 @@ src/
93
138
  http-entry.ts process entry for the HTTP server
94
139
  server-manager.ts lockfile + spawn coordination
95
140
  session-store.ts disk persistence + retention sweep
96
- session-id.ts 3-tier resolver (env / hook file / transcript scan)
141
+ session-id.ts 5-tier resolver (env / hook file / transcript scan / synthetic PPID)
97
142
  config-store.ts preset / theme / density persistence
98
- paths.ts shared constants
99
- cli.ts `easel open|url|setup|config|server|version`
143
+ client-setup.ts per-client config writers (cursor, claude-desktop, windsurf)
144
+ paths.ts shared constants + legacy-dir migration
145
+ cli.ts `easel open|url|setup|config|server|restart|update|version`
100
146
  client/
101
147
  viewer.html single-session feed
102
148
  index.html sessions index page
@@ -106,7 +152,7 @@ src/
106
152
  index.js index page client
107
153
  scripts/
108
154
  easel-session-id.mjs SessionStart hook (Node, zero deps)
109
- install.sh one-shot installer
155
+ install.sh one-shot installer (clone installs)
110
156
  copy-client.mjs build-time copy of client assets
111
157
  bin/
112
158
  easel shebang → dist/cli.js
package/dist/cli.js CHANGED
@@ -8,6 +8,7 @@ import { ensureHttpServer, readLock } from "./server-manager.js";
8
8
  import { resolveClaudeSessionId } from "./session-id.js";
9
9
  import { HOOK_DIR, DATA_ROOT } from "./paths.js";
10
10
  import { registerSession } from "./session-store.js";
11
+ import { listClients, setupClient, } from "./client-setup.js";
11
12
  const __dirname = dirname(fileURLToPath(import.meta.url));
12
13
  const PROJECT_ROOT = resolve(__dirname, "..");
13
14
  function help() {
@@ -22,8 +23,12 @@ Usage:
22
23
  easel config preset paper set preset to paper | aurora | slate
23
24
  easel config theme dark set theme to light | dark
24
25
  easel config preset aurora theme light set both at once
25
- easel setup install SessionStart hook + register MCP in ~/.claude/settings.json
26
+ easel setup install Claude Code hook + register MCP in ~/.claude/settings.json
27
+ easel setup --client cursor register the MCP in Cursor's config
28
+ easel setup --client claude-desktop register the MCP in Claude Desktop's config
29
+ easel setup --client windsurf register the MCP in Windsurf's config
26
30
  easel update git pull + npm install + build + setup (re-runs setup to apply new conventions)
31
+ easel mcp run the stdio MCP server in the foreground (used by clients)
27
32
  easel restart kill the running HTTP server and respawn it (picks up new builds/paths)
28
33
  easel server run the HTTP server in the foreground (debug)
29
34
  easel version
@@ -330,9 +335,32 @@ async function main() {
330
335
  case "url":
331
336
  await cmdUrl();
332
337
  return;
333
- case "setup":
338
+ case "setup": {
339
+ const clientIdx = rest.indexOf("--client");
340
+ if (clientIdx !== -1) {
341
+ const name = rest[clientIdx + 1];
342
+ if (!name) {
343
+ console.error(`[easel] --client requires a name. Available: ${listClients().join(", ")}`);
344
+ process.exitCode = 1;
345
+ return;
346
+ }
347
+ if (!listClients().includes(name)) {
348
+ console.error(`[easel] unknown client "${name}". Available: ${listClients().join(", ")}`);
349
+ process.exitCode = 1;
350
+ return;
351
+ }
352
+ try {
353
+ setupClient(name);
354
+ }
355
+ catch (err) {
356
+ console.error(`[easel] setup --client ${name} failed:`, err.message);
357
+ process.exitCode = 1;
358
+ }
359
+ return;
360
+ }
334
361
  cmdSetup();
335
362
  return;
363
+ }
336
364
  case "server":
337
365
  await cmdServer();
338
366
  return;
@@ -350,7 +378,23 @@ async function main() {
350
378
  case "-v":
351
379
  cmdVersion();
352
380
  return;
353
- case undefined:
381
+ case "mcp": {
382
+ const { main: mcpMain } = await import("./mcp.js");
383
+ await mcpMain();
384
+ return;
385
+ }
386
+ case undefined: {
387
+ // When invoked over a pipe (no TTY on stdin) — e.g. an MCP client
388
+ // launching us via `npx -y @ammduncan/easel` — boot the MCP server.
389
+ // Interactive terminal use still gets the help text.
390
+ if (!process.stdin.isTTY) {
391
+ const { main: mcpMain } = await import("./mcp.js");
392
+ await mcpMain();
393
+ return;
394
+ }
395
+ help();
396
+ return;
397
+ }
354
398
  case "help":
355
399
  case "--help":
356
400
  case "-h":
@@ -0,0 +1,73 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import { homedir, platform } from "node:os";
4
+ const CLIENTS = {
5
+ "claude-desktop": {
6
+ name: "claude-desktop",
7
+ label: "Claude Desktop",
8
+ configPath: () => {
9
+ const home = homedir();
10
+ if (platform() === "darwin") {
11
+ return join(home, "Library", "Application Support", "Claude", "claude_desktop_config.json");
12
+ }
13
+ if (platform() === "win32") {
14
+ const appData = process.env.APPDATA ?? join(home, "AppData", "Roaming");
15
+ return join(appData, "Claude", "claude_desktop_config.json");
16
+ }
17
+ return join(home, ".config", "Claude", "claude_desktop_config.json");
18
+ },
19
+ postSetup: "Quit and relaunch Claude Desktop to load the MCP server.",
20
+ },
21
+ cursor: {
22
+ name: "cursor",
23
+ label: "Cursor",
24
+ configPath: () => join(homedir(), ".cursor", "mcp.json"),
25
+ postSetup: "Open Cursor and toggle MCP servers in Settings → Features → MCP, " +
26
+ "or restart Cursor for the registration to take effect.",
27
+ },
28
+ windsurf: {
29
+ name: "windsurf",
30
+ label: "Windsurf",
31
+ configPath: () => join(homedir(), ".codeium", "windsurf", "mcp_config.json"),
32
+ postSetup: "Restart Windsurf to load the MCP server.",
33
+ },
34
+ };
35
+ export function listClients() {
36
+ return Object.keys(CLIENTS);
37
+ }
38
+ export function setupClient(name) {
39
+ const spec = CLIENTS[name];
40
+ if (!spec) {
41
+ throw new Error(`unknown client: ${name}`);
42
+ }
43
+ const configPath = spec.configPath();
44
+ const config = readJson(configPath);
45
+ const mcpServers = config.mcpServers ?? {};
46
+ mcpServers.easel = {
47
+ command: "npx",
48
+ args: ["-y", "@ammduncan/easel"],
49
+ };
50
+ config.mcpServers = mcpServers;
51
+ mkdirSync(dirname(configPath), { recursive: true });
52
+ writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`);
53
+ console.log(`[easel] ${spec.label} configured`);
54
+ console.log(` - wrote ${configPath}`);
55
+ console.log(` - ${spec.postSetup}`);
56
+ }
57
+ function readJson(path) {
58
+ if (!existsSync(path))
59
+ return {};
60
+ try {
61
+ const text = readFileSync(path, "utf-8").trim();
62
+ if (!text)
63
+ return {};
64
+ const parsed = JSON.parse(text);
65
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
66
+ return parsed;
67
+ }
68
+ return {};
69
+ }
70
+ catch (err) {
71
+ throw new Error(`couldn't parse existing config at ${path}: ${err.message}`);
72
+ }
73
+ }
package/dist/mcp.js CHANGED
@@ -79,7 +79,7 @@ async function pushToServer(args) {
79
79
  }
80
80
  return (await r.json());
81
81
  }
82
- async function main() {
82
+ export async function main() {
83
83
  const server = new Server({ name: "easel", version: "0.1.0" }, { capabilities: { tools: {} } });
84
84
  server.setRequestHandler(ListToolsRequestSchema, async () => ({
85
85
  tools: [
@@ -229,7 +229,12 @@ async function main() {
229
229
  const transport = new StdioServerTransport();
230
230
  await server.connect(transport);
231
231
  }
232
- main().catch((err) => {
233
- console.error("[easel mcp] fatal:", err);
234
- process.exit(1);
235
- });
232
+ // Auto-run when invoked directly (e.g. `node dist/mcp.js`), not when imported.
233
+ const invokedDirectly = import.meta.url === `file://${process.argv[1]}` ||
234
+ import.meta.url.endsWith("/dist/mcp.js");
235
+ if (invokedDirectly) {
236
+ main().catch((err) => {
237
+ console.error("[easel mcp] fatal:", err);
238
+ process.exit(1);
239
+ });
240
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ammduncan/easel",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "description": "A live browser tab for every Claude Code (and MCP) session. The push MCP tool appends HTML cards to a scrolling feed you keep open in split-screen.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -36,6 +36,7 @@
36
36
  "scripts/easel-session-id.mjs",
37
37
  "skills/",
38
38
  "README.md",
39
+ "CHANGELOG.md",
39
40
  "LICENCE"
40
41
  ],
41
42
  "scripts": {