stoaken 0.4.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 +29 -9
- package/dist/index.js +329 -64
- 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
|
|
|
@@ -32,22 +32,42 @@ npx stoaken init --client cursor
|
|
|
32
32
|
| `copilot` | `.vscode/mcp.json` (`servers.stoaken`, VS Code agent mode) |
|
|
33
33
|
| `claude-desktop` | Nothing: prints the steps to add a custom connector (Customize > Connectors) |
|
|
34
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 |
|
|
35
43
|
|
|
36
44
|
init merges: other servers in the file are kept and only the `stoaken` entry is replaced. If the
|
|
37
|
-
file isn't plain JSON (for example `.vscode/mcp.json` with comments) it is
|
|
38
|
-
No secret goes into these files, so they are safe to commit:
|
|
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:
|
|
39
47
|
|
|
40
48
|
- Signed in with the browser: the entry has just the URL, and the tool signs you in itself with
|
|
41
|
-
OAuth (Claude Code `/mcp`, Cursor Connect, `codex mcp login stoaken`, VS Code Start
|
|
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).
|
|
42
52
|
- Pasted an API key: the entry reads the `STOAKEN_API_KEY` environment variable (VS Code instead
|
|
43
|
-
asks for the key once and stores it securely).
|
|
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.
|
|
44
57
|
|
|
45
58
|
## Authentication
|
|
46
59
|
|
|
47
60
|
Two paths, both landing on the same bearer credential:
|
|
48
61
|
|
|
49
|
-
- **
|
|
50
|
-
-
|
|
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.
|
|
51
71
|
|
|
52
72
|
## Export your tokens as code
|
|
53
73
|
|
|
@@ -101,7 +121,7 @@ Bitbucket Pipelines, Azure Pipelines and Vercel builds: see /docs/webhooks#ci an
|
|
|
101
121
|
|
|
102
122
|
## What this connects to
|
|
103
123
|
|
|
104
|
-
Once set up, any MCP-capable client (Claude Code, Claude Desktop, Cursor, Codex, Copilot, v0) or a direct HTTP client can reach Stoaken's MCP server at `/mcp`. Its main tools (full list at /docs/mcp):
|
|
124
|
+
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):
|
|
105
125
|
|
|
106
126
|
- `list_brands` -- every Brand you hold at least Viewer on.
|
|
107
127
|
- `get_theme_tokens(themeId)` -- a Theme's cascade-merged token tree.
|
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
|
-
|
|
224
|
+
function errorCode(text) {
|
|
225
|
+
try {
|
|
226
|
+
return String(JSON.parse(text).error ?? "");
|
|
227
|
+
} catch {
|
|
228
|
+
return "";
|
|
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;
|
|
182
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.
|
|
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: {} });
|
|
@@ -229,7 +357,22 @@ async function listBrands(serverUrl, credential) {
|
|
|
229
357
|
}
|
|
230
358
|
|
|
231
359
|
// src/client-config.ts
|
|
232
|
-
var CLIENTS = [
|
|
360
|
+
var CLIENTS = [
|
|
361
|
+
"claude-code",
|
|
362
|
+
"claude-desktop",
|
|
363
|
+
"cursor",
|
|
364
|
+
"codex",
|
|
365
|
+
"copilot",
|
|
366
|
+
"v0",
|
|
367
|
+
"windsurf",
|
|
368
|
+
"zed",
|
|
369
|
+
"gemini-cli",
|
|
370
|
+
"jetbrains",
|
|
371
|
+
"cline",
|
|
372
|
+
"lovable",
|
|
373
|
+
"bolt",
|
|
374
|
+
"replit"
|
|
375
|
+
];
|
|
233
376
|
var SERVER_NAME = "stoaken";
|
|
234
377
|
var API_KEY_ENV = "STOAKEN_API_KEY";
|
|
235
378
|
var CLIENT_LABELS = {
|
|
@@ -238,17 +381,43 @@ var CLIENT_LABELS = {
|
|
|
238
381
|
cursor: "Cursor",
|
|
239
382
|
codex: "OpenAI Codex",
|
|
240
383
|
copilot: "GitHub Copilot (VS Code)",
|
|
241
|
-
v0: "v0 (Vercel)"
|
|
384
|
+
v0: "v0 (Vercel)",
|
|
385
|
+
windsurf: "Windsurf / Devin Desktop",
|
|
386
|
+
zed: "Zed",
|
|
387
|
+
"gemini-cli": "Gemini CLI",
|
|
388
|
+
jetbrains: "JetBrains (Junie)",
|
|
389
|
+
cline: "Cline",
|
|
390
|
+
lovable: "Lovable",
|
|
391
|
+
bolt: "Bolt (bolt.new)",
|
|
392
|
+
replit: "Replit Agent"
|
|
393
|
+
};
|
|
394
|
+
var JSON_CLIENTS = ["claude-code", "cursor", "copilot", "windsurf", "zed", "gemini-cli", "jetbrains"];
|
|
395
|
+
var SERVERS_KEY = {
|
|
396
|
+
"claude-code": "mcpServers",
|
|
397
|
+
cursor: "mcpServers",
|
|
398
|
+
copilot: "servers",
|
|
399
|
+
windsurf: "mcpServers",
|
|
400
|
+
zed: "context_servers",
|
|
401
|
+
"gemini-cli": "mcpServers",
|
|
402
|
+
jetbrains: "mcpServers"
|
|
242
403
|
};
|
|
243
404
|
function isClient(value) {
|
|
244
405
|
return CLIENTS.includes(value);
|
|
245
406
|
}
|
|
407
|
+
function isJsonClient(client) {
|
|
408
|
+
return JSON_CLIENTS.includes(client);
|
|
409
|
+
}
|
|
246
410
|
function mcpUrl(serverUrl) {
|
|
247
411
|
return `${serverUrl.replace(/\/+$/, "")}/mcp`;
|
|
248
412
|
}
|
|
413
|
+
function replitInstallLink(serverUrl) {
|
|
414
|
+
const json = JSON.stringify({ displayName: "Stoaken", baseUrl: mcpUrl(serverUrl) });
|
|
415
|
+
return `https://replit.com/integrations?mcp=${Buffer.from(json, "utf8").toString("base64")}`;
|
|
416
|
+
}
|
|
249
417
|
function planFor(client, auth, serverUrl) {
|
|
250
418
|
const url = mcpUrl(serverUrl);
|
|
251
419
|
const keyStep = `Set ${API_KEY_ENV} in your shell to an API key from Settings > API keys (never commit it).`;
|
|
420
|
+
const oauthInstead = "Or run init again, sign in with the browser, and use OAuth instead.";
|
|
252
421
|
switch (client) {
|
|
253
422
|
case "claude-code":
|
|
254
423
|
return {
|
|
@@ -268,7 +437,8 @@ function planFor(client, auth, serverUrl) {
|
|
|
268
437
|
case "copilot":
|
|
269
438
|
return {
|
|
270
439
|
file: ".vscode/mcp.json",
|
|
271
|
-
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."]
|
|
440
|
+
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."],
|
|
441
|
+
keyNote: "VS Code asks for the key once and stores it securely"
|
|
272
442
|
};
|
|
273
443
|
case "claude-desktop":
|
|
274
444
|
return {
|
|
@@ -290,10 +460,104 @@ function planFor(client, auth, serverUrl) {
|
|
|
290
460
|
"v0 uses the tools while it generates; the app it builds cannot call them."
|
|
291
461
|
]
|
|
292
462
|
};
|
|
463
|
+
case "windsurf":
|
|
464
|
+
return {
|
|
465
|
+
file: ".devin/mcp_config.json",
|
|
466
|
+
nextSteps: auth === "oauth" ? [
|
|
467
|
+
"Open this project in Devin Desktop (formerly Windsurf) with the Devin Local agent. Devin CLI reads the same file.",
|
|
468
|
+
"When stoaken shows Needs auth in the MCP list, click Authenticate (or run: devin mcp login stoaken)."
|
|
469
|
+
] : [
|
|
470
|
+
keyStep,
|
|
471
|
+
`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}"`,
|
|
472
|
+
"That override replaces the project entry for you only. The legacy Cascade agent ignores project files (see the guide)."
|
|
473
|
+
],
|
|
474
|
+
keyNote: "the key goes in the gitignored `.devin/mcp_config.local.json` override"
|
|
475
|
+
};
|
|
476
|
+
case "zed":
|
|
477
|
+
return auth === "oauth" ? {
|
|
478
|
+
file: ".zed/settings.json",
|
|
479
|
+
nextSteps: [
|
|
480
|
+
"Trust this project in Zed: in Restricted Mode project settings and their MCP servers don't load.",
|
|
481
|
+
"Zed asks you to sign in to stoaken (MCP OAuth). Settings > AI > MCP Servers shows a green dot when it's active."
|
|
482
|
+
]
|
|
483
|
+
} : {
|
|
484
|
+
file: null,
|
|
485
|
+
nextSteps: [
|
|
486
|
+
"Zed documents no variable expansion in headers, so the key can't go in a project file.",
|
|
487
|
+
`Run "zed: open settings file" and add under "context_servers": "stoaken": { "url": "${url}", "headers": { "Authorization": "Bearer <API key>" } }`,
|
|
488
|
+
oauthInstead
|
|
489
|
+
]
|
|
490
|
+
};
|
|
491
|
+
case "gemini-cli":
|
|
492
|
+
return {
|
|
493
|
+
file: ".gemini/settings.json",
|
|
494
|
+
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."]
|
|
495
|
+
};
|
|
496
|
+
case "jetbrains":
|
|
497
|
+
return auth === "oauth" ? {
|
|
498
|
+
file: ".junie/mcp/mcp.json",
|
|
499
|
+
nextSteps: [
|
|
500
|
+
"Junie in the IDE: Settings > Tools > Junie > MCP Settings lists stoaken and its status.",
|
|
501
|
+
"Junie CLI: run /mcp, select stoaken, choose Authorize and sign in in the browser."
|
|
502
|
+
]
|
|
503
|
+
} : {
|
|
504
|
+
file: null,
|
|
505
|
+
nextSteps: [
|
|
506
|
+
"Junie has no secret storage or variable expansion for MCP configs, so the key can't go in a project file.",
|
|
507
|
+
`In your user file ~/.junie/mcp/mcp.json, under "mcpServers", add "stoaken": { "url": "${url}", "headers": { "Authorization": "Bearer <API key>" } }`,
|
|
508
|
+
oauthInstead
|
|
509
|
+
]
|
|
510
|
+
};
|
|
511
|
+
case "cline":
|
|
512
|
+
return {
|
|
513
|
+
file: null,
|
|
514
|
+
nextSteps: [
|
|
515
|
+
"Cline keeps MCP servers in its own settings, not a project file.",
|
|
516
|
+
...auth === "oauth" ? [
|
|
517
|
+
`Cline panel > MCP Servers > Remote Servers: name stoaken, URL ${url}, transport Streamable HTTP, Add Server.`,
|
|
518
|
+
"If it asks you to authenticate, sign in in the browser (Cline CLI: cline mcp > Authorize OAuth)."
|
|
519
|
+
] : [
|
|
520
|
+
`Cline panel > MCP Servers > Configure > Configure MCP Servers, and add under "mcpServers": "stoaken": { "type": "streamableHttp", "url": "${url}", "headers": { "Authorization": "Bearer <API key>" } }`
|
|
521
|
+
]
|
|
522
|
+
]
|
|
523
|
+
};
|
|
524
|
+
case "lovable":
|
|
525
|
+
return {
|
|
526
|
+
file: null,
|
|
527
|
+
nextSteps: [
|
|
528
|
+
"Lovable adds MCP servers in its own UI, not through a project file.",
|
|
529
|
+
"Connectors > Add custom > MCP server. Server name: Stoaken.",
|
|
530
|
+
`Server URL: ${url}. Authentication: OAuth (Add & authorize), or Bearer token with an API key.`,
|
|
531
|
+
"The connection is yours alone and only for Lovable's chat; the app it publishes can't call it."
|
|
532
|
+
]
|
|
533
|
+
};
|
|
534
|
+
case "bolt":
|
|
535
|
+
return {
|
|
536
|
+
file: null,
|
|
537
|
+
nextSteps: [
|
|
538
|
+
"Bolt adds MCP servers in its own UI, not through a project file.",
|
|
539
|
+
"In the chatbox: + > Connectors > Manage connectors > Custom MCP server.",
|
|
540
|
+
`Name: Stoaken. URL: ${url}. Transport type: HTTP. Authentication: MCP OAuth.`,
|
|
541
|
+
"Then turn the connector on for the project (+ > Connectors)."
|
|
542
|
+
]
|
|
543
|
+
};
|
|
544
|
+
case "replit":
|
|
545
|
+
return {
|
|
546
|
+
file: null,
|
|
547
|
+
nextSteps: [
|
|
548
|
+
"Replit adds MCP servers in its own UI, not through a project file.",
|
|
549
|
+
`Open ${replitInstallLink(serverUrl)} (or Integrations > MCP Servers > + Add MCP server, URL ${url}).`,
|
|
550
|
+
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."
|
|
551
|
+
]
|
|
552
|
+
};
|
|
293
553
|
}
|
|
294
554
|
}
|
|
295
555
|
function jsonEntry(client, auth, serverUrl) {
|
|
296
556
|
const url = mcpUrl(serverUrl);
|
|
557
|
+
if (client === "windsurf" || client === "zed" || client === "jetbrains") return { url };
|
|
558
|
+
if (client === "gemini-cli") {
|
|
559
|
+
return auth === "oauth" ? { httpUrl: url } : { httpUrl: url, headers: { Authorization: `Bearer \${${API_KEY_ENV}}` } };
|
|
560
|
+
}
|
|
297
561
|
if (client === "claude-code") {
|
|
298
562
|
return auth === "oauth" ? { type: "http", url } : { type: "http", url, headers: { Authorization: `Bearer \${${API_KEY_ENV}}` } };
|
|
299
563
|
}
|
|
@@ -325,8 +589,9 @@ var COPILOT_INPUT = {
|
|
|
325
589
|
};
|
|
326
590
|
function mergeJsonConfig(client, existing, auth, serverUrl) {
|
|
327
591
|
const file = planFor(client, auth, serverUrl).file;
|
|
592
|
+
if (file === null) throw new ConfigMergeError(`${CLIENT_LABELS[client]} gets no project file with an API key; follow the steps instead.`);
|
|
328
593
|
const doc = parseObject(existing, file);
|
|
329
|
-
const key = client
|
|
594
|
+
const key = SERVERS_KEY[client];
|
|
330
595
|
const current = doc[key];
|
|
331
596
|
if (current !== void 0 && (typeof current !== "object" || current === null || Array.isArray(current))) {
|
|
332
597
|
throw new ConfigMergeError(`${file} has a "${key}" that isn't an object, so it was left alone.`);
|
|
@@ -377,7 +642,7 @@ file -- it's already in \`.gitignore\`).
|
|
|
377
642
|
}
|
|
378
643
|
const { client, auth } = target;
|
|
379
644
|
const plan = planFor(client, auth, serverUrl);
|
|
380
|
-
const how = auth === "oauth" ? "the client signs in with OAuth" : `it reads the \`${API_KEY_ENV}\` environment variable`;
|
|
645
|
+
const how = auth === "oauth" ? "the client signs in with OAuth" : plan.keyNote ?? `it reads the \`${API_KEY_ENV}\` environment variable`;
|
|
381
646
|
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.`;
|
|
382
647
|
return `${where}
|
|
383
648
|
|
|
@@ -451,7 +716,7 @@ async function runInit(options) {
|
|
|
451
716
|
options: [
|
|
452
717
|
{ value: "continue", label: `Continue with ${existing.brandName}` },
|
|
453
718
|
{ value: "switch", label: "Switch Brand" },
|
|
454
|
-
{ value: "reauth", label: "
|
|
719
|
+
{ value: "reauth", label: "Sign in again" }
|
|
455
720
|
]
|
|
456
721
|
});
|
|
457
722
|
if (p2.isCancel(choice)) {
|
|
@@ -462,13 +727,12 @@ async function runInit(options) {
|
|
|
462
727
|
skipToWrite = { brandId: existing.brandId, brandName: existing.brandName };
|
|
463
728
|
}
|
|
464
729
|
}
|
|
465
|
-
let
|
|
466
|
-
let credentialKind;
|
|
730
|
+
let saved;
|
|
467
731
|
if (!skipToWrite) {
|
|
468
732
|
const authChoice = await p2.select({
|
|
469
733
|
message: "How would you like to sign in?",
|
|
470
734
|
options: [
|
|
471
|
-
{ value: "browser", label: "
|
|
735
|
+
{ value: "browser", label: "Sign in with browser" },
|
|
472
736
|
{ value: "key", label: "Paste an API key" }
|
|
473
737
|
]
|
|
474
738
|
});
|
|
@@ -477,12 +741,13 @@ async function runInit(options) {
|
|
|
477
741
|
return;
|
|
478
742
|
}
|
|
479
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.");
|
|
480
745
|
const s2 = p2.spinner();
|
|
481
746
|
s2.start("Waiting for you to finish signing in...");
|
|
482
747
|
try {
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
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.");
|
|
486
751
|
} catch (error) {
|
|
487
752
|
s2.stop("Sign-in failed.");
|
|
488
753
|
p2.log.error(error instanceof Error ? error.message : String(error));
|
|
@@ -494,14 +759,13 @@ async function runInit(options) {
|
|
|
494
759
|
p2.cancel("Cancelled.");
|
|
495
760
|
return;
|
|
496
761
|
}
|
|
497
|
-
|
|
498
|
-
credentialKind = "api_key";
|
|
762
|
+
saved = { kind: "api_key", token: key };
|
|
499
763
|
}
|
|
500
764
|
const s = p2.spinner();
|
|
501
765
|
s.start("Fetching your Brands...");
|
|
502
766
|
let brands;
|
|
503
767
|
try {
|
|
504
|
-
brands = await listBrands(serverUrl,
|
|
768
|
+
brands = await listBrands(serverUrl, saved.token);
|
|
505
769
|
} catch (error) {
|
|
506
770
|
s.stop("Failed to reach Stoaken.");
|
|
507
771
|
p2.log.error(error instanceof Error ? error.message : String(error));
|
|
@@ -521,7 +785,7 @@ async function runInit(options) {
|
|
|
521
785
|
return;
|
|
522
786
|
}
|
|
523
787
|
const brand = brands.find((b) => b.id === brandChoice);
|
|
524
|
-
await writeCredentials(cwd,
|
|
788
|
+
await writeCredentials(cwd, saved);
|
|
525
789
|
skipToWrite = { brandId: brand.id, brandName: brand.name };
|
|
526
790
|
}
|
|
527
791
|
const keepExports = existing?.brandId === skipToWrite.brandId && existing.exports !== void 0;
|
|
@@ -557,6 +821,7 @@ async function runInit(options) {
|
|
|
557
821
|
const path = join2(cwd, plan.file);
|
|
558
822
|
const existingText = existsSync2(path) ? await readFile2(path, "utf8") : null;
|
|
559
823
|
try {
|
|
824
|
+
if (client !== "codex" && !isJsonClient(client)) throw new Error(`No file writer for ${client}.`);
|
|
560
825
|
const next = client === "codex" ? mergeCodexToml(existingText, auth, serverUrl) : mergeJsonConfig(client, existingText, auth, serverUrl);
|
|
561
826
|
await mkdir2(dirname(path), { recursive: true });
|
|
562
827
|
await writeFile2(path, next, "utf8");
|
|
@@ -590,7 +855,7 @@ async function call(serverUrl, token, path) {
|
|
|
590
855
|
let response;
|
|
591
856
|
try {
|
|
592
857
|
response = await fetch(new URL(path, serverUrl), {
|
|
593
|
-
headers: { Authorization: `Bearer ${token}`, "User-Agent": "stoaken-cli/0.
|
|
858
|
+
headers: { Authorization: `Bearer ${token}`, "User-Agent": "stoaken-cli/0.5.1" }
|
|
594
859
|
});
|
|
595
860
|
} catch (error) {
|
|
596
861
|
throw new Error(`Couldn't reach ${serverUrl}: ${error instanceof Error ? error.message : String(error)}`);
|
|
@@ -795,7 +1060,7 @@ async function runExport(argv) {
|
|
|
795
1060
|
if (!config) throw new ExportUsageError('No stoaken.config.json here. Run "stoaken init" in your project first.');
|
|
796
1061
|
const serverUrl = args.server ?? config.serverUrl;
|
|
797
1062
|
const envToken = process.env.STOAKEN_TOKEN?.trim();
|
|
798
|
-
const token = envToken || (await
|
|
1063
|
+
const token = envToken || (await freshCredentials(cwd, serverUrl))?.token;
|
|
799
1064
|
if (!token) throw new ExportUsageError('No credentials. Run "stoaken init", or set STOAKEN_TOKEN to an API key.');
|
|
800
1065
|
const targets = args.target ? [args.target] : readSavedTargets(config.exports);
|
|
801
1066
|
if (targets.length === 0) targets.push({ format: "css" });
|
|
@@ -901,7 +1166,7 @@ switch (command) {
|
|
|
901
1166
|
"",
|
|
902
1167
|
"Usage:",
|
|
903
1168
|
" stoaken init [--server <url>] [--client <name>]",
|
|
904
|
-
"
|
|
1169
|
+
" Sign in, pick a Brand, and connect an AI tool",
|
|
905
1170
|
` (${CLIENTS.join(", ")})`,
|
|
906
1171
|
" stoaken export [options] Write your tokens as code (stoaken export --help)"
|
|
907
1172
|
].join("\n")
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "stoaken",
|
|
3
|
-
"version": "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
|
-
"
|
|
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
|
}
|