stoaken 0.5.0 → 0.5.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.
Files changed (3) hide show
  1. package/README.md +11 -4
  2. package/dist/index.js +186 -59
  3. package/package.json +7 -4
package/README.md CHANGED
@@ -10,13 +10,13 @@ npx stoaken init
10
10
 
11
11
  Run it in the root of the project you want connected to a Stoaken Brand. It will:
12
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`.
13
+ 1. Ask how you'd like to sign in -- in your browser (OAuth 2.1), or by pasting an API key you've already created in Settings › API keys.
14
14
  2. List the real Brands your account can see, and let you pick one.
15
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
16
  4. Ask which AI tool to connect (or take `--client`) and write that tool's MCP config.
17
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
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.
19
+ Run `stoaken init` again in an already-configured directory to continue with the same Brand, switch to a different one, or sign in again.
20
20
 
21
21
  ## Connect an AI tool
22
22
 
@@ -59,8 +59,15 @@ left alone and you're told. No secret goes into these files, so they are safe to
59
59
 
60
60
  Two paths, both landing on the same bearer credential:
61
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.
62
+ - **Sign 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) and opens your browser. You sign in as you do on the web, then a consent screen asks whether **Stoaken CLI** may use your account: check the name, then allow it. The terminal then says `Signed in as you@example.com.`
63
+ - From 0.5.1 the CLI identifies itself with a Client ID Metadata Document (`https://www.stoaken.com/oauth/cli-client.json`), so nothing is registered; if the server doesn't support that, it registers itself once (Dynamic Client Registration) and reuses that registration. Its redirect is `http://127.0.0.1:51820/callback`.
64
+ - It asks for a token for `https://www.stoaken.com/mcp` with `offline_access`, and saves the refresh token (with the server it came from) in `.stoaken/credentials.json`, `chmod 600`. `stoaken export` renews an expired sign-in itself; when that's no longer possible it says "Your Stoaken sign-in ended." Run `stoaken init` to sign in again.
65
+ - Browser sign-in has to be switched on for Stoaken's sign-in service. Until it is, use an API key.
66
+ - **Paste an API key** -- for CI, scripts, or anywhere a browser isn't available. Create one in Settings › API keys in the Stoaken app.
67
+
68
+ ## Changes
69
+
70
+ - **0.5.1** -- Browser sign-in uses a Client ID Metadata Document (Dynamic Client Registration as the fallback), asks for the /mcp resource and a refresh token, and renews itself. "Sign in" wording throughout.
64
71
 
65
72
  ## Export your tokens as code
66
73
 
package/dist/index.js CHANGED
@@ -14,7 +14,7 @@ import open from "open";
14
14
 
15
15
  // src/config.ts
16
16
  import { existsSync } from "fs";
17
- import { appendFile, mkdir, readFile, writeFile } from "fs/promises";
17
+ import { appendFile, chmod, mkdir, readFile, rename, writeFile } from "fs/promises";
18
18
  import { join } from "path";
19
19
  var CONFIG_FILE = "stoaken.config.json";
20
20
  var STOAKEN_DIR = ".stoaken";
@@ -40,9 +40,14 @@ async function ensureStoakenDir(cwd) {
40
40
  }
41
41
  async function writeCredentials(cwd, credentials) {
42
42
  await ensureStoakenDir(cwd);
43
- const path = join(cwd, CREDENTIALS_FILE);
44
- await writeFile(path, `${JSON.stringify(credentials, null, 2)}
45
- `, { encoding: "utf8", mode: 384 });
43
+ await writePrivate(join(cwd, CREDENTIALS_FILE), `${JSON.stringify(credentials, null, 2)}
44
+ `);
45
+ }
46
+ async function writePrivate(path, text) {
47
+ const tmp = `${path}.${process.pid}.tmp`;
48
+ await writeFile(tmp, text, { encoding: "utf8", mode: 384, flag: "wx" });
49
+ await chmod(tmp, 384);
50
+ await rename(tmp, path);
46
51
  }
47
52
  async function readCredentials(cwd) {
48
53
  try {
@@ -52,18 +57,23 @@ async function readCredentials(cwd) {
52
57
  return null;
53
58
  }
54
59
  }
55
- async function readOAuthClient(cwd) {
60
+ async function readOAuthClient(cwd, authServer) {
56
61
  try {
57
62
  const raw = await readFile(join(cwd, OAUTH_CLIENT_FILE), "utf8");
58
- return JSON.parse(raw);
63
+ const client = JSON.parse(raw);
64
+ return client.clientId && sameServer(client.authServer, authServer) ? client : null;
59
65
  } catch {
60
66
  return null;
61
67
  }
62
68
  }
63
69
  async function writeOAuthClient(cwd, client) {
64
70
  await ensureStoakenDir(cwd);
65
- await writeFile(join(cwd, OAUTH_CLIENT_FILE), `${JSON.stringify(client, null, 2)}
66
- `, "utf8");
71
+ await writePrivate(join(cwd, OAUTH_CLIENT_FILE), `${JSON.stringify(client, null, 2)}
72
+ `);
73
+ }
74
+ function sameServer(a, b) {
75
+ const norm = (u) => (u ?? "").trim().replace(/\/+$/, "").toLowerCase();
76
+ return Boolean(norm(a)) && norm(a) === norm(b);
67
77
  }
68
78
  async function writeSkillFile(cwd, content) {
69
79
  await ensureStoakenDir(cwd);
@@ -86,9 +96,18 @@ async function ensureGitignored(cwd) {
86
96
  `, "utf8");
87
97
  }
88
98
 
89
- // src/auth.ts
99
+ // src/oauth-client.ts
90
100
  var CALLBACK_PORT = 51820;
91
101
  var REDIRECT_URI = `http://127.0.0.1:${CALLBACK_PORT}/callback`;
102
+ var CLIENT_METADATA_PATH = "/oauth/cli-client.json";
103
+ function cimdClientId(origin) {
104
+ return `${new URL(origin).origin}${CLIENT_METADATA_PATH}`;
105
+ }
106
+ var OAUTH_SCOPE = "openid offline_access";
107
+
108
+ // src/auth.ts
109
+ var SIGN_IN_ENDED = 'Your Stoaken sign-in ended. Run "stoaken init" to sign in again, or set STOAKEN_TOKEN to an API key.';
110
+ var CALLBACK_TIMEOUT_MS = 5 * 60 * 1e3;
92
111
  async function discoverAuthorizationServer(serverUrl) {
93
112
  const metadataUrl = new URL("/.well-known/oauth-protected-resource", serverUrl);
94
113
  const response = await fetch(metadataUrl);
@@ -96,26 +115,44 @@ async function discoverAuthorizationServer(serverUrl) {
96
115
  throw new Error(`Failed to discover the authorization server (${response.status}) at ${metadataUrl}`);
97
116
  }
98
117
  const metadata = await response.json();
99
- const authServer = metadata.authorization_servers?.[0];
100
- if (!authServer) {
118
+ const issuer = metadata.authorization_servers?.[0]?.replace(/\/+$/, "");
119
+ if (!issuer) {
101
120
  throw new Error(`${metadataUrl} did not name an authorization server`);
102
121
  }
103
- return authServer;
122
+ const as = await authServerMetadata(issuer);
123
+ return {
124
+ issuer,
125
+ resource: metadata.resource || new URL("/mcp", serverUrl).toString(),
126
+ authorize: as.authorization_endpoint ?? `${issuer}/oauth2/authorize`,
127
+ token: as.token_endpoint ?? `${issuer}/oauth2/token`,
128
+ register: as.registration_endpoint ?? `${issuer}/oauth2/register`,
129
+ cimd: as.client_id_metadata_document_supported
130
+ };
131
+ }
132
+ async function authServerMetadata(issuer) {
133
+ try {
134
+ const response = await fetch(`${issuer}/.well-known/oauth-authorization-server`);
135
+ return response.ok ? await response.json() : {};
136
+ } catch {
137
+ return {};
138
+ }
104
139
  }
105
- async function registerOAuthClient(authServer) {
106
- const response = await fetch(`${authServer}/oauth2/register`, {
140
+ var RegistrationRefused = class extends Error {
141
+ };
142
+ async function registerOAuthClient(server) {
143
+ const response = await fetch(server.register, {
107
144
  method: "POST",
108
145
  headers: { "Content-Type": "application/json" },
109
146
  body: JSON.stringify({
110
147
  client_name: "Stoaken CLI",
111
148
  redirect_uris: [REDIRECT_URI],
112
- grant_types: ["authorization_code"],
149
+ grant_types: ["authorization_code", "refresh_token"],
113
150
  response_types: ["code"],
114
151
  token_endpoint_auth_method: "none"
115
152
  })
116
153
  });
117
154
  if (!response.ok) {
118
- throw new Error(`Dynamic Client Registration failed (${response.status}): ${await response.text()}`);
155
+ throw new RegistrationRefused(`Dynamic Client Registration failed (${response.status}): ${await response.text()}`);
119
156
  }
120
157
  const data = await response.json();
121
158
  return data.client_id;
@@ -128,8 +165,15 @@ function generatePkce() {
128
165
  const challenge = base64url(createHash("sha256").update(verifier).digest());
129
166
  return { verifier, challenge };
130
167
  }
131
- function waitForCallback() {
168
+ var ClientRefused = class extends Error {
169
+ };
170
+ var CLIENT_ERRORS = /* @__PURE__ */ new Set(["invalid_client", "unauthorized_client"]);
171
+ function waitForCallback(state) {
132
172
  return new Promise((resolve2, reject) => {
173
+ const timer = setTimeout(() => {
174
+ server.close();
175
+ reject(new Error('Sign-in timed out. Run "stoaken init" to try again.'));
176
+ }, CALLBACK_TIMEOUT_MS);
133
177
  const server = createServer((req, res) => {
134
178
  const url = new URL(req.url ?? "/", REDIRECT_URI);
135
179
  if (url.pathname !== "/callback") {
@@ -138,64 +182,148 @@ function waitForCallback() {
138
182
  }
139
183
  const code = url.searchParams.get("code");
140
184
  const error = url.searchParams.get("error");
185
+ if (url.searchParams.get("state") !== state) {
186
+ res.writeHead(400, { "Content-Type": "text/html" }).end("<p>That isn't the answer to this sign-in.</p>");
187
+ return;
188
+ }
141
189
  res.writeHead(200, { "Content-Type": "text/html" });
142
190
  res.end(
143
- error ? "<p>Login failed. You can close this tab and return to the terminal.</p>" : "<p>Login complete -- you can close this tab and return to the terminal.</p>"
191
+ error || !code ? "<p>Sign-in failed. You can close this tab and return to the terminal.</p>" : "<p>Signed in. You can close this tab and return to the terminal.</p>"
144
192
  );
193
+ clearTimeout(timer);
145
194
  server.close();
146
195
  if (error) {
147
- reject(new Error(`WorkOS returned an error: ${error}`));
196
+ const why = `WorkOS returned an error: ${error}`;
197
+ reject(CLIENT_ERRORS.has(error) ? new ClientRefused(why) : new Error(why));
148
198
  } else if (!code) {
149
199
  reject(new Error("No authorization code in the callback"));
150
200
  } else {
151
201
  resolve2(code);
152
202
  }
153
203
  });
154
- server.on("error", reject);
204
+ server.on("error", (err) => {
205
+ clearTimeout(timer);
206
+ reject(err);
207
+ });
155
208
  server.listen(CALLBACK_PORT, "127.0.0.1");
156
209
  });
157
210
  }
158
- async function exchangeCodeForToken(authServer, clientId, code, verifier) {
159
- const response = await fetch(`${authServer}/oauth2/token`, {
211
+ async function tokenRequest(server, params) {
212
+ const response = await fetch(server.token, {
160
213
  method: "POST",
161
214
  headers: { "Content-Type": "application/x-www-form-urlencoded" },
162
- body: new URLSearchParams({
163
- grant_type: "authorization_code",
164
- code,
165
- redirect_uri: REDIRECT_URI,
166
- client_id: clientId,
167
- code_verifier: verifier
168
- })
215
+ body: new URLSearchParams({ ...params, resource: server.resource })
169
216
  });
170
217
  if (!response.ok) {
171
- throw new Error(`Token exchange failed (${response.status}): ${await response.text()}`);
218
+ const text = await response.text();
219
+ const why = `Token exchange failed (${response.status}): ${text}`;
220
+ throw CLIENT_ERRORS.has(errorCode(text)) ? new ClientRefused(why) : new Error(why);
172
221
  }
173
- const data = await response.json();
174
- return data.access_token;
222
+ return await response.json();
175
223
  }
176
- async function loginWithBrowser(cwd, serverUrl) {
177
- const authServer = await discoverAuthorizationServer(serverUrl);
178
- let clientId = (await readOAuthClient(cwd))?.clientId;
179
- if (!clientId) {
180
- clientId = await registerOAuthClient(authServer);
181
- await writeOAuthClient(cwd, { clientId });
224
+ function errorCode(text) {
225
+ try {
226
+ return String(JSON.parse(text).error ?? "");
227
+ } catch {
228
+ return "";
182
229
  }
230
+ }
231
+ function credentialsFrom(data, server, clientId, serverUrl, now = Date.now()) {
232
+ return {
233
+ kind: "oauth",
234
+ token: data.access_token,
235
+ ...data.refresh_token ? { refreshToken: data.refresh_token } : {},
236
+ ...typeof data.expires_in === "number" ? { expiresAt: now + data.expires_in * 1e3 } : {},
237
+ authServer: server.issuer,
238
+ clientId,
239
+ serverUrl
240
+ };
241
+ }
242
+ function tokenEmail(token) {
243
+ try {
244
+ const payload = JSON.parse(Buffer.from(token.split(".")[1] ?? "", "base64url").toString("utf8"));
245
+ return typeof payload.email === "string" ? payload.email : null;
246
+ } catch {
247
+ return null;
248
+ }
249
+ }
250
+ async function authorizeWith(server, clientId) {
183
251
  const { verifier, challenge } = generatePkce();
184
- const authorizeUrl = new URL("/oauth2/authorize", authServer);
252
+ const state = base64url(randomBytes(16));
253
+ const authorizeUrl = new URL(server.authorize);
185
254
  authorizeUrl.searchParams.set("client_id", clientId);
186
255
  authorizeUrl.searchParams.set("redirect_uri", REDIRECT_URI);
187
256
  authorizeUrl.searchParams.set("response_type", "code");
188
257
  authorizeUrl.searchParams.set("code_challenge", challenge);
189
258
  authorizeUrl.searchParams.set("code_challenge_method", "S256");
190
- const callbackPromise = waitForCallback();
259
+ authorizeUrl.searchParams.set("state", state);
260
+ authorizeUrl.searchParams.set("scope", OAUTH_SCOPE);
261
+ authorizeUrl.searchParams.set("resource", server.resource);
262
+ const callbackPromise = waitForCallback(state);
191
263
  await open(authorizeUrl.toString());
192
- p.log.info(`Opened your browser to sign in. Waiting for you to finish...`);
193
- const code = await callbackPromise;
194
- return exchangeCodeForToken(authServer, clientId, code, verifier);
264
+ return { code: await callbackPromise, verifier };
265
+ }
266
+ async function dcrClient(cwd, server) {
267
+ const cached = await readOAuthClient(cwd, server.issuer);
268
+ if (cached) return cached.clientId;
269
+ let clientId;
270
+ try {
271
+ clientId = await registerOAuthClient(server);
272
+ } catch (error) {
273
+ if (error instanceof RegistrationRefused) {
274
+ throw new Error("Browser sign-in isn't switched on for this Stoaken server yet. Paste an API key instead (Settings \u203A API keys).");
275
+ }
276
+ throw error;
277
+ }
278
+ await writeOAuthClient(cwd, { clientId, authServer: server.issuer });
279
+ return clientId;
280
+ }
281
+ async function loginWithBrowser(cwd, serverUrl) {
282
+ const server = await discoverAuthorizationServer(serverUrl);
283
+ const signIn = async (clientId) => {
284
+ const { code, verifier } = await authorizeWith(server, clientId);
285
+ const data = await tokenRequest(server, { grant_type: "authorization_code", code, redirect_uri: REDIRECT_URI, client_id: clientId, code_verifier: verifier });
286
+ return credentialsFrom(data, server, clientId, serverUrl);
287
+ };
288
+ let credentials;
289
+ if (server.cimd) {
290
+ try {
291
+ credentials = await signIn(cimdClientId(new URL(server.resource).origin));
292
+ } catch (error) {
293
+ if (!(error instanceof ClientRefused)) throw error;
294
+ p.log.info("Stoaken's sign-in didn't accept the CLI's published client. Trying again with a registered one...");
295
+ credentials = await signIn(await dcrClient(cwd, server));
296
+ }
297
+ } else {
298
+ credentials = await signIn(await dcrClient(cwd, server));
299
+ }
300
+ return { credentials, email: tokenEmail(credentials.token) };
301
+ }
302
+ function needsRefresh(credentials, now = Date.now()) {
303
+ return credentials.kind === "oauth" && typeof credentials.expiresAt === "number" && now >= credentials.expiresAt - 6e4;
304
+ }
305
+ async function freshCredentials(cwd, serverUrl) {
306
+ const saved = await readCredentials(cwd);
307
+ if (!saved || saved.kind !== "oauth") return saved;
308
+ if (saved.serverUrl && !sameServer(saved.serverUrl, serverUrl)) {
309
+ throw new Error(`Your saved sign-in is for ${saved.serverUrl}, not ${serverUrl}. Run "stoaken init" to sign in again, or set STOAKEN_TOKEN to an API key.`);
310
+ }
311
+ if (!needsRefresh(saved)) return saved;
312
+ if (!saved.refreshToken || !saved.authServer || !saved.clientId) throw new Error(SIGN_IN_ENDED);
313
+ try {
314
+ const server = await discoverAuthorizationServer(saved.serverUrl ?? serverUrl);
315
+ if (!sameServer(server.issuer, saved.authServer)) throw new Error("authorization server changed");
316
+ const data = await tokenRequest(server, { grant_type: "refresh_token", refresh_token: saved.refreshToken, client_id: saved.clientId });
317
+ const next = credentialsFrom({ refresh_token: saved.refreshToken, ...data }, server, saved.clientId, saved.serverUrl ?? serverUrl);
318
+ await writeCredentials(cwd, next);
319
+ return next;
320
+ } catch {
321
+ throw new Error(SIGN_IN_ENDED);
322
+ }
195
323
  }
196
324
  async function promptForApiKey() {
197
325
  const key = await p.password({
198
- message: "Paste your Stoaken API key (from /settings/api-keys):",
326
+ message: "Paste your Stoaken API key (Settings \u203A API keys):",
199
327
  validate: (value) => {
200
328
  if (!value?.startsWith("stk_") && !value?.startsWith("twk_")) {
201
329
  return "That doesn't look like a Stoaken API key (should start with stk_)";
@@ -214,7 +342,7 @@ async function listBrands(serverUrl, credential) {
214
342
  const transport = new StreamableHTTPClientTransport(new URL("/mcp", serverUrl), {
215
343
  requestInit: { headers: { Authorization: `Bearer ${credential}` } }
216
344
  });
217
- const client = new Client({ name: "stoaken-cli", version: "0.5.0" });
345
+ const client = new Client({ name: "stoaken-cli", version: "0.5.1" });
218
346
  await client.connect(transport);
219
347
  try {
220
348
  const result = await client.callTool({ name: "list_brands", arguments: {} });
@@ -588,7 +716,7 @@ async function runInit(options) {
588
716
  options: [
589
717
  { value: "continue", label: `Continue with ${existing.brandName}` },
590
718
  { value: "switch", label: "Switch Brand" },
591
- { value: "reauth", label: "Re-authenticate" }
719
+ { value: "reauth", label: "Sign in again" }
592
720
  ]
593
721
  });
594
722
  if (p2.isCancel(choice)) {
@@ -599,13 +727,12 @@ async function runInit(options) {
599
727
  skipToWrite = { brandId: existing.brandId, brandName: existing.brandName };
600
728
  }
601
729
  }
602
- let credential;
603
- let credentialKind;
730
+ let saved;
604
731
  if (!skipToWrite) {
605
732
  const authChoice = await p2.select({
606
733
  message: "How would you like to sign in?",
607
734
  options: [
608
- { value: "browser", label: "Log in with browser" },
735
+ { value: "browser", label: "Sign in with browser" },
609
736
  { value: "key", label: "Paste an API key" }
610
737
  ]
611
738
  });
@@ -614,12 +741,13 @@ async function runInit(options) {
614
741
  return;
615
742
  }
616
743
  if (authChoice === "browser") {
744
+ p2.log.info("Opening your browser to sign in to Stoaken. It will ask whether Stoaken CLI may use your account. Check the name, then allow it.");
617
745
  const s2 = p2.spinner();
618
746
  s2.start("Waiting for you to finish signing in...");
619
747
  try {
620
- credential = await loginWithBrowser(cwd, serverUrl);
621
- credentialKind = "oauth";
622
- s2.stop("Signed in.");
748
+ const { credentials, email } = await loginWithBrowser(cwd, serverUrl);
749
+ saved = credentials;
750
+ s2.stop(email ? `Signed in as ${email}.` : "Signed in.");
623
751
  } catch (error) {
624
752
  s2.stop("Sign-in failed.");
625
753
  p2.log.error(error instanceof Error ? error.message : String(error));
@@ -631,14 +759,13 @@ async function runInit(options) {
631
759
  p2.cancel("Cancelled.");
632
760
  return;
633
761
  }
634
- credential = key;
635
- credentialKind = "api_key";
762
+ saved = { kind: "api_key", token: key };
636
763
  }
637
764
  const s = p2.spinner();
638
765
  s.start("Fetching your Brands...");
639
766
  let brands;
640
767
  try {
641
- brands = await listBrands(serverUrl, credential);
768
+ brands = await listBrands(serverUrl, saved.token);
642
769
  } catch (error) {
643
770
  s.stop("Failed to reach Stoaken.");
644
771
  p2.log.error(error instanceof Error ? error.message : String(error));
@@ -658,7 +785,7 @@ async function runInit(options) {
658
785
  return;
659
786
  }
660
787
  const brand = brands.find((b) => b.id === brandChoice);
661
- await writeCredentials(cwd, { kind: credentialKind, token: credential });
788
+ await writeCredentials(cwd, saved);
662
789
  skipToWrite = { brandId: brand.id, brandName: brand.name };
663
790
  }
664
791
  const keepExports = existing?.brandId === skipToWrite.brandId && existing.exports !== void 0;
@@ -728,7 +855,7 @@ async function call(serverUrl, token, path) {
728
855
  let response;
729
856
  try {
730
857
  response = await fetch(new URL(path, serverUrl), {
731
- headers: { Authorization: `Bearer ${token}`, "User-Agent": "stoaken-cli/0.5.0" }
858
+ headers: { Authorization: `Bearer ${token}`, "User-Agent": "stoaken-cli/0.5.1" }
732
859
  });
733
860
  } catch (error) {
734
861
  throw new Error(`Couldn't reach ${serverUrl}: ${error instanceof Error ? error.message : String(error)}`);
@@ -933,7 +1060,7 @@ async function runExport(argv) {
933
1060
  if (!config) throw new ExportUsageError('No stoaken.config.json here. Run "stoaken init" in your project first.');
934
1061
  const serverUrl = args.server ?? config.serverUrl;
935
1062
  const envToken = process.env.STOAKEN_TOKEN?.trim();
936
- const token = envToken || (await readCredentials(cwd))?.token;
1063
+ const token = envToken || (await freshCredentials(cwd, serverUrl))?.token;
937
1064
  if (!token) throw new ExportUsageError('No credentials. Run "stoaken init", or set STOAKEN_TOKEN to an API key.');
938
1065
  const targets = args.target ? [args.target] : readSavedTargets(config.exports);
939
1066
  if (targets.length === 0) targets.push({ format: "css" });
@@ -1039,7 +1166,7 @@ switch (command) {
1039
1166
  "",
1040
1167
  "Usage:",
1041
1168
  " stoaken init [--server <url>] [--client <name>]",
1042
- " Authenticate, pick a Brand, and connect an AI tool",
1169
+ " Sign in, pick a Brand, and connect an AI tool",
1043
1170
  ` (${CLIENTS.join(", ")})`,
1044
1171
  " stoaken export [options] Write your tokens as code (stoaken export --help)"
1045
1172
  ].join("\n")
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "stoaken",
3
- "version": "0.5.0",
3
+ "version": "0.5.1",
4
4
  "description": "Stoaken CLI -- set up a project for MCP/API access, and export your design tokens as code.",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",
@@ -39,13 +39,16 @@
39
39
  },
40
40
  "dependencies": {
41
41
  "@clack/prompts": "^1.8.0",
42
- "open": "^11.0.2",
43
- "@modelcontextprotocol/client": "^2.4.0"
42
+ "@modelcontextprotocol/client": "^2.4.0",
43
+ "open": "^11.0.2"
44
44
  },
45
45
  "devDependencies": {
46
46
  "@types/node": "^26.5.0",
47
- "tsup": "^8.5.1",
47
+ "tsup": "^9.0.0",
48
48
  "tsx": "^4.23.13",
49
49
  "typescript": "^7.0.2"
50
+ },
51
+ "overrides": {
52
+ "esbuild": "0.28.2"
50
53
  }
51
54
  }