@ammduncan/easel 0.6.1 → 0.7.0
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 +24 -0
- package/README.md +4 -2
- package/dist/cli.js +106 -2
- package/dist/client/viewer.js +82 -235
- package/dist/http-server.js +2 -2
- package/dist/mcp.js +41 -13
- package/dist/session-store.js +2 -0
- package/package.json +1 -1
- package/scripts/easel-session-id.mjs +8 -0
- package/skills/using-easel/SKILL.md +29 -30
- package/skills/using-easel/kit/EASEL-GUIDE.md +161 -0
- package/skills/using-easel/kit/easel-base.css +176 -0
- package/skills/using-easel/kit/easel-icons.svg +28 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,30 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to easel. This project adheres to [Semantic Versioning](https://semver.org/).
|
|
4
4
|
|
|
5
|
+
## Unreleased
|
|
6
|
+
|
|
7
|
+
## 0.7.0 — 2026-06-27
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
- **Pushed cards are now immutable snapshots — each is sealed to one light/dark mode at push time and never reflows when the global theme is toggled.** Previously every rendered card live-tracked the global theme: a config/theme message was broadcast into every iframe, so toggling Easel light↔dark flipped *all* existing cards — and forced authors into adaptive `light-dark()` styling that frequently produced dark-on-dark / washed-out text. Now the iframe-config broadcast and the in-iframe config/theme listeners are removed (print is preserved); a card's `data-theme`/`preset`/`density` are baked once at render. The global toggle still restyles Easel's own chrome and sets the default for *future* pushes — it just never touches existing cards. Verified live: with two cards pushed under different modes, toggling the global light↔dark left both sealed (the light card stayed light, the dark stayed dark). The convention across the `push` tool description + `using-easel` SKILL/kit inverts accordingly: "own your canvas, commit to one mode" replaces "adapt to both modes via `light-dark()`".
|
|
11
|
+
- **The default push wrapper no longer injects the design-system token layer — authors own the canvas.** With cards now frozen to one mode, the six-combo `--ds-*` preset token block, the presentation type scale, and the Inter webfont are dropped from the default wrapper. What remains is a minimal floor: a box-sizing reset, a system-sans default, a committed base surface + ink for the sealed mode, and the prose-width reading cap (plus the `.full-bleed` escape). The structural primitives (`.window`/`.code`/`.terminal`/`.easel-*`) and semantic chips are **retained**, and the `using-easel` kit (`easel-base.css`) re-supplies the full presentation scaffold on demand via its `light-dark()` token fallbacks — so kit-based pushes are unchanged, while bare semantic HTML now renders on the clean floor instead of the auto-injected design system. Verified by rendering bare-HTML, kit-inlined, and primitive (`.window`/`.code`/`.chip`) pushes against the live build.
|
|
12
|
+
|
|
13
|
+
### Removed
|
|
14
|
+
- **The injected `--ds-*` preset token block, presentation type scale, and Inter webfont** from the default push wrapper (the "own the canvas" change above). Pushes that relied on the auto-injected tokens/type *without* inlining the kit render plainer; inline `easel-base.css` for the presentation scaffold.
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
- **Reserved primitive class names (`code`/`terminal`/`window`) no longer render invisible dark-on-dark when an author reuses them.** Two correct-in-isolation rules were mutually destructive: the structural primitives set a fixed dark background + light ink at specificity `(0,1,0)`, while the documented `.wrap * { color: inherit }` guard is *also* `(0,1,0)` but lives later in source order (author `<body>` vs injected `<head>`), so it won the tie and flipped the primitive's ink back to the author's near-black `.wrap` colour → near-black text on the primitive's dark fill, invisible. Because the names are generic English words, author markup naturally reused them (`<td class="code">`, `<span class="code">`) and inherited the dark fill unintentionally. Fixes: (1) **Hardened** — each primitive's `background` + `color` are now committed with `!important` on the *container only* (not on `*`/token rules), so `.wrap * { color: inherit }` can't flip the ink; syntax-highlight tokens still win by specificity and are untouched. Existing content becomes readable with no author change. (2) **Namespaced** — `.easel-code` / `.easel-terminal` / `.easel-window` (and `[data-easel="code|terminal|window"]`) are the canonical collision-free forms; bare `code`/`terminal`/`window` remain as deprecated aliases. (3) **Warned** — the in-iframe guard now logs a console warning when a reserved primitive name lands on an inline/table element (`span`/`td`/…), the accidental-collision signature; the existing low-contrast warning points at the same cause. `using-easel` SKILL + kit guide document the reserved names, the `.easel-*` forms, and the `mono`/`<code>` workaround. Verified by rendering the exact `<td class="code">` / `<span class="code">` / `.code` / `.easel-code` repro against the real injected CSS in light mode.
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
- **`theme: 'light' | 'dark'` param on the `push` tool — declares the card's sealed canvas mode.** Frozen at push time; never flips with the global toggle. Omit to snapshot the current global theme (also frozen). Pairs with the immutable-snapshot change above: an author told "present in dark" passes `theme:'dark'` and gets a card that stays dark regardless of the viewer's global setting.
|
|
21
|
+
- **`EASEL_SUPPRESS_SESSION=1` env var suppresses switcher-session registration.** When set on a `claude` (or any client) invocation, easel still loads as an MCP but every tool (`push`/`label`/`open`/`config`) short-circuits to a no-op — the MCP never contacts the HTTP server and the session never appears in the switcher. The SessionStart hook also skips its convention reminder so the agent isn't nagged to label a session that can't register. Built for automated/headless consumers that run easel-registered Claude on a tight cadence (e.g. the ammiels-bot dispatcher tick fired every ~60s by launchd), which otherwise pile up churny "Bot watcher tick" entries in the switcher. Deliberately MCP-local: it leaves the Slack connector and every other MCP/account connector fully intact, unlike `claude --strict-mcp-config`, which also strips the claude.ai account connectors.
|
|
22
|
+
|
|
23
|
+
## 0.6.2 — 2026-06-04
|
|
24
|
+
|
|
25
|
+
### Fixed
|
|
26
|
+
- **`setup` now actually puts `easel` on PATH, making the documented bare CLI commands truthful.** The README's CLI section documents `easel open / update / restart / …`, but neither install path ever exposed the binary: `npx -y @ammduncan/easel setup` runs once from the ephemeral npx cache and leaves nothing behind, and clone installs' `bin/easel setup` never linked. Setup now closes the gap per install flavor: clone installs get `npm link` (skipped when `easel` already resolves); npx-cache runs get a real `npm install -g` pinned to their own version, then **delegate setup to the global copy** so the MCP/hook registrations point at paths that survive npx cache pruning (the cache-pruning drift left one machine's MCP pinned to a stale 0.5.1 cache while 0.6.x shipped). Failures degrade to a printed hint — setup never hard-fails on the PATH step.
|
|
27
|
+
- **`easel update` now works for npm installs.** It previously assumed a git checkout (`git pull` flavor, "clone installs only") and just failed elsewhere. Without a `.git` dir it now runs `npm install -g @ammduncan/easel@latest` and re-runs setup from the new copy.
|
|
28
|
+
|
|
5
29
|
## 0.6.1 — 2026-06-04
|
|
6
30
|
|
|
7
31
|
### Changed
|
package/README.md
CHANGED
|
@@ -31,7 +31,7 @@ Requires Node 20+.
|
|
|
31
31
|
npx -y @ammduncan/easel setup
|
|
32
32
|
```
|
|
33
33
|
|
|
34
|
-
That registers the MCP at user scope, installs the `using-easel` skill so the agent knows when to push,
|
|
34
|
+
That registers the MCP at user scope, installs the `using-easel` skill so the agent knows when to push, adds the `SessionStart` hooks that resolve session IDs and auto-open the tab, and installs the package globally so the bare `easel` command works from any shell. Restart Claude Code and you're done.
|
|
35
35
|
|
|
36
36
|
### Cursor / Claude Desktop / Windsurf / Codex
|
|
37
37
|
|
|
@@ -81,6 +81,8 @@ npm install && npm run build
|
|
|
81
81
|
bin/easel setup
|
|
82
82
|
```
|
|
83
83
|
|
|
84
|
+
Setup runs `npm link`, so bare `easel` works from any shell afterwards.
|
|
85
|
+
|
|
84
86
|
## Tools the agent gets
|
|
85
87
|
|
|
86
88
|
| Tool | What it does |
|
|
@@ -134,7 +136,7 @@ easel config print / set { preset, theme, density }
|
|
|
134
136
|
easel setup Claude Code: hooks + MCP + skill
|
|
135
137
|
easel setup --client <name> register the MCP in another client (cursor, claude-desktop, windsurf, codex)
|
|
136
138
|
easel restart kill + respawn the HTTP server (handy after a build)
|
|
137
|
-
easel update git pull + build + setup
|
|
139
|
+
easel update clone installs: git pull + build + setup · npm installs: npm install -g @latest + setup
|
|
138
140
|
easel server run the HTTP server in the foreground (debug)
|
|
139
141
|
easel version
|
|
140
142
|
```
|
package/dist/cli.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { spawn, spawnSync } from "node:child_process";
|
|
3
3
|
import { copyFileSync, mkdirSync, readFileSync, rmSync, writeFileSync, existsSync } from "node:fs";
|
|
4
|
-
import { dirname, join, resolve } from "node:path";
|
|
4
|
+
import { dirname, join, resolve, sep } from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
import { homedir } from "node:os";
|
|
7
7
|
import { ensureHttpServer, readLock } from "./server-manager.js";
|
|
@@ -11,6 +11,7 @@ import { registerSession } from "./session-store.js";
|
|
|
11
11
|
import { listClients, setupClient, } from "./client-setup.js";
|
|
12
12
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
13
13
|
const PROJECT_ROOT = resolve(__dirname, "..");
|
|
14
|
+
const NPM_PKG = "@ammduncan/easel";
|
|
14
15
|
function help() {
|
|
15
16
|
console.log(`easel — live browser feed for Claude Code for Claude Code sessions
|
|
16
17
|
|
|
@@ -28,7 +29,7 @@ Usage:
|
|
|
28
29
|
easel setup --client claude-desktop register MCP in ~/Library/Application Support/Claude/claude_desktop_config.json
|
|
29
30
|
easel setup --client windsurf register MCP in ~/.codeium/windsurf/mcp_config.json
|
|
30
31
|
easel setup --client codex register MCP in ~/.codex/config.toml + copy skill to ~/.codex/skills/
|
|
31
|
-
easel update git pull +
|
|
32
|
+
easel update clone installs: git pull + build + setup · npm installs: npm install -g @latest + setup
|
|
32
33
|
easel mcp run the stdio MCP server in the foreground (used by clients)
|
|
33
34
|
easel restart kill the running HTTP server and respawn it (picks up new builds/paths)
|
|
34
35
|
easel server run the HTTP server in the foreground (debug)
|
|
@@ -97,7 +98,91 @@ function openInBrowser(url) {
|
|
|
97
98
|
console.error(`[easel] couldn't open browser: ${err.message}`);
|
|
98
99
|
}
|
|
99
100
|
}
|
|
101
|
+
function binResolvesOnPath() {
|
|
102
|
+
const cmd = process.platform === "win32" ? "where" : "which";
|
|
103
|
+
try {
|
|
104
|
+
const r = spawnSync(cmd, ["easel"], { encoding: "utf-8" });
|
|
105
|
+
return r.status === 0 && r.stdout.trim().length > 0;
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
function pkgVersion() {
|
|
112
|
+
try {
|
|
113
|
+
const pkg = JSON.parse(readFileSync(join(PROJECT_ROOT, "package.json"), "utf-8"));
|
|
114
|
+
return pkg.version ?? "latest";
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
return "latest";
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
function globalPkgDir() {
|
|
121
|
+
try {
|
|
122
|
+
const r = spawnSync("npm", ["root", "-g"], { encoding: "utf-8" });
|
|
123
|
+
if (r.status !== 0)
|
|
124
|
+
return null;
|
|
125
|
+
const dir = join(r.stdout.trim(), NPM_PKG);
|
|
126
|
+
return existsSync(dir) ? dir : null;
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
// Re-runs setup from the globally installed copy so its registrations point at
|
|
133
|
+
// the global paths. Returns false if there is no global copy to delegate to.
|
|
134
|
+
function rerunSetupFromGlobal() {
|
|
135
|
+
const dir = globalPkgDir();
|
|
136
|
+
if (!dir) {
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
const r = spawnSync("node", [join(dir, "dist", "cli.js"), "setup"], {
|
|
140
|
+
stdio: "inherit",
|
|
141
|
+
env: { ...process.env, EASEL_SETUP_CHILD: "1" },
|
|
142
|
+
});
|
|
143
|
+
return r.status === 0;
|
|
144
|
+
}
|
|
145
|
+
// Make bare `easel` work after setup, as the README documents. Clone installs
|
|
146
|
+
// get `npm link`. npx-cache installs get a real global install — the cache is
|
|
147
|
+
// pruned unpredictably — and setup is then re-run from the global copy so the
|
|
148
|
+
// MCP/hook registrations point at paths that survive pruning.
|
|
149
|
+
// Returns true when setup was fully delegated to the global copy.
|
|
150
|
+
function ensureBinOnPath() {
|
|
151
|
+
const inNpxCache = PROJECT_ROOT.split(sep).includes("_npx");
|
|
152
|
+
if (!inNpxCache) {
|
|
153
|
+
if (binResolvesOnPath()) {
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
const r = spawnSync("npm", ["link", "--silent", "--no-audit", "--no-fund"], {
|
|
157
|
+
cwd: PROJECT_ROOT,
|
|
158
|
+
stdio: "ignore",
|
|
159
|
+
});
|
|
160
|
+
if (r.status === 0) {
|
|
161
|
+
console.log(" - linked `easel` into the global bin (npm link)");
|
|
162
|
+
}
|
|
163
|
+
else {
|
|
164
|
+
console.warn(` - couldn't put \`easel\` on PATH — run \`npm link\` in ${PROJECT_ROOT}`);
|
|
165
|
+
}
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
const version = pkgVersion();
|
|
169
|
+
console.log(`[easel] installing ${NPM_PKG}@${version} globally so \`easel\` is on PATH…`);
|
|
170
|
+
const installed = spawnSync("npm", ["install", "-g", "--silent", "--no-audit", "--no-fund", `${NPM_PKG}@${version}`], { stdio: "inherit" });
|
|
171
|
+
if (installed.status !== 0 || !rerunSetupFromGlobal()) {
|
|
172
|
+
console.warn(` - global install failed — \`easel\` won't be on PATH; run \`npm install -g ${NPM_PKG}\` manually`);
|
|
173
|
+
return false;
|
|
174
|
+
}
|
|
175
|
+
return true;
|
|
176
|
+
}
|
|
100
177
|
function cmdSetup() {
|
|
178
|
+
// Put `easel` on PATH first — for npx-cache runs this delegates the whole
|
|
179
|
+
// setup to a freshly installed global copy (so registered paths outlive the
|
|
180
|
+
// cache), in which case there's nothing left to do here.
|
|
181
|
+
if (!process.env.EASEL_SETUP_CHILD) {
|
|
182
|
+
if (ensureBinOnPath()) {
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
101
186
|
mkdirSync(HOOK_DIR, { recursive: true });
|
|
102
187
|
const settingsPath = join(homedir(), ".claude", "settings.json");
|
|
103
188
|
const hookScript = resolve(PROJECT_ROOT, "scripts", "easel-session-id.mjs");
|
|
@@ -264,6 +349,25 @@ async function cmdConfig(args) {
|
|
|
264
349
|
function cmdUpdate() {
|
|
265
350
|
console.log("[easel] checking for updates…");
|
|
266
351
|
const run = (cmd, args) => spawnSync(cmd, args, { stdio: "inherit", cwd: PROJECT_ROOT });
|
|
352
|
+
// npm/global installs have no git checkout — update from the registry and
|
|
353
|
+
// re-run setup from the new copy so registrations track the new paths.
|
|
354
|
+
if (!existsSync(join(PROJECT_ROOT, ".git"))) {
|
|
355
|
+
const installed = run("npm", [
|
|
356
|
+
"install",
|
|
357
|
+
"-g",
|
|
358
|
+
"--no-audit",
|
|
359
|
+
"--no-fund",
|
|
360
|
+
`${NPM_PKG}@latest`,
|
|
361
|
+
]);
|
|
362
|
+
if (installed.status !== 0) {
|
|
363
|
+
console.error("[easel] npm install -g failed");
|
|
364
|
+
process.exitCode = 1;
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
rerunSetupFromGlobal();
|
|
368
|
+
console.log("[easel] updated. Restart Claude Code to pick up tool/skill changes.");
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
267
371
|
let r = run("git", ["fetch", "--quiet", "origin", "main"]);
|
|
268
372
|
if (r.status !== 0) {
|
|
269
373
|
console.error("[easel] git fetch failed");
|