@ammduncan/easel 0.2.1 → 0.2.4
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 +23 -0
- package/dist/cli.js +18 -1
- package/dist/client/index.css +6 -9
- package/dist/client/index.js +5 -5
- package/dist/mcp.js +47 -6
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,29 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to easel. This project adheres to [Semantic Versioning](https://semver.org/).
|
|
4
4
|
|
|
5
|
+
## 0.2.4 — 2026-05-22
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
- **Pushes from non-Claude-Code clients (Claude Desktop, Cursor, Windsurf, etc.) ignored the easel style guide.** The full guide lives in the `using-easel` skill — but skills are a Claude Code feature; other MCP clients never see them. The MCP `push` tool's description only said "Pass full HTML" and contained none of the styling rules, so agents in non-CC clients hardcoded one mode's colors. Result: lede text colored `#475569` went invisible in dark mode, hardcoded `#e5e5e5` borders became hard white lines, mockups crammed into half-width columns, etc.
|
|
9
|
+
- The `push` tool description now carries the essentials inline: adaptive-color rules (`light-dark()` + `color: inherit` re-scoping + locked-mode container inverse rule), a copy-paste starter pattern, presentation-scale typography, whitespace, tangible-visual heuristics, vertical stacking of desktop mockups, and the proactive-push convention. Every MCP client surfaces tool descriptions to its agent, so this lands cross-client.
|
|
10
|
+
|
|
11
|
+
## 0.2.3 — 2026-05-22
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
- **Every push was being delivered twice.** The auto-run guard in `dist/mcp.js` had a sloppy fallback (`endsWith("/dist/mcp.js")`) that matched even when the file was imported, not just when it was invoked directly. Result: when the CLI's no-TTY path dynamically imported `mcp.js`, the guard fired AND the CLI explicitly called `main()`, so two MCP servers ran in the same process attached to the same stdin. Every tool call was processed twice. Guard now uses strict equality only.
|
|
15
|
+
- **Sessions index — trash icon overlapped the count/timestamp on short rows.** The hover-revealed delete button was absolutely positioned and collided with the right-column text whenever the row was tight. Promoted to its own grid column so it sits cleanly to the right of the count/when stack regardless of row height.
|
|
16
|
+
|
|
17
|
+
## 0.2.2 — 2026-05-22
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
- **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.
|
|
21
|
+
|
|
22
|
+
### Added
|
|
23
|
+
- 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).
|
|
24
|
+
|
|
25
|
+
### Changed
|
|
26
|
+
- `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.
|
|
27
|
+
|
|
5
28
|
## 0.2.1 — 2026-05-22
|
|
6
29
|
|
|
7
30
|
### Added
|
package/dist/cli.js
CHANGED
|
@@ -28,6 +28,7 @@ Usage:
|
|
|
28
28
|
easel setup --client claude-desktop register the MCP in Claude Desktop's config
|
|
29
29
|
easel setup --client windsurf register the MCP in Windsurf's config
|
|
30
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)
|
|
31
32
|
easel restart kill the running HTTP server and respawn it (picks up new builds/paths)
|
|
32
33
|
easel server run the HTTP server in the foreground (debug)
|
|
33
34
|
easel version
|
|
@@ -377,7 +378,23 @@ async function main() {
|
|
|
377
378
|
case "-v":
|
|
378
379
|
cmdVersion();
|
|
379
380
|
return;
|
|
380
|
-
case
|
|
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
|
+
}
|
|
381
398
|
case "help":
|
|
382
399
|
case "--help":
|
|
383
400
|
case "-h":
|
package/dist/client/index.css
CHANGED
|
@@ -40,9 +40,9 @@
|
|
|
40
40
|
.session-row {
|
|
41
41
|
position: relative;
|
|
42
42
|
display: grid;
|
|
43
|
-
grid-template-columns: 1fr auto;
|
|
43
|
+
grid-template-columns: 1fr auto auto;
|
|
44
44
|
gap: 18px;
|
|
45
|
-
align-items:
|
|
45
|
+
align-items: center;
|
|
46
46
|
padding: 20px 22px;
|
|
47
47
|
background: var(--ds-surface);
|
|
48
48
|
border: 1px solid var(--ds-line);
|
|
@@ -55,14 +55,11 @@
|
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
.session-del {
|
|
58
|
-
position: absolute;
|
|
59
|
-
top: 14px;
|
|
60
|
-
right: 14px;
|
|
61
58
|
display: inline-flex;
|
|
62
59
|
align-items: center;
|
|
63
60
|
justify-content: center;
|
|
64
|
-
width:
|
|
65
|
-
height:
|
|
61
|
+
width: 28px;
|
|
62
|
+
height: 28px;
|
|
66
63
|
background: transparent;
|
|
67
64
|
border: 0;
|
|
68
65
|
border-radius: 6px;
|
|
@@ -70,8 +67,9 @@
|
|
|
70
67
|
cursor: pointer;
|
|
71
68
|
opacity: 0;
|
|
72
69
|
transition: opacity 120ms ease, background 120ms ease, color 120ms ease;
|
|
70
|
+
align-self: center;
|
|
73
71
|
}
|
|
74
|
-
.session-row:hover .session-del { opacity: 0.
|
|
72
|
+
.session-row:hover .session-del { opacity: 0.55; }
|
|
75
73
|
.session-del:hover {
|
|
76
74
|
background: var(--ds-surface-soft);
|
|
77
75
|
color: var(--ds-danger);
|
|
@@ -178,7 +176,6 @@
|
|
|
178
176
|
align-items: flex-end;
|
|
179
177
|
gap: 6px;
|
|
180
178
|
white-space: nowrap;
|
|
181
|
-
padding-right: 34px;
|
|
182
179
|
}
|
|
183
180
|
|
|
184
181
|
.session-pushcount {
|
package/dist/client/index.js
CHANGED
|
@@ -179,8 +179,10 @@
|
|
|
179
179
|
when.textContent = relTime(session.lastActivity);
|
|
180
180
|
right.appendChild(when);
|
|
181
181
|
|
|
182
|
-
|
|
183
|
-
|
|
182
|
+
a.appendChild(right);
|
|
183
|
+
|
|
184
|
+
// Hover-revealed delete — sits in its own grid column to the right of
|
|
185
|
+
// the count/when stack so it never overlaps the text on short rows.
|
|
184
186
|
const del = document.createElement("button");
|
|
185
187
|
del.className = "session-del";
|
|
186
188
|
del.type = "button";
|
|
@@ -197,9 +199,7 @@
|
|
|
197
199
|
});
|
|
198
200
|
load();
|
|
199
201
|
});
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
a.appendChild(right);
|
|
202
|
+
a.appendChild(del);
|
|
203
203
|
|
|
204
204
|
return a;
|
|
205
205
|
}
|
package/dist/mcp.js
CHANGED
|
@@ -79,13 +79,46 @@ 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: [
|
|
86
86
|
{
|
|
87
87
|
name: TOOL_PUSH,
|
|
88
|
-
description: "Push an HTML card to this session's live browser tab
|
|
88
|
+
description: "Push an HTML card to this session's live browser tab. Renders in a sandboxed iframe over a host-controlled canvas that can be LIGHT or DARK depending on the user's OS theme. Treat each card as a presentation slide — generous whitespace, presentation-scale type, tangible visuals. Your HTML MUST adapt to both light and dark modes.\n\n" +
|
|
89
|
+
"═══ ADAPTIVE COLOR (gets wrong most often) ═══\n" +
|
|
90
|
+
"• Do NOT set `background` on `body` or your root wrapper. The host paints the canvas — setting bg fights it and creates a wrong-shade block in the opposite mode.\n" +
|
|
91
|
+
"• Use `light-dark()` for ALL text colors, card backgrounds, borders, and decorative shades. Add `:root { color-scheme: light dark; }` so the function resolves. Hardcoded `color: #475569` goes invisible in dark mode; hardcoded `border: 1px solid #e5e5e5` becomes a hard white line.\n" +
|
|
92
|
+
"• After setting `.wrap { color: light-dark(...); }`, re-scope `color: inherit` to every descendant so child elements don't fall back to the host's default.\n" +
|
|
93
|
+
"• Inverse rule: if you DO paint a fixed background on a container (a code block locked to dark, a brand-color hero), you MUST also set its text color AND re-scope `color: inherit` to its children. Background and text are a pair.\n\n" +
|
|
94
|
+
"═══ COPY-PASTE STARTER ═══\n" +
|
|
95
|
+
" :root { color-scheme: light dark; }\n" +
|
|
96
|
+
" .wrap { color: light-dark(#111, #e8e8e8); padding: 56px 48px; font-family: -apple-system, 'Inter', system-ui, sans-serif; max-width: 820px; }\n" +
|
|
97
|
+
" .wrap *, .wrap h1, .wrap h2, .wrap h3, .wrap p, .wrap li, .wrap span { color: inherit; }\n" +
|
|
98
|
+
" .card { background: light-dark(#fff, #161616); border: 1px solid light-dark(#e0d9c3, #2a2a2a); border-radius: 12px; padding: 24px; }\n\n" +
|
|
99
|
+
"═══ TYPOGRAPHY (presentation scale, NOT dashboard) ═══\n" +
|
|
100
|
+
"• Hero title: 44–52px, weight 500, letter-spacing -0.025em\n" +
|
|
101
|
+
"• Section titles: 28–36px, weight 500\n" +
|
|
102
|
+
"• Body: 18–22px, line-height 1.55+\n" +
|
|
103
|
+
"• Eyebrow / kicker: 13–14px uppercase, letter-spacing 0.14em+, colored as a muted accent\n" +
|
|
104
|
+
"• Inter or system sans-serif. Never go below 13px for readable content.\n\n" +
|
|
105
|
+
"═══ WHITESPACE ═══\n" +
|
|
106
|
+
"• Page padding: 56–80px vertical, 40–56px horizontal\n" +
|
|
107
|
+
"• Card padding: 24–32px\n" +
|
|
108
|
+
"• Between major sections: 56–96px\n\n" +
|
|
109
|
+
"═══ VISUALS — tangible beats abstract ═══\n" +
|
|
110
|
+
"The test: 'Could a bullet list communicate this just as well?' If yes, the visual is decoration not explanation — rebuild it as something tangible.\n" +
|
|
111
|
+
"• YES: skeuomorphic browser chrome (3 traffic-light dots + URL bar), terminal windows with monospace + prompt, code-editor frames with line gutters, real device mockups, proportional timeline bars with phase markers, pipe-shaped funnels.\n" +
|
|
112
|
+
"• NO: 5 labeled rectangles connected by arrows; abstract 'sequence diagrams' of thin lines with text labels; numbered-box explainers where each box is just a title + 1 sentence.\n\n" +
|
|
113
|
+
"═══ LAYOUT ═══\n" +
|
|
114
|
+
"• Stack desktop mockups VERTICALLY with labels ('Now', 'Proposed') — don't squeeze them side-by-side. The iframe is ~900px wide; two desktop screens at half-width crush columns, wrap headings to 3 lines, and turn tables unreadable.\n" +
|
|
115
|
+
"• Side-by-side is fine only for narrow mobile mockups, small cards, or short text columns that genuinely fit in half-width.\n" +
|
|
116
|
+
"• One accent color, 3–4 instances max per card. Status colors (red/amber/green) only when state genuinely maps to status.\n\n" +
|
|
117
|
+
"═══ WHEN TO PUSH ═══\n" +
|
|
118
|
+
"A response that would otherwise contain: >2 paragraphs of explanation, any UI mockup, a diagram, a code diff, a ≥3-option comparison, or a multi-step progress view. Do NOT ask permission — push proactively. After pushing, reply in chat with ONE LINE: 'pushed to easel ↗ — #<index>'. Don't restate the card's content.\n\n" +
|
|
119
|
+
"═══ OTHER ═══\n" +
|
|
120
|
+
"• Pass full HTML only — no Markdown. The iframe injects baseline typography so plain `<h1>/<p>` works without extra CSS, but for anything multi-section define your own `<style>` block.\n" +
|
|
121
|
+
"• `<script>` tags trying to mutate the parent window are sandbox-blocked; in-iframe `<script>` (for animations, charts, interactivity) is fine.",
|
|
89
122
|
inputSchema,
|
|
90
123
|
},
|
|
91
124
|
{
|
|
@@ -229,7 +262,15 @@ async function main() {
|
|
|
229
262
|
const transport = new StdioServerTransport();
|
|
230
263
|
await server.connect(transport);
|
|
231
264
|
}
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
265
|
+
// Auto-run only when invoked directly (e.g. `node dist/mcp.js`), not when
|
|
266
|
+
// imported (e.g. by the CLI's no-arg / `mcp` subcommand path). The strict
|
|
267
|
+
// equality is what guarantees this — anything fuzzier (like an endsWith
|
|
268
|
+
// check) matches on import too and ends up running main() twice, which the
|
|
269
|
+
// stdio transport then connects to the same stdin → every message
|
|
270
|
+
// processed twice → every push duplicated. Don't add fallbacks here.
|
|
271
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
272
|
+
main().catch((err) => {
|
|
273
|
+
console.error("[easel mcp] fatal:", err);
|
|
274
|
+
process.exit(1);
|
|
275
|
+
});
|
|
276
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ammduncan/easel",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.4",
|
|
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",
|