@ammduncan/easel 0.2.4 → 0.2.5
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 +8 -0
- package/dist/cli.js +23 -4
- package/dist/client-setup.js +107 -18
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to easel. This project adheres to [Semantic Versioning](https://semver.org/).
|
|
4
4
|
|
|
5
|
+
## 0.2.5 — 2026-05-22
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- **Codex client support: `easel setup --client codex`.** Writes the MCP entry into `~/.codex/config.toml` under `[mcp_servers.easel]` and copies the `using-easel` skill into `~/.codex/skills/using-easel/SKILL.md` so Codex agents have the full style guide in addition to the tool description. Line-based TOML upsert preserves other sections and comments.
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
- **`easel setup --help` no longer silently runs the destructive Claude Code setup.** Previously, the `--help` flag fell through the `--client` check and reached `cmdSetup()`, which writes to `~/.claude/settings.json`. Now a dedicated help branch fires before any side effects. Includes a manual-install JSON snippet for clients beyond the four officially supported ones.
|
|
12
|
+
|
|
5
13
|
## 0.2.4 — 2026-05-22
|
|
6
14
|
|
|
7
15
|
### Fixed
|
package/dist/cli.js
CHANGED
|
@@ -23,10 +23,11 @@ Usage:
|
|
|
23
23
|
easel config preset paper set preset to paper | aurora | slate
|
|
24
24
|
easel config theme dark set theme to light | dark
|
|
25
25
|
easel config preset aurora theme light set both at once
|
|
26
|
-
easel setup
|
|
27
|
-
easel setup --client cursor
|
|
28
|
-
easel setup --client claude-desktop
|
|
29
|
-
easel setup --client windsurf
|
|
26
|
+
easel setup install Claude Code hook + register MCP in ~/.claude/settings.json
|
|
27
|
+
easel setup --client cursor register MCP in ~/.cursor/mcp.json
|
|
28
|
+
easel setup --client claude-desktop register MCP in ~/Library/Application Support/Claude/claude_desktop_config.json
|
|
29
|
+
easel setup --client windsurf register MCP in ~/.codeium/windsurf/mcp_config.json
|
|
30
|
+
easel setup --client codex register MCP in ~/.codex/config.toml + copy skill to ~/.codex/skills/
|
|
30
31
|
easel update git pull + npm install + build + setup (re-runs setup to apply new conventions)
|
|
31
32
|
easel mcp run the stdio MCP server in the foreground (used by clients)
|
|
32
33
|
easel restart kill the running HTTP server and respawn it (picks up new builds/paths)
|
|
@@ -336,6 +337,24 @@ async function main() {
|
|
|
336
337
|
await cmdUrl();
|
|
337
338
|
return;
|
|
338
339
|
case "setup": {
|
|
340
|
+
// Catch --help BEFORE doing anything destructive. The default
|
|
341
|
+
// cmdSetup() writes to ~/.claude/settings.json, so an unguarded
|
|
342
|
+
// `easel setup --help` would silently reconfigure Claude Code.
|
|
343
|
+
if (rest.includes("--help") || rest.includes("-h") || rest[0] === "help") {
|
|
344
|
+
console.log([
|
|
345
|
+
"easel setup — install easel into one of the supported MCP clients.",
|
|
346
|
+
"",
|
|
347
|
+
"Usage:",
|
|
348
|
+
" easel setup Claude Code (default): MCP + SessionStart hook + skill",
|
|
349
|
+
" easel setup --client <name> register MCP in another client",
|
|
350
|
+
"",
|
|
351
|
+
`Available clients: ${listClients().join(", ")}`,
|
|
352
|
+
"",
|
|
353
|
+
"Manual install (any other MCP client): drop this into the client's MCP config —",
|
|
354
|
+
' { "mcpServers": { "easel": { "command": "npx", "args": ["-y", "@ammduncan/easel"] } } }',
|
|
355
|
+
].join("\n"));
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
339
358
|
const clientIdx = rest.indexOf("--client");
|
|
340
359
|
if (clientIdx !== -1) {
|
|
341
360
|
const name = rest[clientIdx + 1];
|
package/dist/client-setup.js
CHANGED
|
@@ -1,36 +1,39 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
-
import { dirname, join } from "node:path";
|
|
1
|
+
import { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join, resolve } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
3
4
|
import { homedir, platform } from "node:os";
|
|
5
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
6
|
+
const PROJECT_ROOT = resolve(__dirname, "..");
|
|
4
7
|
const CLIENTS = {
|
|
5
8
|
"claude-desktop": {
|
|
6
9
|
name: "claude-desktop",
|
|
7
10
|
label: "Claude Desktop",
|
|
8
|
-
|
|
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
|
-
},
|
|
11
|
+
run: () => upsertJsonMcpServer(claudeDesktopConfigPath()),
|
|
19
12
|
postSetup: "Quit and relaunch Claude Desktop to load the MCP server.",
|
|
20
13
|
},
|
|
21
14
|
cursor: {
|
|
22
15
|
name: "cursor",
|
|
23
16
|
label: "Cursor",
|
|
24
|
-
|
|
17
|
+
run: () => upsertJsonMcpServer(join(homedir(), ".cursor", "mcp.json")),
|
|
25
18
|
postSetup: "Open Cursor and toggle MCP servers in Settings → Features → MCP, " +
|
|
26
19
|
"or restart Cursor for the registration to take effect.",
|
|
27
20
|
},
|
|
28
21
|
windsurf: {
|
|
29
22
|
name: "windsurf",
|
|
30
23
|
label: "Windsurf",
|
|
31
|
-
|
|
24
|
+
run: () => upsertJsonMcpServer(join(homedir(), ".codeium", "windsurf", "mcp_config.json")),
|
|
32
25
|
postSetup: "Restart Windsurf to load the MCP server.",
|
|
33
26
|
},
|
|
27
|
+
codex: {
|
|
28
|
+
name: "codex",
|
|
29
|
+
label: "Codex",
|
|
30
|
+
run: () => {
|
|
31
|
+
upsertTomlMcpServer(join(homedir(), ".codex", "config.toml"));
|
|
32
|
+
installEaselSkillTo(join(homedir(), ".codex", "skills", "using-easel"));
|
|
33
|
+
},
|
|
34
|
+
postSetup: "Restart Codex to load the MCP server. The using-easel skill has " +
|
|
35
|
+
"been copied to ~/.codex/skills/ so Codex will know when to push.",
|
|
36
|
+
},
|
|
34
37
|
};
|
|
35
38
|
export function listClients() {
|
|
36
39
|
return Object.keys(CLIENTS);
|
|
@@ -40,7 +43,17 @@ export function setupClient(name) {
|
|
|
40
43
|
if (!spec) {
|
|
41
44
|
throw new Error(`unknown client: ${name}`);
|
|
42
45
|
}
|
|
43
|
-
|
|
46
|
+
spec.run();
|
|
47
|
+
console.log(`[easel] ${spec.label} configured`);
|
|
48
|
+
console.log(` - ${spec.postSetup}`);
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Installs the easel MCP entry into a JSON config file with the standard
|
|
52
|
+
* `mcpServers: { name: { command, args } }` shape — used by Claude Desktop,
|
|
53
|
+
* Cursor, Windsurf, and friends. Merges into any existing config; preserves
|
|
54
|
+
* sibling top-level keys and other registered MCP servers.
|
|
55
|
+
*/
|
|
56
|
+
function upsertJsonMcpServer(configPath) {
|
|
44
57
|
const config = readJson(configPath);
|
|
45
58
|
const mcpServers = config.mcpServers ?? {};
|
|
46
59
|
mcpServers.easel = {
|
|
@@ -50,9 +63,74 @@ export function setupClient(name) {
|
|
|
50
63
|
config.mcpServers = mcpServers;
|
|
51
64
|
mkdirSync(dirname(configPath), { recursive: true });
|
|
52
65
|
writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`);
|
|
53
|
-
console.log(`[easel] ${spec.label} configured`);
|
|
54
66
|
console.log(` - wrote ${configPath}`);
|
|
55
|
-
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Installs the easel MCP entry into a TOML config file under the
|
|
70
|
+
* `[mcp_servers.easel]` section — used by Codex. Line-based upsert: replaces
|
|
71
|
+
* the existing `[mcp_servers.easel]` block in place if it's there, otherwise
|
|
72
|
+
* appends. Other sections and comments preserved.
|
|
73
|
+
*/
|
|
74
|
+
function upsertTomlMcpServer(configPath) {
|
|
75
|
+
const newSection = "[mcp_servers.easel]\n" +
|
|
76
|
+
'command = "npx"\n' +
|
|
77
|
+
'args = ["-y", "@ammduncan/easel"]\n';
|
|
78
|
+
const existing = existsSync(configPath) ? readFileSync(configPath, "utf-8") : "";
|
|
79
|
+
const updated = upsertTomlSection(existing, "mcp_servers.easel", newSection);
|
|
80
|
+
mkdirSync(dirname(configPath), { recursive: true });
|
|
81
|
+
writeFileSync(configPath, updated);
|
|
82
|
+
console.log(` - wrote ${configPath}`);
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Line-based TOML section upsert. Replaces an existing `[<header>]` block
|
|
86
|
+
* (defined as the lines from the header up to the next top-level `[...]`
|
|
87
|
+
* header or EOF) with `newBlock`; appends if no such block exists. Other
|
|
88
|
+
* sections are left untouched. Adequate for our narrow use case — not a
|
|
89
|
+
* general-purpose TOML editor.
|
|
90
|
+
*/
|
|
91
|
+
function upsertTomlSection(content, header, newBlock) {
|
|
92
|
+
const targetHeader = `[${header}]`;
|
|
93
|
+
const lines = content.split("\n");
|
|
94
|
+
let startIdx = -1;
|
|
95
|
+
let endIdx = lines.length;
|
|
96
|
+
for (let i = 0; i < lines.length; i++) {
|
|
97
|
+
if (lines[i].trim() === targetHeader) {
|
|
98
|
+
startIdx = i;
|
|
99
|
+
for (let j = i + 1; j < lines.length; j++) {
|
|
100
|
+
const stripped = lines[j].trim();
|
|
101
|
+
if (/^\[[^\]]+\]$/.test(stripped)) {
|
|
102
|
+
endIdx = j;
|
|
103
|
+
break;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
break;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
const newBlockLines = newBlock.replace(/\n+$/, "").split("\n");
|
|
110
|
+
if (startIdx === -1) {
|
|
111
|
+
const trailing = content.length === 0 || content.endsWith("\n") ? "" : "\n";
|
|
112
|
+
const spacer = content.length === 0 ? "" : "\n";
|
|
113
|
+
return content + trailing + spacer + newBlockLines.join("\n") + "\n";
|
|
114
|
+
}
|
|
115
|
+
const before = lines.slice(0, startIdx);
|
|
116
|
+
const after = lines.slice(endIdx);
|
|
117
|
+
const trailingBlank = after.length > 0 && after[0] !== "" ? [""] : [];
|
|
118
|
+
return [...before, ...newBlockLines, ...trailingBlank, ...after].join("\n");
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Copies the bundled `using-easel/SKILL.md` into a target skills directory.
|
|
122
|
+
* Used by clients that have a skill-discovery mechanism (Claude Code,
|
|
123
|
+
* Codex).
|
|
124
|
+
*/
|
|
125
|
+
export function installEaselSkillTo(destDir) {
|
|
126
|
+
const src = resolve(PROJECT_ROOT, "skills", "using-easel", "SKILL.md");
|
|
127
|
+
if (!existsSync(src)) {
|
|
128
|
+
console.warn(` - skipped skill install: source missing at ${src}`);
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
mkdirSync(destDir, { recursive: true });
|
|
132
|
+
copyFileSync(src, join(destDir, "SKILL.md"));
|
|
133
|
+
console.log(` - installed using-easel skill into ${destDir}`);
|
|
56
134
|
}
|
|
57
135
|
function readJson(path) {
|
|
58
136
|
if (!existsSync(path))
|
|
@@ -71,3 +149,14 @@ function readJson(path) {
|
|
|
71
149
|
throw new Error(`couldn't parse existing config at ${path}: ${err.message}`);
|
|
72
150
|
}
|
|
73
151
|
}
|
|
152
|
+
function claudeDesktopConfigPath() {
|
|
153
|
+
const home = homedir();
|
|
154
|
+
if (platform() === "darwin") {
|
|
155
|
+
return join(home, "Library", "Application Support", "Claude", "claude_desktop_config.json");
|
|
156
|
+
}
|
|
157
|
+
if (platform() === "win32") {
|
|
158
|
+
const appData = process.env.APPDATA ?? join(home, "AppData", "Roaming");
|
|
159
|
+
return join(appData, "Claude", "claude_desktop_config.json");
|
|
160
|
+
}
|
|
161
|
+
return join(home, ".config", "Claude", "claude_desktop_config.json");
|
|
162
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ammduncan/easel",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.5",
|
|
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",
|