stoaken 0.3.0 → 0.5.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.
Files changed (3) hide show
  1. package/README.md +127 -85
  2. package/dist/index.js +361 -19
  3. package/package.json +51 -51
package/README.md CHANGED
@@ -1,85 +1,127 @@
1
- # stoaken
2
-
3
- The official CLI for [Stoaken](https://www.stoaken.com) -- a design token management platform (Brands, Token Sets, Themes, DTCG-conformant token storage, versioned Releases with an approval gate) that AI coding agents and MCP-capable tools can read and propose changes to directly.
4
-
5
- ## Quickstart
6
-
7
- ```
8
- npx stoaken init
9
- ```
10
-
11
- Run it in the root of the project you want connected to a Stoaken Brand. It will:
12
-
13
- 1. Ask how you'd like to sign in -- open your browser for a real OAuth 2.1 login, or let you paste an API key you've already created at `/settings/api-keys`.
14
- 2. List the real Brands your account can see, and let you pick one.
15
- 3. Write `stoaken.config.json` (which Brand, which server -- safe to commit, holds no secret) and `.stoaken/credentials.json` (your bearer credential, `chmod 600`, automatically added to `.gitignore`).
16
- 4. Generate `.stoaken/SKILL.md` -- a short brief for AI coding agents (Claude Code, Cursor) explaining how to connect to this project's Stoaken MCP server and what its tools do.
17
-
18
- Run `stoaken init` again in an already-configured directory to continue with the same Brand, switch to a different one, or re-authenticate.
19
-
20
- ## Authentication
21
-
22
- Two paths, both landing on the same bearer credential:
23
-
24
- - **Log in with browser** -- a real OAuth 2.1 Authorization Code + PKCE flow. The CLI discovers Stoaken's Authorization Server automatically (via its `/.well-known/oauth-protected-resource` metadata), registers itself as an OAuth client on first run, and opens your browser to sign in.
25
- - **Paste an API key** -- for CI, scripts, or anywhere a browser isn't available. Create one at `/settings/api-keys` in the Stoaken app.
26
-
27
- ## Export your tokens as code
28
-
29
- `stoaken export` writes a theme's live release as a file: CSS variables, W3C JSON, Style
30
- Dictionary v4 JSON, or a JS/TS module. There are no timestamps in the output, so committed files
31
- give clean diffs.
32
-
33
- ```sh
34
- npx stoaken export --theme Default --format css --out src/styles/tokens.css
35
- npx stoaken export --theme Default --format json-dtcg --mode color-scheme:dark --out tokens/dark.json
36
- npx stoaken export --theme Default --format ts --out src/tokens.ts
37
- npx stoaken export --theme Default --out - # to stdout
38
- ```
39
-
40
- | Option | |
41
- | --- | --- |
42
- | `--theme <name\|id>` | Which theme (needed when the Brand has more than one) |
43
- | `--format` | `css` (default), `json-dtcg`, `json-sd4`, `js`, `ts` |
44
- | `--mode <key>` | A mode combination for the one-mode formats; CSS includes every mode |
45
- | `--prefix <prefix>` | CSS only: a prefix for every variable name |
46
- | `--release <id>` | One release (e.g. one still in review) instead of the live one |
47
- | `--out <file\|->` | Where to write; default is the server's file name |
48
- | `--check` | Write nothing; exit 1 if the file on disk is out of date |
49
-
50
- Save your targets in `stoaken.config.json` and a plain `npx stoaken export` runs them all
51
- (`npx stoaken export --check` checks them all):
52
-
53
- ```json
54
- {
55
- "brandId": "…",
56
- "brandName": "Acme",
57
- "serverUrl": "https://www.stoaken.com",
58
- "exports": [
59
- { "theme": "Default", "format": "css", "out": "src/styles/tokens.css" },
60
- { "theme": "Default", "format": "ts", "out": "src/tokens.ts" }
61
- ]
62
- }
63
- ```
64
-
65
- **In CI**, set `STOAKEN_TOKEN` to an API key (Settings > API keys); it's used instead of the
66
- sign-in saved by `stoaken init`. For example, to fail a build when the committed tokens are behind
67
- the live release:
68
-
69
- ```sh
70
- STOAKEN_TOKEN=${{ secrets.STOAKEN_TOKEN }} npx stoaken export --check
71
- ```
72
-
73
- ## What this connects to
74
-
75
- Once set up, any MCP-capable client (Claude Code, Cursor, Claude Desktop) or a direct HTTP client can use the credential in `.stoaken/credentials.json` as a Bearer token against Stoaken's MCP server, which exposes four tools:
76
-
77
- - `list_brands` -- every Brand you hold at least Viewer on.
78
- - `get_theme_tokens(themeId)` -- a Theme's cascade-merged token tree.
79
- - `list_releases(themeId)` -- a Theme's Releases with version, status, and change summary.
80
- - `propose_token_change(themeId, tokenSetId, document, version, versionType)` -- proposes a token change and submits it for approval. **This never publishes.** A human reviewer always approves through the Stoaken Command Center before it goes live.
81
-
82
- ## Links
83
-
84
- - [Stoaken on GitHub](https://github.com/craig-astravale/stoaken)
85
- - [Report an issue](https://github.com/craig-astravale/stoaken/issues)
1
+ # stoaken
2
+
3
+ The official CLI for [Stoaken](https://www.stoaken.com) -- a design token management platform (Brands, Token Sets, Themes, DTCG-conformant token storage, versioned Releases with an approval gate) that AI coding agents and MCP-capable tools can read and propose changes to directly.
4
+
5
+ ## Quickstart
6
+
7
+ ```
8
+ npx stoaken init
9
+ ```
10
+
11
+ Run it in the root of the project you want connected to a Stoaken Brand. It will:
12
+
13
+ 1. Ask how you'd like to sign in -- open your browser for a real OAuth 2.1 login, or let you paste an API key you've already created at `/settings/api-keys`.
14
+ 2. List the real Brands your account can see, and let you pick one.
15
+ 3. Write `stoaken.config.json` (which Brand, which server -- safe to commit, holds no secret) and `.stoaken/credentials.json` (your bearer credential, `chmod 600`, automatically added to `.gitignore`).
16
+ 4. Ask which AI tool to connect (or take `--client`) and write that tool's MCP config.
17
+ 5. Generate `.stoaken/SKILL.md` -- a short brief for AI coding agents explaining how this project connects to Stoaken's MCP server and what its tools do.
18
+
19
+ Run `stoaken init` again in an already-configured directory to continue with the same Brand, switch to a different one, or re-authenticate.
20
+
21
+ ## Connect an AI tool
22
+
23
+ ```
24
+ npx stoaken init --client cursor
25
+ ```
26
+
27
+ | `--client` | What init writes |
28
+ | --- | --- |
29
+ | `claude-code` | `.mcp.json` (`mcpServers.stoaken`, `type: http`) |
30
+ | `cursor` | `.cursor/mcp.json` (`mcpServers.stoaken`) |
31
+ | `codex` | `.codex/config.toml` (`[mcp_servers.stoaken]`; loads for trusted projects only) |
32
+ | `copilot` | `.vscode/mcp.json` (`servers.stoaken`, VS Code agent mode) |
33
+ | `claude-desktop` | Nothing: prints the steps to add a custom connector (Customize > Connectors) |
34
+ | `v0` | Nothing: prints the steps to add a custom MCP server in v0 (+ > MCPs) |
35
+ | `windsurf` | `.devin/mcp_config.json` (`mcpServers.stoaken`, URL only). Windsurf is now Devin Desktop; this is for its Devin Local agent and Devin CLI |
36
+ | `zed` | `.zed/settings.json` (`context_servers.stoaken`, URL only; OAuth). With an API key: nothing, prints how to add it to your user settings |
37
+ | `gemini-cli` | `.gemini/settings.json` (`mcpServers.stoaken.httpUrl`; loads in trusted folders only) |
38
+ | `jetbrains` | `.junie/mcp/mcp.json` (`mcpServers.stoaken`, URL only; Junie). With an API key: nothing, prints how to add it to `~/.junie/mcp/mcp.json` |
39
+ | `cline` | Nothing: prints the steps for Cline's Remote Servers tab |
40
+ | `lovable` | Nothing: prints the steps to add a custom MCP server (Connectors > Add custom) |
41
+ | `bolt` | Nothing: prints the steps to add a custom connector (+ > Connectors) |
42
+ | `replit` | Nothing: prints a one-click Replit install link and the manual steps |
43
+
44
+ init merges: other servers in the file are kept and only the `stoaken` entry is replaced. If the
45
+ file isn't plain JSON (for example `.vscode/mcp.json` or `.zed/settings.json` with comments) it is
46
+ left alone and you're told. No secret goes into these files, so they are safe to commit:
47
+
48
+ - Signed in with the browser: the entry has just the URL, and the tool signs you in itself with
49
+ OAuth (Claude Code `/mcp`, Cursor Connect, `codex mcp login stoaken`, VS Code Start,
50
+ `/mcp auth stoaken` in Gemini CLI, Authenticate in Devin Local, Zed's sign-in prompt, Junie's
51
+ `/mcp` Authorize).
52
+ - Pasted an API key: the entry reads the `STOAKEN_API_KEY` environment variable (VS Code instead
53
+ asks for the key once and stores it securely). Devin Local, Zed and Junie document no variable
54
+ expansion in headers, so init never writes the key for them: Devin gets a gitignored
55
+ `.devin/mcp_config.local.json` override (`devin mcp add -s local ... -H`), and Zed and Junie are
56
+ told to use a user-level file outside the repo.
57
+
58
+ ## Authentication
59
+
60
+ Two paths, both landing on the same bearer credential:
61
+
62
+ - **Log in with browser** -- a real OAuth 2.1 Authorization Code + PKCE flow. The CLI discovers Stoaken's Authorization Server automatically (via its `/.well-known/oauth-protected-resource` metadata), registers itself as an OAuth client on first run, and opens your browser to sign in.
63
+ - **Paste an API key** -- for CI, scripts, or anywhere a browser isn't available. Create one at `/settings/api-keys` in the Stoaken app.
64
+
65
+ ## Export your tokens as code
66
+
67
+ `stoaken export` writes a theme's live release as a file: CSS variables, W3C JSON, Style
68
+ Dictionary v4 JSON, or a JS/TS module. There are no timestamps in the output, so committed files
69
+ give clean diffs.
70
+
71
+ ```sh
72
+ npx stoaken export --theme Default --format css --out src/styles/tokens.css
73
+ npx stoaken export --theme Default --format json-dtcg --mode color-scheme:dark --out tokens/dark.json
74
+ npx stoaken export --theme Default --format ts --out src/tokens.ts
75
+ npx stoaken export --theme Default --out - # to stdout
76
+ ```
77
+
78
+ | Option | |
79
+ | --- | --- |
80
+ | `--theme <name\|id>` | Which theme (needed when the Brand has more than one) |
81
+ | `--format` | `css` (default), `json-dtcg`, `json-sd4`, `js`, `ts` |
82
+ | `--mode <key>` | A mode combination for the one-mode formats; CSS includes every mode |
83
+ | `--prefix <prefix>` | CSS only: a prefix for every variable name |
84
+ | `--release <id>` | One release (e.g. one still in review) instead of the live one |
85
+ | `--out <file\|->` | Where to write; default is the server's file name |
86
+ | `--check` | Write nothing; exit 1 if the file on disk is out of date |
87
+
88
+ Save your targets in `stoaken.config.json` and a plain `npx stoaken export` runs them all
89
+ (`npx stoaken export --check` checks them all):
90
+
91
+ ```json
92
+ {
93
+ "brandId": "…",
94
+ "brandName": "Acme",
95
+ "serverUrl": "https://www.stoaken.com",
96
+ "exports": [
97
+ { "theme": "Default", "format": "css", "out": "src/styles/tokens.css" },
98
+ { "theme": "Default", "format": "ts", "out": "src/tokens.ts" }
99
+ ]
100
+ }
101
+ ```
102
+
103
+ **In CI**, set `STOAKEN_TOKEN` to an API key (Settings > API keys); it's used instead of the
104
+ sign-in saved by `stoaken init`. For example, to fail a build when the committed tokens are behind
105
+ the live release:
106
+
107
+ ```sh
108
+ STOAKEN_TOKEN=${{ secrets.STOAKEN_TOKEN }} npx stoaken export --check
109
+ ```
110
+
111
+ `--check` ignores CRLF vs LF line endings (unreleased change, Stage 332), so Windows runners
112
+ that check files out as CRLF don't fail it. Ready-made pipelines for GitHub Actions, GitLab CI,
113
+ Bitbucket Pipelines, Azure Pipelines and Vercel builds: see /docs/webhooks#ci and /docs/cli#ci.
114
+
115
+ ## What this connects to
116
+
117
+ Once set up, any MCP-capable client (Claude Code, Claude Desktop, Cursor, Codex, Copilot, v0, Devin Desktop, Zed, Gemini CLI, Junie, Cline, Lovable, Bolt, Replit) or a direct HTTP client can reach Stoaken's MCP server at `/mcp`. Its main tools (full list at /docs/mcp):
118
+
119
+ - `list_brands` -- every Brand you hold at least Viewer on.
120
+ - `get_theme_tokens(themeId)` -- a Theme's cascade-merged token tree.
121
+ - `list_releases(themeId)` -- a Theme's Releases with version, status, and change summary.
122
+ - `propose_token_change(themeId, tokenSetId, document, version, versionType)` -- proposes a token change and submits it for approval. **This never publishes.** A human reviewer always approves through the Stoaken Command Center before it goes live.
123
+
124
+ ## Links
125
+
126
+ - [Stoaken on GitHub](https://github.com/craig-astravale/stoaken)
127
+ - [Report an issue](https://github.com/craig-astravale/stoaken/issues)
package/dist/index.js CHANGED
@@ -209,13 +209,12 @@ async function promptForApiKey() {
209
209
  }
210
210
 
211
211
  // src/mcp-client.ts
212
- import { Client } from "@modelcontextprotocol/sdk/client/index.js";
213
- import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
212
+ import { Client, StreamableHTTPClientTransport } from "@modelcontextprotocol/client";
214
213
  async function listBrands(serverUrl, credential) {
215
214
  const transport = new StreamableHTTPClientTransport(new URL("/mcp", serverUrl), {
216
215
  requestInit: { headers: { Authorization: `Bearer ${credential}` } }
217
216
  });
218
- const client = new Client({ name: "stoaken-cli", version: "0.3.0" });
217
+ const client = new Client({ name: "stoaken-cli", version: "0.5.0" });
219
218
  await client.connect(transport);
220
219
  try {
221
220
  const result = await client.callTool({ name: "list_brands", arguments: {} });
@@ -229,16 +228,306 @@ async function listBrands(serverUrl, credential) {
229
228
  }
230
229
  }
231
230
 
231
+ // src/client-config.ts
232
+ var CLIENTS = [
233
+ "claude-code",
234
+ "claude-desktop",
235
+ "cursor",
236
+ "codex",
237
+ "copilot",
238
+ "v0",
239
+ "windsurf",
240
+ "zed",
241
+ "gemini-cli",
242
+ "jetbrains",
243
+ "cline",
244
+ "lovable",
245
+ "bolt",
246
+ "replit"
247
+ ];
248
+ var SERVER_NAME = "stoaken";
249
+ var API_KEY_ENV = "STOAKEN_API_KEY";
250
+ var CLIENT_LABELS = {
251
+ "claude-code": "Claude Code",
252
+ "claude-desktop": "Claude Desktop / claude.ai",
253
+ cursor: "Cursor",
254
+ codex: "OpenAI Codex",
255
+ copilot: "GitHub Copilot (VS Code)",
256
+ v0: "v0 (Vercel)",
257
+ windsurf: "Windsurf / Devin Desktop",
258
+ zed: "Zed",
259
+ "gemini-cli": "Gemini CLI",
260
+ jetbrains: "JetBrains (Junie)",
261
+ cline: "Cline",
262
+ lovable: "Lovable",
263
+ bolt: "Bolt (bolt.new)",
264
+ replit: "Replit Agent"
265
+ };
266
+ var JSON_CLIENTS = ["claude-code", "cursor", "copilot", "windsurf", "zed", "gemini-cli", "jetbrains"];
267
+ var SERVERS_KEY = {
268
+ "claude-code": "mcpServers",
269
+ cursor: "mcpServers",
270
+ copilot: "servers",
271
+ windsurf: "mcpServers",
272
+ zed: "context_servers",
273
+ "gemini-cli": "mcpServers",
274
+ jetbrains: "mcpServers"
275
+ };
276
+ function isClient(value) {
277
+ return CLIENTS.includes(value);
278
+ }
279
+ function isJsonClient(client) {
280
+ return JSON_CLIENTS.includes(client);
281
+ }
282
+ function mcpUrl(serverUrl) {
283
+ return `${serverUrl.replace(/\/+$/, "")}/mcp`;
284
+ }
285
+ function replitInstallLink(serverUrl) {
286
+ const json = JSON.stringify({ displayName: "Stoaken", baseUrl: mcpUrl(serverUrl) });
287
+ return `https://replit.com/integrations?mcp=${Buffer.from(json, "utf8").toString("base64")}`;
288
+ }
289
+ function planFor(client, auth, serverUrl) {
290
+ const url = mcpUrl(serverUrl);
291
+ const keyStep = `Set ${API_KEY_ENV} in your shell to an API key from Settings > API keys (never commit it).`;
292
+ const oauthInstead = "Or run init again, sign in with the browser, and use OAuth instead.";
293
+ switch (client) {
294
+ case "claude-code":
295
+ return {
296
+ file: ".mcp.json",
297
+ nextSteps: auth === "oauth" ? ["Start Claude Code, approve the project server, then run /mcp and choose stoaken to sign in."] : [keyStep, "Start Claude Code and approve the project server."]
298
+ };
299
+ case "cursor":
300
+ return {
301
+ file: ".cursor/mcp.json",
302
+ nextSteps: auth === "oauth" ? ["Open Cursor Settings > MCP, enable stoaken and click Connect to sign in."] : [keyStep, "Restart Cursor so it picks up the variable, then enable stoaken in Settings > MCP."]
303
+ };
304
+ case "codex":
305
+ return {
306
+ file: ".codex/config.toml",
307
+ nextSteps: auth === "oauth" ? ["Trust this project in Codex (project config only loads for trusted projects).", "Run: codex mcp login stoaken"] : [keyStep, "Trust this project in Codex (project config only loads for trusted projects)."]
308
+ };
309
+ case "copilot":
310
+ return {
311
+ file: ".vscode/mcp.json",
312
+ nextSteps: auth === "oauth" ? ["In VS Code open .vscode/mcp.json, click Start above stoaken and sign in in the browser.", "Use Copilot Chat in Agent mode."] : ["In VS Code open .vscode/mcp.json, click Start above stoaken and paste an API key when asked (VS Code stores it securely).", "Use Copilot Chat in Agent mode."],
313
+ keyNote: "VS Code asks for the key once and stores it securely"
314
+ };
315
+ case "claude-desktop":
316
+ return {
317
+ file: null,
318
+ nextSteps: [
319
+ "Claude Desktop and claude.ai add remote servers as connectors, not through a project file.",
320
+ "Customize > Connectors > + Add > Add custom connector.",
321
+ `Name: Stoaken. URL: ${url}. Sign in with OAuth (Register automatically), or add the header Authorization: Bearer <API key>.`,
322
+ "On Team/Enterprise only an Owner can add it. Connections come from Anthropic's cloud, so this needs the public server."
323
+ ]
324
+ };
325
+ case "v0":
326
+ return {
327
+ file: null,
328
+ nextSteps: [
329
+ "v0 adds MCP servers in its own UI, not through a project file.",
330
+ "In a v0 chat: + > MCPs > custom server.",
331
+ `URL: ${url}. Auth: OAuth, or Bearer Token with an API key from Settings > API keys.`,
332
+ "v0 uses the tools while it generates; the app it builds cannot call them."
333
+ ]
334
+ };
335
+ case "windsurf":
336
+ return {
337
+ file: ".devin/mcp_config.json",
338
+ nextSteps: auth === "oauth" ? [
339
+ "Open this project in Devin Desktop (formerly Windsurf) with the Devin Local agent. Devin CLI reads the same file.",
340
+ "When stoaken shows Needs auth in the MCP list, click Authenticate (or run: devin mcp login stoaken)."
341
+ ] : [
342
+ keyStep,
343
+ `Devin documents no variable expansion in MCP headers, so add the key to the gitignored local override: devin mcp add -s local stoaken ${url} -H "Authorization: Bearer $${API_KEY_ENV}"`,
344
+ "That override replaces the project entry for you only. The legacy Cascade agent ignores project files (see the guide)."
345
+ ],
346
+ keyNote: "the key goes in the gitignored `.devin/mcp_config.local.json` override"
347
+ };
348
+ case "zed":
349
+ return auth === "oauth" ? {
350
+ file: ".zed/settings.json",
351
+ nextSteps: [
352
+ "Trust this project in Zed: in Restricted Mode project settings and their MCP servers don't load.",
353
+ "Zed asks you to sign in to stoaken (MCP OAuth). Settings > AI > MCP Servers shows a green dot when it's active."
354
+ ]
355
+ } : {
356
+ file: null,
357
+ nextSteps: [
358
+ "Zed documents no variable expansion in headers, so the key can't go in a project file.",
359
+ `Run "zed: open settings file" and add under "context_servers": "stoaken": { "url": "${url}", "headers": { "Authorization": "Bearer <API key>" } }`,
360
+ oauthInstead
361
+ ]
362
+ };
363
+ case "gemini-cli":
364
+ return {
365
+ file: ".gemini/settings.json",
366
+ nextSteps: auth === "oauth" ? ["Trust this folder (gemini trust): untrusted folders ignore .gemini/settings.json.", "In Gemini CLI run /mcp auth stoaken to sign in in the browser."] : [keyStep, "Trust this folder (gemini trust): untrusted folders ignore .gemini/settings.json."]
367
+ };
368
+ case "jetbrains":
369
+ return auth === "oauth" ? {
370
+ file: ".junie/mcp/mcp.json",
371
+ nextSteps: [
372
+ "Junie in the IDE: Settings > Tools > Junie > MCP Settings lists stoaken and its status.",
373
+ "Junie CLI: run /mcp, select stoaken, choose Authorize and sign in in the browser."
374
+ ]
375
+ } : {
376
+ file: null,
377
+ nextSteps: [
378
+ "Junie has no secret storage or variable expansion for MCP configs, so the key can't go in a project file.",
379
+ `In your user file ~/.junie/mcp/mcp.json, under "mcpServers", add "stoaken": { "url": "${url}", "headers": { "Authorization": "Bearer <API key>" } }`,
380
+ oauthInstead
381
+ ]
382
+ };
383
+ case "cline":
384
+ return {
385
+ file: null,
386
+ nextSteps: [
387
+ "Cline keeps MCP servers in its own settings, not a project file.",
388
+ ...auth === "oauth" ? [
389
+ `Cline panel > MCP Servers > Remote Servers: name stoaken, URL ${url}, transport Streamable HTTP, Add Server.`,
390
+ "If it asks you to authenticate, sign in in the browser (Cline CLI: cline mcp > Authorize OAuth)."
391
+ ] : [
392
+ `Cline panel > MCP Servers > Configure > Configure MCP Servers, and add under "mcpServers": "stoaken": { "type": "streamableHttp", "url": "${url}", "headers": { "Authorization": "Bearer <API key>" } }`
393
+ ]
394
+ ]
395
+ };
396
+ case "lovable":
397
+ return {
398
+ file: null,
399
+ nextSteps: [
400
+ "Lovable adds MCP servers in its own UI, not through a project file.",
401
+ "Connectors > Add custom > MCP server. Server name: Stoaken.",
402
+ `Server URL: ${url}. Authentication: OAuth (Add & authorize), or Bearer token with an API key.`,
403
+ "The connection is yours alone and only for Lovable's chat; the app it publishes can't call it."
404
+ ]
405
+ };
406
+ case "bolt":
407
+ return {
408
+ file: null,
409
+ nextSteps: [
410
+ "Bolt adds MCP servers in its own UI, not through a project file.",
411
+ "In the chatbox: + > Connectors > Manage connectors > Custom MCP server.",
412
+ `Name: Stoaken. URL: ${url}. Transport type: HTTP. Authentication: MCP OAuth.`,
413
+ "Then turn the connector on for the project (+ > Connectors)."
414
+ ]
415
+ };
416
+ case "replit":
417
+ return {
418
+ file: null,
419
+ nextSteps: [
420
+ "Replit adds MCP servers in its own UI, not through a project file.",
421
+ `Open ${replitInstallLink(serverUrl)} (or Integrations > MCP Servers > + Add MCP server, URL ${url}).`,
422
+ auth === "oauth" ? "Click Test & save and sign in when Replit runs the OAuth flow." : "Under Advanced settings add the header Authorization: Bearer <API key>, then Test & save."
423
+ ]
424
+ };
425
+ }
426
+ }
427
+ function jsonEntry(client, auth, serverUrl) {
428
+ const url = mcpUrl(serverUrl);
429
+ if (client === "windsurf" || client === "zed" || client === "jetbrains") return { url };
430
+ if (client === "gemini-cli") {
431
+ return auth === "oauth" ? { httpUrl: url } : { httpUrl: url, headers: { Authorization: `Bearer \${${API_KEY_ENV}}` } };
432
+ }
433
+ if (client === "claude-code") {
434
+ return auth === "oauth" ? { type: "http", url } : { type: "http", url, headers: { Authorization: `Bearer \${${API_KEY_ENV}}` } };
435
+ }
436
+ if (client === "cursor") {
437
+ return auth === "oauth" ? { url } : { url, headers: { Authorization: `Bearer \${env:${API_KEY_ENV}}` } };
438
+ }
439
+ return auth === "oauth" ? { type: "http", url } : { type: "http", url, headers: { Authorization: "Bearer ${input:stoaken-api-key}" } };
440
+ }
441
+ var ConfigMergeError = class extends Error {
442
+ };
443
+ function parseObject(existing, file) {
444
+ if (existing === null || existing.trim() === "") return {};
445
+ let parsed;
446
+ try {
447
+ parsed = JSON.parse(existing);
448
+ } catch {
449
+ throw new ConfigMergeError(`${file} isn't plain JSON (comments?), so it was left alone. Add the stoaken entry by hand.`);
450
+ }
451
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
452
+ throw new ConfigMergeError(`${file} isn't a JSON object, so it was left alone.`);
453
+ }
454
+ return parsed;
455
+ }
456
+ var COPILOT_INPUT = {
457
+ type: "promptString",
458
+ id: "stoaken-api-key",
459
+ description: "Stoaken API key (Settings > API keys)",
460
+ password: true
461
+ };
462
+ function mergeJsonConfig(client, existing, auth, serverUrl) {
463
+ const file = planFor(client, auth, serverUrl).file;
464
+ if (file === null) throw new ConfigMergeError(`${CLIENT_LABELS[client]} gets no project file with an API key; follow the steps instead.`);
465
+ const doc = parseObject(existing, file);
466
+ const key = SERVERS_KEY[client];
467
+ const current = doc[key];
468
+ if (current !== void 0 && (typeof current !== "object" || current === null || Array.isArray(current))) {
469
+ throw new ConfigMergeError(`${file} has a "${key}" that isn't an object, so it was left alone.`);
470
+ }
471
+ doc[key] = { ...current, [SERVER_NAME]: jsonEntry(client, auth, serverUrl) };
472
+ if (client === "copilot") {
473
+ const inputs = Array.isArray(doc.inputs) ? doc.inputs : [];
474
+ const others = inputs.filter((i) => !(typeof i === "object" && i !== null && i.id === COPILOT_INPUT.id));
475
+ if (auth === "api_key") doc.inputs = [...others, COPILOT_INPUT];
476
+ else if (doc.inputs !== void 0) doc.inputs = others;
477
+ }
478
+ return `${JSON.stringify(doc, null, 2)}
479
+ `;
480
+ }
481
+ function codexTable(auth, serverUrl) {
482
+ const lines = [`[mcp_servers.${SERVER_NAME}]`, `url = ${JSON.stringify(mcpUrl(serverUrl))}`];
483
+ if (auth === "api_key") lines.push(`bearer_token_env_var = "${API_KEY_ENV}"`);
484
+ return `${lines.join("\n")}
485
+ `;
486
+ }
487
+ function mergeCodexToml(existing, auth, serverUrl) {
488
+ const table = codexTable(auth, serverUrl);
489
+ if (existing === null || existing.trim() === "") return table;
490
+ const header = /^\s*\[\s*mcp_servers\.(?:stoaken|"stoaken")\s*(?:\]|\.)/;
491
+ const anyHeader = /^\s*\[/;
492
+ const kept = [];
493
+ let skipping = false;
494
+ for (const line of existing.split("\n")) {
495
+ if (header.test(line)) {
496
+ skipping = true;
497
+ continue;
498
+ }
499
+ if (skipping && anyHeader.test(line)) skipping = false;
500
+ if (!skipping) kept.push(line);
501
+ }
502
+ const body = kept.join("\n").replace(/\n+$/, "");
503
+ return body === "" ? table : `${body}
504
+
505
+ ${table}`;
506
+ }
507
+
232
508
  // src/skill-template.ts
233
- function generateSkillFile(config) {
509
+ function connectSection(target, serverUrl) {
510
+ if (!target) {
511
+ return `Connect with the credential in \`.stoaken/credentials.json\` as a Bearer token (never commit that
512
+ file -- it's already in \`.gitignore\`).
513
+ `;
514
+ }
515
+ const { client, auth } = target;
516
+ const plan = planFor(client, auth, serverUrl);
517
+ const how = auth === "oauth" ? "the client signs in with OAuth" : plan.keyNote ?? `it reads the \`${API_KEY_ENV}\` environment variable`;
518
+ const where = plan.file ? `It is configured for ${CLIENT_LABELS[client]} in \`${plan.file}\` (no secret in that file: ${how}).` : `${CLIENT_LABELS[client]} connects through its own settings, not a project file.`;
519
+ return `${where}
520
+
521
+ ${plan.nextSteps.map((step) => `- ${step}`).join("\n")}
522
+ `;
523
+ }
524
+ function generateSkillFile(config, client) {
234
525
  return `# Working with Stoaken
235
526
 
236
527
  This project is connected to the **${config.brandName}** Brand on Stoaken (\`${config.serverUrl}\`).
237
528
 
238
- Stoaken's MCP server exposes the tools below. Connect to it at \`${config.serverUrl}/mcp\` using
239
- the credential in \`.stoaken/credentials.json\` as a Bearer token (never commit that file --
240
- it's already in \`.gitignore\`).
241
-
529
+ Stoaken's MCP server exposes the tools below at \`${mcpUrl(config.serverUrl)}\`.
530
+ ${connectSection(client, config.serverUrl)}
242
531
  ## Tools
243
532
 
244
533
  - **list_brands** -- every Brand you hold at least Viewer on.
@@ -283,6 +572,9 @@ Re-run \`stoaken init\` if you need to switch Brands or re-authenticate.
283
572
  }
284
573
 
285
574
  // src/commands/init.ts
575
+ import { existsSync as existsSync2 } from "fs";
576
+ import { mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
577
+ import { dirname, join as join2 } from "path";
286
578
  var DEFAULT_SERVER_URL = "https://www.stoaken.com";
287
579
  async function runInit(options) {
288
580
  const cwd = process.cwd();
@@ -378,13 +670,49 @@ async function runInit(options) {
378
670
  };
379
671
  await writeConfig(cwd, config);
380
672
  await ensureGitignored(cwd);
381
- await writeSkillFile(cwd, generateSkillFile(config));
673
+ let client = null;
674
+ if (options.client) {
675
+ client = options.client;
676
+ } else {
677
+ const pick = await p2.select({
678
+ message: "Which AI tool should connect to Stoaken?",
679
+ options: [
680
+ ...CLIENTS.map((c) => ({ value: c, label: CLIENT_LABELS[c] })),
681
+ { value: "none", label: "None for now (just write .stoaken/SKILL.md)" }
682
+ ]
683
+ });
684
+ if (p2.isCancel(pick)) {
685
+ p2.cancel("Cancelled.");
686
+ return;
687
+ }
688
+ client = pick === "none" ? null : pick;
689
+ }
690
+ const auth = (await readCredentials(cwd))?.kind === "api_key" ? "api_key" : "oauth";
691
+ if (client) {
692
+ const plan = planFor(client, auth, serverUrl);
693
+ if (plan.file) {
694
+ const path = join2(cwd, plan.file);
695
+ const existingText = existsSync2(path) ? await readFile2(path, "utf8") : null;
696
+ try {
697
+ if (client !== "codex" && !isJsonClient(client)) throw new Error(`No file writer for ${client}.`);
698
+ const next = client === "codex" ? mergeCodexToml(existingText, auth, serverUrl) : mergeJsonConfig(client, existingText, auth, serverUrl);
699
+ await mkdir2(dirname(path), { recursive: true });
700
+ await writeFile2(path, next, "utf8");
701
+ p2.log.success(`${existingText === null ? "Wrote" : "Updated"} ${plan.file} (other servers kept).`);
702
+ } catch (error) {
703
+ if (!(error instanceof ConfigMergeError)) throw error;
704
+ p2.log.warn(error.message);
705
+ }
706
+ }
707
+ p2.note(plan.nextSteps.map((s) => `- ${s}`).join("\n"), `Next, in ${CLIENT_LABELS[client]}`);
708
+ }
709
+ await writeSkillFile(cwd, generateSkillFile(config, client ? { client, auth } : void 0));
382
710
  p2.outro(`Done -- this project is set up for ${config.brandName}.`);
383
711
  }
384
712
 
385
713
  // src/commands/export.ts
386
- import { mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
387
- import { dirname, relative, resolve } from "path";
714
+ import { mkdir as mkdir3, readFile as readFile3, writeFile as writeFile3 } from "fs/promises";
715
+ import { dirname as dirname2, relative, resolve } from "path";
388
716
  import * as p3 from "@clack/prompts";
389
717
 
390
718
  // src/api.ts
@@ -400,7 +728,7 @@ async function call(serverUrl, token, path) {
400
728
  let response;
401
729
  try {
402
730
  response = await fetch(new URL(path, serverUrl), {
403
- headers: { Authorization: `Bearer ${token}`, "User-Agent": "stoaken-cli/0.3.0" }
731
+ headers: { Authorization: `Bearer ${token}`, "User-Agent": "stoaken-cli/0.5.0" }
404
732
  });
405
733
  } catch (error) {
406
734
  throw new Error(`Couldn't reach ${serverUrl}: ${error instanceof Error ? error.message : String(error)}`);
@@ -562,6 +890,11 @@ function problemMessage(problem, context) {
562
890
  return `Stoaken answered ${problem.status}${problem.detail ? `: ${problem.detail}` : problem.title ? `: ${problem.title}` : "."}`;
563
891
  }
564
892
  }
893
+ function isUpToDate(onDisk, live) {
894
+ if (onDisk === null) return false;
895
+ const lf = (s) => s.replace(/\r\n/g, "\n");
896
+ return lf(onDisk) === lf(live);
897
+ }
565
898
  function formatBytes(n) {
566
899
  return n < 1024 ? `${n} B` : `${(n / 1024).toFixed(1)} KB`;
567
900
  }
@@ -659,17 +992,17 @@ async function exportOne(serverUrl, token, target, theme, check, cwd) {
659
992
  if (check) {
660
993
  let current = null;
661
994
  try {
662
- current = await readFile2(path, "utf8");
995
+ current = await readFile3(path, "utf8");
663
996
  } catch {
664
997
  }
665
- if (current !== file.text) {
998
+ if (!isUpToDate(current, file.text)) {
666
999
  throw new ExportUsageError(`${shown} is out of date with ${label}${current === null ? " (it doesn't exist)" : ""}. Run "stoaken export" and commit the result.`);
667
1000
  }
668
1001
  say(`\u2714 ${shown} is up to date with ${label}`);
669
1002
  return;
670
1003
  }
671
- await mkdir2(dirname(path), { recursive: true });
672
- await writeFile2(path, file.text, "utf8");
1004
+ await mkdir3(dirname2(path), { recursive: true });
1005
+ await writeFile3(path, file.text, "utf8");
673
1006
  say(`\u2714 ${label} \u2192 ${shown} (${target.format}, ${formatBytes(Buffer.byteLength(file.text))})${warnings}`);
674
1007
  }
675
1008
  function describe(error, themeName, usingEnvToken) {
@@ -684,9 +1017,16 @@ function flagValue(args, name) {
684
1017
  return index === -1 ? void 0 : args[index + 1];
685
1018
  }
686
1019
  switch (command) {
687
- case "init":
688
- await runInit({ server: flagValue(rest, "server") });
1020
+ case "init": {
1021
+ const client = flagValue(rest, "client");
1022
+ if (client !== void 0 && !isClient(client)) {
1023
+ p4.log.error(`Unknown --client "${client}". Use one of: ${CLIENTS.join(", ")}`);
1024
+ process.exitCode = 1;
1025
+ break;
1026
+ }
1027
+ await runInit({ server: flagValue(rest, "server"), client });
689
1028
  break;
1029
+ }
690
1030
  case "export":
691
1031
  process.exitCode = await runExport(rest);
692
1032
  break;
@@ -698,7 +1038,9 @@ switch (command) {
698
1038
  "stoaken -- Stoaken CLI",
699
1039
  "",
700
1040
  "Usage:",
701
- " stoaken init [--server <url>] Authenticate, pick a Brand, and set up this project",
1041
+ " stoaken init [--server <url>] [--client <name>]",
1042
+ " Authenticate, pick a Brand, and connect an AI tool",
1043
+ ` (${CLIENTS.join(", ")})`,
702
1044
  " stoaken export [options] Write your tokens as code (stoaken export --help)"
703
1045
  ].join("\n")
704
1046
  );
package/package.json CHANGED
@@ -1,51 +1,51 @@
1
- {
2
- "name": "stoaken",
3
- "version": "0.3.0",
4
- "description": "Stoaken CLI -- set up a project for MCP/API access, and export your design tokens as code.",
5
- "license": "UNLICENSED",
6
- "type": "module",
7
- "bin": {
8
- "stoaken": "dist/index.js"
9
- },
10
- "files": [
11
- "dist"
12
- ],
13
- "repository": {
14
- "type": "git",
15
- "url": "git+https://github.com/craig-astravale/stoaken.git",
16
- "directory": "cli"
17
- },
18
- "bugs": {
19
- "url": "https://github.com/craig-astravale/stoaken/issues"
20
- },
21
- "homepage": "https://github.com/craig-astravale/stoaken/tree/main/cli#readme",
22
- "keywords": [
23
- "design-tokens",
24
- "dtcg",
25
- "mcp",
26
- "model-context-protocol",
27
- "cli",
28
- "design-system"
29
- ],
30
- "author": "Stoaken",
31
- "engines": {
32
- "node": ">=18"
33
- },
34
- "scripts": {
35
- "build": "tsup src/index.ts --format esm --clean --shims",
36
- "dev": "tsx src/index.ts",
37
- "typecheck": "tsc --noEmit",
38
- "prepublishOnly": "npm run build"
39
- },
40
- "dependencies": {
41
- "@clack/prompts": "^1.8.0",
42
- "@modelcontextprotocol/sdk": "^1.30.0",
43
- "open": "^11.0.2"
44
- },
45
- "devDependencies": {
46
- "@types/node": "^26.5.0",
47
- "tsup": "^8.5.1",
48
- "tsx": "^4.23.13",
49
- "typescript": "^7.0.2"
50
- }
51
- }
1
+ {
2
+ "name": "stoaken",
3
+ "version": "0.5.0",
4
+ "description": "Stoaken CLI -- set up a project for MCP/API access, and export your design tokens as code.",
5
+ "license": "UNLICENSED",
6
+ "type": "module",
7
+ "bin": {
8
+ "stoaken": "dist/index.js"
9
+ },
10
+ "files": [
11
+ "dist"
12
+ ],
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "git+https://github.com/craig-astravale/stoaken.git",
16
+ "directory": "cli"
17
+ },
18
+ "bugs": {
19
+ "url": "https://github.com/craig-astravale/stoaken/issues"
20
+ },
21
+ "homepage": "https://github.com/craig-astravale/stoaken/tree/main/cli#readme",
22
+ "keywords": [
23
+ "design-tokens",
24
+ "dtcg",
25
+ "mcp",
26
+ "model-context-protocol",
27
+ "cli",
28
+ "design-system"
29
+ ],
30
+ "author": "Stoaken",
31
+ "engines": {
32
+ "node": ">=20"
33
+ },
34
+ "scripts": {
35
+ "build": "tsup src/index.ts --format esm --clean --shims",
36
+ "dev": "tsx src/index.ts",
37
+ "typecheck": "tsc --noEmit",
38
+ "prepublishOnly": "npm run build"
39
+ },
40
+ "dependencies": {
41
+ "@clack/prompts": "^1.8.0",
42
+ "open": "^11.0.2",
43
+ "@modelcontextprotocol/client": "^2.4.0"
44
+ },
45
+ "devDependencies": {
46
+ "@types/node": "^26.5.0",
47
+ "tsup": "^8.5.1",
48
+ "tsx": "^4.23.13",
49
+ "typescript": "^7.0.2"
50
+ }
51
+ }