@ammduncan/easel 0.2.0 → 0.2.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/CHANGELOG.md +37 -0
- package/README.md +68 -22
- package/dist/cli.js +29 -2
- package/dist/client-setup.js +73 -0
- package/package.json +2 -1
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to easel. This project adheres to [Semantic Versioning](https://semver.org/).
|
|
4
|
+
|
|
5
|
+
## 0.2.1 — 2026-05-22
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- `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`)
|
|
9
|
+
- README leads with the npx install; per-client install commands documented; generic JSON snippet for any other MCP-speaking client.
|
|
10
|
+
- This `CHANGELOG.md`.
|
|
11
|
+
|
|
12
|
+
### Notes
|
|
13
|
+
- The Claude Code setup path (bare `easel setup`) is unchanged.
|
|
14
|
+
|
|
15
|
+
## 0.2.0 — 2026-05-22
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
- 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.
|
|
19
|
+
- `EASEL_SESSION_ID` env var as the highest-priority override for session resolution.
|
|
20
|
+
- 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.
|
|
21
|
+
- 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.
|
|
22
|
+
- `LICENCE` file (MIT).
|
|
23
|
+
|
|
24
|
+
### Changed
|
|
25
|
+
- `resolveClaudeSessionId()` is now total — always returns a string. Dead "no session id" error branches removed from `cli.ts` and `mcp.ts`.
|
|
26
|
+
- `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`.
|
|
27
|
+
- Package size: 47 kB tarball, 165 kB unpacked, 3 runtime deps.
|
|
28
|
+
|
|
29
|
+
## 0.1.0 — internal baseline
|
|
30
|
+
|
|
31
|
+
The pre-publish baseline. Highlights from the unreleased history:
|
|
32
|
+
|
|
33
|
+
- 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).
|
|
34
|
+
- Dropped `jq` as a runtime dependency. Ported `scripts/easel-session-id.sh` to `scripts/easel-session-id.mjs` (Node stdlib only).
|
|
35
|
+
- Added `easel restart` command.
|
|
36
|
+
- MCP tool names finalised as `push`, `open`, `config`, `label` (invoked as `mcp__easel__*`).
|
|
37
|
+
- 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
|
|
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
|
-
┌────────────
|
|
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
|
|
10
|
+
│ pushed to easel ↗ — #1 │ │ ┌────────────────────────────────┐│
|
|
11
11
|
│ │ │ │ Three actors talk to each… ││
|
|
12
12
|
│ > what could break? │ │ └────────────────────────────────┘│
|
|
13
13
|
│ │ │ │
|
|
14
|
-
│ pushed to
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
48
|
+
### Any other MCP client
|
|
33
49
|
|
|
34
|
-
|
|
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
|
|
64
|
-
- Session IDs come from Claude Code
|
|
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 (`
|
|
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
|
|
141
|
+
session-id.ts 5-tier resolver (env / hook file / transcript scan / synthetic PPID)
|
|
97
142
|
config-store.ts preset / theme / density persistence
|
|
98
|
-
|
|
99
|
-
|
|
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,7 +23,10 @@ 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
|
|
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)
|
|
27
31
|
easel restart kill the running HTTP server and respawn it (picks up new builds/paths)
|
|
28
32
|
easel server run the HTTP server in the foreground (debug)
|
|
@@ -330,9 +334,32 @@ async function main() {
|
|
|
330
334
|
case "url":
|
|
331
335
|
await cmdUrl();
|
|
332
336
|
return;
|
|
333
|
-
case "setup":
|
|
337
|
+
case "setup": {
|
|
338
|
+
const clientIdx = rest.indexOf("--client");
|
|
339
|
+
if (clientIdx !== -1) {
|
|
340
|
+
const name = rest[clientIdx + 1];
|
|
341
|
+
if (!name) {
|
|
342
|
+
console.error(`[easel] --client requires a name. Available: ${listClients().join(", ")}`);
|
|
343
|
+
process.exitCode = 1;
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
if (!listClients().includes(name)) {
|
|
347
|
+
console.error(`[easel] unknown client "${name}". Available: ${listClients().join(", ")}`);
|
|
348
|
+
process.exitCode = 1;
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
try {
|
|
352
|
+
setupClient(name);
|
|
353
|
+
}
|
|
354
|
+
catch (err) {
|
|
355
|
+
console.error(`[easel] setup --client ${name} failed:`, err.message);
|
|
356
|
+
process.exitCode = 1;
|
|
357
|
+
}
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
334
360
|
cmdSetup();
|
|
335
361
|
return;
|
|
362
|
+
}
|
|
336
363
|
case "server":
|
|
337
364
|
await cmdServer();
|
|
338
365
|
return;
|
|
@@ -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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ammduncan/easel",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1",
|
|
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": {
|