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.
- package/README.md +11 -4
- package/dist/index.js +186 -59
- 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 --
|
|
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
|
|
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
|
-
- **
|
|
63
|
-
-
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
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
|
|
66
|
-
|
|
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/
|
|
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
|
|
100
|
-
if (!
|
|
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
|
-
|
|
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
|
-
|
|
106
|
-
|
|
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
|
|
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
|
-
|
|
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>
|
|
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
|
-
|
|
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",
|
|
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
|
|
159
|
-
const response = await fetch(
|
|
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
|
-
|
|
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
|
-
|
|
174
|
-
return data.access_token;
|
|
222
|
+
return await response.json();
|
|
175
223
|
}
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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 (
|
|
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.
|
|
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: "
|
|
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
|
|
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: "
|
|
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
|
-
|
|
621
|
-
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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.
|
|
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
|
|
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
|
-
"
|
|
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.
|
|
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
|
-
"
|
|
43
|
-
"
|
|
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": "^
|
|
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
|
}
|