@supatest/cli 0.0.41 → 0.0.42

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 (2) hide show
  1. package/dist/index.js +1209 -1277
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -313,7 +313,7 @@ Use these commands in interactive mode (type them and press Enter):
313
313
  - Choose between "Supatest Managed" (default) or "Claude Max"
314
314
  - Supatest Managed: Uses models through Supatest infrastructure
315
315
  - Claude Max: Uses your Claude subscription directly
316
- - Requires Claude Code login for Claude Max
316
+ - Authentication flow starts automatically when selecting Claude Max
317
317
  - Available on macOS, Linux, and Windows
318
318
 
319
319
  - **/mcp** - Show configured MCP servers
@@ -6054,591 +6054,9 @@ var init_shared_es = __esm({
6054
6054
  }
6055
6055
  });
6056
6056
 
6057
- // src/utils/claude-oauth.ts
6058
- var claude_oauth_exports = {};
6059
- __export(claude_oauth_exports, {
6060
- ClaudeOAuthService: () => ClaudeOAuthService
6061
- });
6062
- import { spawn } from "child_process";
6063
- import { createHash, randomBytes } from "crypto";
6064
- import http from "http";
6065
- import { platform } from "os";
6066
- var OAUTH_CONFIG, CALLBACK_PORT, CALLBACK_TIMEOUT_MS, ClaudeOAuthService;
6067
- var init_claude_oauth = __esm({
6068
- "src/utils/claude-oauth.ts"() {
6069
- "use strict";
6070
- OAUTH_CONFIG = {
6071
- clientId: "9d1c250a-e61b-44d9-88ed-5944d1962f5e",
6072
- // Claude Code's client ID
6073
- authorizationEndpoint: "https://claude.ai/oauth/authorize",
6074
- tokenEndpoint: "https://console.anthropic.com/v1/oauth/token",
6075
- redirectUri: "http://localhost:8421/callback",
6076
- // Local callback for CLI
6077
- scopes: ["user:inference", "user:profile", "org:create_api_key"]
6078
- };
6079
- CALLBACK_PORT = 8421;
6080
- CALLBACK_TIMEOUT_MS = 3e5;
6081
- ClaudeOAuthService = class _ClaudeOAuthService {
6082
- secretStorage;
6083
- static TOKEN_REFRESH_BUFFER_MS = 5 * 60 * 1e3;
6084
- // 5 minutes
6085
- pendingCodeVerifier = null;
6086
- // Store code verifier for PKCE
6087
- constructor(secretStorage) {
6088
- this.secretStorage = secretStorage;
6089
- }
6090
- /**
6091
- * Starts the OAuth authorization flow
6092
- * Opens the default browser for user authentication
6093
- * Returns after successful authentication
6094
- */
6095
- async authorize() {
6096
- try {
6097
- const state = this.generateRandomState();
6098
- const pkce = this.generatePKCEChallenge();
6099
- this.pendingCodeVerifier = pkce.codeVerifier;
6100
- const authUrl = this.buildAuthorizationUrl(state, pkce.codeChallenge);
6101
- console.log("\nAuthenticating with Claude...\n");
6102
- console.log(`Opening browser to: ${authUrl}
6103
- `);
6104
- const tokenPromise = this.startCallbackServer(CALLBACK_PORT, state);
6105
- try {
6106
- this.openBrowser(authUrl);
6107
- } catch (error) {
6108
- console.warn("Failed to open browser automatically:", error);
6109
- console.log(`
6110
- Please manually open this URL in your browser:
6111
- ${authUrl}
6112
- `);
6113
- }
6114
- await tokenPromise;
6115
- console.log("\n\u2705 Successfully authenticated with Claude!\n");
6116
- return { success: true };
6117
- } catch (error) {
6118
- this.pendingCodeVerifier = null;
6119
- return {
6120
- success: false,
6121
- error: error instanceof Error ? error.message : "Authentication failed"
6122
- };
6123
- }
6124
- }
6125
- /**
6126
- * Start local HTTP server to receive OAuth callback
6127
- */
6128
- startCallbackServer(port, expectedState) {
6129
- return new Promise((resolve2, reject) => {
6130
- const server = http.createServer(async (req, res) => {
6131
- if (req.method !== "GET" || !req.url?.startsWith("/callback")) {
6132
- res.writeHead(404);
6133
- res.end("Not Found");
6134
- return;
6135
- }
6136
- const url = new URL(req.url, `http://localhost:${port}`);
6137
- const code = url.searchParams.get("code");
6138
- const returnedState = url.searchParams.get("state");
6139
- const error = url.searchParams.get("error");
6140
- if (error) {
6141
- res.writeHead(200, { "Content-Type": "text/html" });
6142
- res.end(this.buildErrorPage(error));
6143
- server.close();
6144
- reject(new Error(`OAuth error: ${error}`));
6145
- return;
6146
- }
6147
- if (returnedState !== expectedState) {
6148
- const errorMsg = "Security error: state parameter mismatch";
6149
- res.writeHead(200, { "Content-Type": "text/html" });
6150
- res.end(this.buildErrorPage(errorMsg));
6151
- server.close();
6152
- reject(new Error(errorMsg));
6153
- return;
6154
- }
6155
- if (!code) {
6156
- const errorMsg = "No authorization code received";
6157
- res.writeHead(400, { "Content-Type": "text/html" });
6158
- res.end(this.buildErrorPage(errorMsg));
6159
- server.close();
6160
- reject(new Error(errorMsg));
6161
- return;
6162
- }
6163
- try {
6164
- await this.submitAuthCode(code, returnedState);
6165
- res.writeHead(200, { "Content-Type": "text/html" });
6166
- res.end(this.buildSuccessPage());
6167
- server.close();
6168
- resolve2();
6169
- } catch (err) {
6170
- const errorMsg = err instanceof Error ? err.message : "Token exchange failed";
6171
- res.writeHead(200, { "Content-Type": "text/html" });
6172
- res.end(this.buildErrorPage(errorMsg));
6173
- server.close();
6174
- reject(err);
6175
- }
6176
- });
6177
- server.on("error", (error) => {
6178
- if (error.code === "EADDRINUSE") {
6179
- reject(new Error("Port already in use. Please try again."));
6180
- } else {
6181
- reject(error);
6182
- }
6183
- });
6184
- const timeout = setTimeout(() => {
6185
- server.close();
6186
- reject(new Error("Authentication timeout - no response received after 5 minutes"));
6187
- }, CALLBACK_TIMEOUT_MS);
6188
- server.on("close", () => {
6189
- clearTimeout(timeout);
6190
- });
6191
- server.listen(port, "127.0.0.1", () => {
6192
- console.log(`Waiting for authentication callback on http://localhost:${port}/callback`);
6193
- });
6194
- });
6195
- }
6196
- /**
6197
- * Submits the authorization code and exchanges it for tokens
6198
- */
6199
- async submitAuthCode(code, state) {
6200
- const tokens = await this.exchangeCodeForTokens(code, state);
6201
- await this.saveTokens(tokens);
6202
- return tokens;
6203
- }
6204
- /**
6205
- * Exchanges authorization code for access and refresh tokens
6206
- */
6207
- async exchangeCodeForTokens(code, state) {
6208
- if (!this.pendingCodeVerifier) {
6209
- throw new Error("No PKCE code verifier found. Please start the auth flow first.");
6210
- }
6211
- const body = {
6212
- grant_type: "authorization_code",
6213
- code,
6214
- state,
6215
- // Non-standard: state in body
6216
- redirect_uri: OAUTH_CONFIG.redirectUri,
6217
- client_id: OAUTH_CONFIG.clientId,
6218
- code_verifier: this.pendingCodeVerifier
6219
- // PKCE verifier
6220
- };
6221
- const response = await fetch(OAUTH_CONFIG.tokenEndpoint, {
6222
- method: "POST",
6223
- headers: {
6224
- "Content-Type": "application/json"
6225
- // Non-standard: JSON instead of form-encoded
6226
- },
6227
- body: JSON.stringify(body)
6228
- });
6229
- this.pendingCodeVerifier = null;
6230
- if (!response.ok) {
6231
- const error = await response.text();
6232
- throw new Error(`Token exchange failed: ${error}`);
6233
- }
6234
- const data = await response.json();
6235
- return {
6236
- accessToken: data.access_token,
6237
- refreshToken: data.refresh_token,
6238
- expiresAt: Date.now() + data.expires_in * 1e3
6239
- };
6240
- }
6241
- /**
6242
- * Refreshes the access token using the refresh token
6243
- */
6244
- async refreshTokens() {
6245
- const tokens = await this.getTokens();
6246
- if (!tokens) {
6247
- throw new Error("No tokens found to refresh");
6248
- }
6249
- const body = {
6250
- grant_type: "refresh_token",
6251
- refresh_token: tokens.refreshToken,
6252
- client_id: OAUTH_CONFIG.clientId
6253
- };
6254
- const response = await fetch(OAUTH_CONFIG.tokenEndpoint, {
6255
- method: "POST",
6256
- headers: {
6257
- "Content-Type": "application/json"
6258
- },
6259
- body: JSON.stringify(body)
6260
- });
6261
- if (!response.ok) {
6262
- const error = await response.text();
6263
- throw new Error(`Token refresh failed: ${error}`);
6264
- }
6265
- const data = await response.json();
6266
- const newTokens = {
6267
- accessToken: data.access_token,
6268
- refreshToken: data.refresh_token,
6269
- expiresAt: Date.now() + data.expires_in * 1e3
6270
- };
6271
- await this.saveTokens(newTokens);
6272
- return newTokens;
6273
- }
6274
- /**
6275
- * Gets the current access token, refreshing if necessary
6276
- */
6277
- async getAccessToken() {
6278
- const tokens = await this.getTokens();
6279
- if (!tokens) {
6280
- return null;
6281
- }
6282
- if (Date.now() > tokens.expiresAt - _ClaudeOAuthService.TOKEN_REFRESH_BUFFER_MS) {
6283
- try {
6284
- const refreshedTokens = await this.refreshTokens();
6285
- return refreshedTokens.accessToken;
6286
- } catch (error) {
6287
- console.warn("Token refresh failed:", error);
6288
- return null;
6289
- }
6290
- }
6291
- return tokens.accessToken;
6292
- }
6293
- /**
6294
- * Gets the stored tokens
6295
- */
6296
- async getTokens() {
6297
- try {
6298
- const accessToken = await this.secretStorage.getSecret("claude_oauth_access_token");
6299
- const refreshToken = await this.secretStorage.getSecret("claude_oauth_refresh_token");
6300
- const expiresAt = await this.secretStorage.getSecret("claude_oauth_expires_at");
6301
- if (!accessToken || !refreshToken || !expiresAt) {
6302
- return null;
6303
- }
6304
- return {
6305
- accessToken,
6306
- refreshToken,
6307
- expiresAt: parseInt(expiresAt, 10)
6308
- };
6309
- } catch (error) {
6310
- console.error("Failed to get OAuth tokens:", error);
6311
- return null;
6312
- }
6313
- }
6314
- /**
6315
- * Saves OAuth tokens to secure storage
6316
- */
6317
- async saveTokens(tokens) {
6318
- await this.secretStorage.setSecret("claude_oauth_access_token", tokens.accessToken);
6319
- await this.secretStorage.setSecret("claude_oauth_refresh_token", tokens.refreshToken);
6320
- await this.secretStorage.setSecret("claude_oauth_expires_at", tokens.expiresAt.toString());
6321
- }
6322
- /**
6323
- * Deletes stored OAuth tokens
6324
- */
6325
- async deleteTokens() {
6326
- await this.secretStorage.deleteSecret("claude_oauth_access_token");
6327
- await this.secretStorage.deleteSecret("claude_oauth_refresh_token");
6328
- await this.secretStorage.deleteSecret("claude_oauth_expires_at");
6329
- }
6330
- /**
6331
- * Checks if user is authenticated via OAuth
6332
- */
6333
- async isAuthenticated() {
6334
- const tokens = await this.getTokens();
6335
- return tokens !== null;
6336
- }
6337
- /**
6338
- * Gets the current OAuth authentication status
6339
- */
6340
- async getStatus() {
6341
- try {
6342
- const tokens = await this.getTokens();
6343
- if (!tokens) {
6344
- return { isAuthenticated: false };
6345
- }
6346
- return {
6347
- isAuthenticated: true,
6348
- expiresAt: tokens.expiresAt
6349
- };
6350
- } catch (error) {
6351
- return {
6352
- isAuthenticated: false,
6353
- error: error instanceof Error ? error.message : "Failed to get OAuth status"
6354
- };
6355
- }
6356
- }
6357
- /**
6358
- * Builds the authorization URL with all required parameters
6359
- */
6360
- buildAuthorizationUrl(state, codeChallenge) {
6361
- const params = new URLSearchParams({
6362
- response_type: "code",
6363
- client_id: OAUTH_CONFIG.clientId,
6364
- redirect_uri: OAUTH_CONFIG.redirectUri,
6365
- scope: OAUTH_CONFIG.scopes.join(" "),
6366
- state
6367
- });
6368
- if (codeChallenge) {
6369
- params.set("code_challenge", codeChallenge);
6370
- params.set("code_challenge_method", "S256");
6371
- }
6372
- return `${OAUTH_CONFIG.authorizationEndpoint}?${params.toString()}`;
6373
- }
6374
- /**
6375
- * Generates a random state string for CSRF protection
6376
- */
6377
- generateRandomState() {
6378
- return Array.from(crypto.getRandomValues(new Uint8Array(32))).map((b) => b.toString(16).padStart(2, "0")).join("");
6379
- }
6380
- /**
6381
- * Generates PKCE code verifier and challenge
6382
- * PKCE (Proof Key for Code Exchange) adds security to OAuth for public clients
6383
- */
6384
- generatePKCEChallenge() {
6385
- const codeVerifier = randomBytes(32).toString("base64url");
6386
- const hash = createHash("sha256").update(codeVerifier).digest("base64url");
6387
- return {
6388
- codeVerifier,
6389
- codeChallenge: hash
6390
- };
6391
- }
6392
- /**
6393
- * Open a URL in the default browser cross-platform
6394
- */
6395
- openBrowser(url) {
6396
- const os3 = platform();
6397
- let command;
6398
- let args;
6399
- switch (os3) {
6400
- case "darwin":
6401
- command = "open";
6402
- args = [url];
6403
- break;
6404
- case "win32":
6405
- command = "start";
6406
- args = ["", url];
6407
- break;
6408
- default:
6409
- command = "xdg-open";
6410
- args = [url];
6411
- break;
6412
- }
6413
- const options = { detached: true, stdio: "ignore", shell: os3 === "win32" };
6414
- spawn(command, args, options).unref();
6415
- }
6416
- /**
6417
- * Build success HTML page
6418
- */
6419
- buildSuccessPage() {
6420
- return `
6421
- <!DOCTYPE html>
6422
- <html lang="en">
6423
- <head>
6424
- <meta charset="UTF-8" />
6425
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6426
- <title>Authentication Successful - Supatest CLI</title>
6427
- <style>
6428
- body {
6429
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
6430
- display: flex;
6431
- align-items: center;
6432
- justify-content: center;
6433
- height: 100vh;
6434
- margin: 0;
6435
- background: #fefefe;
6436
- }
6437
- .container {
6438
- background: white;
6439
- padding: 3rem 2rem;
6440
- border-radius: 12px;
6441
- box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
6442
- border: 1px solid #e5e7eb;
6443
- text-align: center;
6444
- max-width: 400px;
6445
- }
6446
- .success-icon {
6447
- font-size: 48px;
6448
- margin-bottom: 1rem;
6449
- }
6450
- h1 {
6451
- color: #10b981;
6452
- margin: 0 0 1rem 0;
6453
- font-size: 24px;
6454
- }
6455
- p {
6456
- color: #666;
6457
- margin: 0;
6458
- line-height: 1.5;
6459
- }
6460
- </style>
6461
- </head>
6462
- <body>
6463
- <div class="container">
6464
- <div class="success-icon">\u2705</div>
6465
- <h1>Authentication Successful!</h1>
6466
- <p>You're now authenticated with Claude.</p>
6467
- <p style="margin-top: 1rem;">You can close this window and return to your terminal.</p>
6468
- </div>
6469
- </body>
6470
- </html>
6471
- `;
6472
- }
6473
- /**
6474
- * Build error HTML page
6475
- */
6476
- buildErrorPage(errorMessage) {
6477
- const escapedError = errorMessage.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
6478
- return `
6479
- <!DOCTYPE html>
6480
- <html lang="en">
6481
- <head>
6482
- <meta charset="UTF-8" />
6483
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6484
- <title>Authentication Failed - Supatest CLI</title>
6485
- <style>
6486
- body {
6487
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
6488
- display: flex;
6489
- align-items: center;
6490
- justify-content: center;
6491
- height: 100vh;
6492
- margin: 0;
6493
- background: #fefefe;
6494
- }
6495
- .container {
6496
- background: white;
6497
- padding: 3rem 2rem;
6498
- border-radius: 12px;
6499
- box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
6500
- border: 1px solid #e5e7eb;
6501
- text-align: center;
6502
- max-width: 400px;
6503
- }
6504
- .error-icon {
6505
- font-size: 48px;
6506
- margin-bottom: 1rem;
6507
- }
6508
- h1 {
6509
- color: #dc2626;
6510
- margin: 0 0 1rem 0;
6511
- font-size: 24px;
6512
- }
6513
- p {
6514
- color: #666;
6515
- margin: 0;
6516
- line-height: 1.5;
6517
- }
6518
- </style>
6519
- </head>
6520
- <body>
6521
- <div class="container">
6522
- <div class="error-icon">\u274C</div>
6523
- <h1>Authentication Failed</h1>
6524
- <p>${escapedError}</p>
6525
- <p style="margin-top: 1rem;">You can close this window and try again.</p>
6526
- </div>
6527
- </body>
6528
- </html>
6529
- `;
6530
- }
6531
- };
6532
- }
6533
- });
6534
-
6535
- // src/utils/secret-storage.ts
6536
- var secret_storage_exports = {};
6537
- __export(secret_storage_exports, {
6538
- deleteSecret: () => deleteSecret,
6539
- getSecret: () => getSecret,
6540
- getSecretStorage: () => getSecretStorage,
6541
- listSecrets: () => listSecrets,
6542
- setSecret: () => setSecret
6543
- });
6544
- import { promises as fs } from "fs";
6545
- import { homedir } from "os";
6546
- import { dirname, join } from "path";
6547
- async function getSecret(key) {
6548
- return storage.getSecret(key);
6549
- }
6550
- async function setSecret(key, value) {
6551
- await storage.setSecret(key, value);
6552
- }
6553
- async function deleteSecret(key) {
6554
- return storage.deleteSecret(key);
6555
- }
6556
- async function listSecrets() {
6557
- return storage.listSecrets();
6558
- }
6559
- function getSecretStorage() {
6560
- return storage;
6561
- }
6562
- var SECRET_FILE_NAME, FileSecretStorage, storage;
6563
- var init_secret_storage = __esm({
6564
- "src/utils/secret-storage.ts"() {
6565
- "use strict";
6566
- SECRET_FILE_NAME = "secrets.json";
6567
- FileSecretStorage = class {
6568
- secretFilePath;
6569
- constructor() {
6570
- const rootDirName = process.env.NODE_ENV === "development" ? ".supatest-dev" : ".supatest";
6571
- const secretsDir = join(homedir(), rootDirName, "claude-auth");
6572
- this.secretFilePath = join(secretsDir, SECRET_FILE_NAME);
6573
- }
6574
- async ensureDirectoryExists() {
6575
- const dir = dirname(this.secretFilePath);
6576
- await fs.mkdir(dir, { recursive: true, mode: 448 });
6577
- }
6578
- async loadSecrets() {
6579
- try {
6580
- const data = await fs.readFile(this.secretFilePath, "utf-8");
6581
- const secrets = JSON.parse(data);
6582
- return new Map(Object.entries(secrets));
6583
- } catch (error) {
6584
- const err = error;
6585
- if (err.code === "ENOENT") {
6586
- return /* @__PURE__ */ new Map();
6587
- }
6588
- try {
6589
- await fs.unlink(this.secretFilePath);
6590
- } catch {
6591
- }
6592
- return /* @__PURE__ */ new Map();
6593
- }
6594
- }
6595
- async saveSecrets(secrets) {
6596
- await this.ensureDirectoryExists();
6597
- const data = Object.fromEntries(secrets);
6598
- const json = JSON.stringify(data, null, 2);
6599
- await fs.writeFile(this.secretFilePath, json, { mode: 384 });
6600
- }
6601
- async getSecret(key) {
6602
- const secrets = await this.loadSecrets();
6603
- return secrets.get(key) ?? null;
6604
- }
6605
- async setSecret(key, value) {
6606
- const secrets = await this.loadSecrets();
6607
- secrets.set(key, value);
6608
- await this.saveSecrets(secrets);
6609
- }
6610
- async deleteSecret(key) {
6611
- const secrets = await this.loadSecrets();
6612
- if (!secrets.has(key)) {
6613
- return false;
6614
- }
6615
- secrets.delete(key);
6616
- if (secrets.size === 0) {
6617
- try {
6618
- await fs.unlink(this.secretFilePath);
6619
- } catch (error) {
6620
- const err = error;
6621
- if (err.code !== "ENOENT") {
6622
- throw error;
6623
- }
6624
- }
6625
- } else {
6626
- await this.saveSecrets(secrets);
6627
- }
6628
- return true;
6629
- }
6630
- async listSecrets() {
6631
- const secrets = await this.loadSecrets();
6632
- return Array.from(secrets.keys());
6633
- }
6634
- };
6635
- storage = new FileSecretStorage();
6636
- }
6637
- });
6638
-
6639
6057
  // src/commands/setup.ts
6640
- import { execSync, spawn as spawn2, spawnSync } from "child_process";
6641
- import fs2 from "fs";
6058
+ import { execSync, spawn, spawnSync } from "child_process";
6059
+ import fs from "fs";
6642
6060
  import os from "os";
6643
6061
  import path from "path";
6644
6062
  function parseVersion(versionString) {
@@ -6692,7 +6110,7 @@ function getPlaywrightCachePath() {
6692
6110
  // Windows
6693
6111
  ];
6694
6112
  for (const cachePath of cachePaths) {
6695
- if (fs2.existsSync(cachePath)) {
6113
+ if (fs.existsSync(cachePath)) {
6696
6114
  return cachePath;
6697
6115
  }
6698
6116
  }
@@ -6702,7 +6120,7 @@ function getInstalledChromiumVersion() {
6702
6120
  const cachePath = getPlaywrightCachePath();
6703
6121
  if (!cachePath) return null;
6704
6122
  try {
6705
- const entries = fs2.readdirSync(cachePath);
6123
+ const entries = fs.readdirSync(cachePath);
6706
6124
  const chromiumVersions = entries.filter((entry) => entry.startsWith("chromium-") && !entry.includes("headless")).map((entry) => entry.replace("chromium-", "")).sort((a, b) => Number(b) - Number(a));
6707
6125
  return chromiumVersions[0] || null;
6708
6126
  } catch {
@@ -6736,7 +6154,7 @@ function checkNodeVersion() {
6736
6154
  }
6737
6155
  async function installChromium() {
6738
6156
  return new Promise((resolve2) => {
6739
- const child = spawn2("npx", ["playwright", "install", "chromium"], {
6157
+ const child = spawn("npx", ["playwright", "install", "chromium"], {
6740
6158
  stdio: "inherit",
6741
6159
  shell: true
6742
6160
  // Required for Windows where npx is npx.cmd
@@ -6766,14 +6184,14 @@ function createSupatestConfig(cwd) {
6766
6184
  const supatestDir = path.join(cwd, ".supatest");
6767
6185
  const mcpJsonPath = path.join(supatestDir, "mcp.json");
6768
6186
  try {
6769
- if (!fs2.existsSync(supatestDir)) {
6770
- fs2.mkdirSync(supatestDir, { recursive: true });
6187
+ if (!fs.existsSync(supatestDir)) {
6188
+ fs.mkdirSync(supatestDir, { recursive: true });
6771
6189
  }
6772
6190
  let config2;
6773
6191
  let fileExisted = false;
6774
- if (fs2.existsSync(mcpJsonPath)) {
6192
+ if (fs.existsSync(mcpJsonPath)) {
6775
6193
  fileExisted = true;
6776
- const existingContent = fs2.readFileSync(mcpJsonPath, "utf-8");
6194
+ const existingContent = fs.readFileSync(mcpJsonPath, "utf-8");
6777
6195
  config2 = JSON.parse(existingContent);
6778
6196
  } else {
6779
6197
  config2 = {};
@@ -6784,7 +6202,7 @@ function createSupatestConfig(cwd) {
6784
6202
  if (!config2.mcpServers.playwright) {
6785
6203
  config2.mcpServers.playwright = DEFAULT_MCP_CONFIG.mcpServers.playwright;
6786
6204
  }
6787
- fs2.writeFileSync(mcpJsonPath, JSON.stringify(config2, null, 2) + "\n", "utf-8");
6205
+ fs.writeFileSync(mcpJsonPath, JSON.stringify(config2, null, 2) + "\n", "utf-8");
6788
6206
  if (fileExisted) {
6789
6207
  return {
6790
6208
  ok: true,
@@ -6911,18 +6329,18 @@ var CLI_VERSION;
6911
6329
  var init_version = __esm({
6912
6330
  "src/version.ts"() {
6913
6331
  "use strict";
6914
- CLI_VERSION = "0.0.41";
6332
+ CLI_VERSION = "0.0.42";
6915
6333
  }
6916
6334
  });
6917
6335
 
6918
6336
  // src/utils/error-logger.ts
6919
- import * as fs3 from "fs";
6337
+ import * as fs2 from "fs";
6920
6338
  import * as os2 from "os";
6921
6339
  import * as path2 from "path";
6922
6340
  function ensureLogDir() {
6923
6341
  try {
6924
- if (!fs3.existsSync(LOGS_DIR)) {
6925
- fs3.mkdirSync(LOGS_DIR, { recursive: true });
6342
+ if (!fs2.existsSync(LOGS_DIR)) {
6343
+ fs2.mkdirSync(LOGS_DIR, { recursive: true });
6926
6344
  }
6927
6345
  return true;
6928
6346
  } catch {
@@ -6931,14 +6349,14 @@ function ensureLogDir() {
6931
6349
  }
6932
6350
  function rotateLogIfNeeded() {
6933
6351
  try {
6934
- if (!fs3.existsSync(ERROR_LOG_FILE)) return;
6935
- const stats = fs3.statSync(ERROR_LOG_FILE);
6352
+ if (!fs2.existsSync(ERROR_LOG_FILE)) return;
6353
+ const stats = fs2.statSync(ERROR_LOG_FILE);
6936
6354
  if (stats.size > MAX_LOG_SIZE) {
6937
6355
  const oldLogFile = `${ERROR_LOG_FILE}.old`;
6938
- if (fs3.existsSync(oldLogFile)) {
6939
- fs3.unlinkSync(oldLogFile);
6356
+ if (fs2.existsSync(oldLogFile)) {
6357
+ fs2.unlinkSync(oldLogFile);
6940
6358
  }
6941
- fs3.renameSync(ERROR_LOG_FILE, oldLogFile);
6359
+ fs2.renameSync(ERROR_LOG_FILE, oldLogFile);
6942
6360
  }
6943
6361
  } catch {
6944
6362
  }
@@ -6974,7 +6392,7 @@ function logError(error, context) {
6974
6392
  const logLine = `${JSON.stringify(entry)}
6975
6393
  `;
6976
6394
  try {
6977
- fs3.appendFileSync(ERROR_LOG_FILE, logLine);
6395
+ fs2.appendFileSync(ERROR_LOG_FILE, logLine);
6978
6396
  } catch {
6979
6397
  }
6980
6398
  }
@@ -6991,7 +6409,7 @@ var init_error_logger = __esm({
6991
6409
  });
6992
6410
 
6993
6411
  // src/utils/logger.ts
6994
- import * as fs4 from "fs";
6412
+ import * as fs3 from "fs";
6995
6413
  import * as path3 from "path";
6996
6414
  import chalk from "chalk";
6997
6415
  var Logger, logger;
@@ -7025,7 +6443,7 @@ ${"=".repeat(80)}
7025
6443
  ${"=".repeat(80)}
7026
6444
  `;
7027
6445
  try {
7028
- fs4.appendFileSync(this.logFile, separator);
6446
+ fs3.appendFileSync(this.logFile, separator);
7029
6447
  } catch (error) {
7030
6448
  }
7031
6449
  }
@@ -7039,7 +6457,7 @@ ${"=".repeat(80)}
7039
6457
  ` : `[${timestamp}] [${level}] ${message}
7040
6458
  `;
7041
6459
  try {
7042
- fs4.appendFileSync(this.logFile, logEntry);
6460
+ fs3.appendFileSync(this.logFile, logEntry);
7043
6461
  } catch (error) {
7044
6462
  }
7045
6463
  }
@@ -7785,7 +7203,7 @@ var init_api_client = __esm({
7785
7203
 
7786
7204
  // src/utils/command-discovery.ts
7787
7205
  import { existsSync as existsSync2, readdirSync, readFileSync, statSync as statSync2 } from "fs";
7788
- import { join as join4, relative } from "path";
7206
+ import { join as join3, relative } from "path";
7789
7207
  function parseMarkdownFrontmatter(content) {
7790
7208
  const frontmatterRegex = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/;
7791
7209
  const match = content.match(frontmatterRegex);
@@ -7810,7 +7228,7 @@ function discoverMarkdownFiles(dir, baseDir, files = []) {
7810
7228
  }
7811
7229
  const entries = readdirSync(dir);
7812
7230
  for (const entry of entries) {
7813
- const fullPath = join4(dir, entry);
7231
+ const fullPath = join3(dir, entry);
7814
7232
  const stat = statSync2(fullPath);
7815
7233
  if (stat.isDirectory()) {
7816
7234
  discoverMarkdownFiles(fullPath, baseDir, files);
@@ -7821,7 +7239,7 @@ function discoverMarkdownFiles(dir, baseDir, files = []) {
7821
7239
  return files;
7822
7240
  }
7823
7241
  function discoverCommands(cwd) {
7824
- const commandsDir = join4(cwd, ".supatest", "commands");
7242
+ const commandsDir = join3(cwd, ".supatest", "commands");
7825
7243
  if (!existsSync2(commandsDir)) {
7826
7244
  return [];
7827
7245
  }
@@ -7844,9 +7262,9 @@ function discoverCommands(cwd) {
7844
7262
  return commands.sort((a, b) => a.name.localeCompare(b.name));
7845
7263
  }
7846
7264
  function expandCommand(cwd, commandName, args) {
7847
- const commandsDir = join4(cwd, ".supatest", "commands");
7265
+ const commandsDir = join3(cwd, ".supatest", "commands");
7848
7266
  const relativePath = commandName.replace(/\./g, "/") + ".md";
7849
- const filePath = join4(commandsDir, relativePath);
7267
+ const filePath = join3(commandsDir, relativePath);
7850
7268
  if (!existsSync2(filePath)) {
7851
7269
  return null;
7852
7270
  }
@@ -7869,7 +7287,7 @@ function expandCommand(cwd, commandName, args) {
7869
7287
  }
7870
7288
  }
7871
7289
  function discoverAgents(cwd) {
7872
- const agentsDir = join4(cwd, ".supatest", "agents");
7290
+ const agentsDir = join3(cwd, ".supatest", "agents");
7873
7291
  if (!existsSync2(agentsDir)) {
7874
7292
  return [];
7875
7293
  }
@@ -7900,8 +7318,8 @@ var init_command_discovery = __esm({
7900
7318
 
7901
7319
  // src/utils/mcp-loader.ts
7902
7320
  import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
7903
- import { homedir as homedir3 } from "os";
7904
- import { join as join5 } from "path";
7321
+ import { homedir as homedir2 } from "os";
7322
+ import { join as join4 } from "path";
7905
7323
  function expandEnvVar(value) {
7906
7324
  return value.replace(/\$\{([^}]+)\}/g, (_, expr) => {
7907
7325
  const [varName, defaultValue] = expr.split(":-");
@@ -7950,9 +7368,9 @@ function loadMcpServersFromFile(mcpPath) {
7950
7368
  }
7951
7369
  }
7952
7370
  function loadMcpServers(cwd) {
7953
- const globalMcpPath = join5(homedir3(), ".supatest", "mcp.json");
7371
+ const globalMcpPath = join4(homedir2(), ".supatest", "mcp.json");
7954
7372
  const globalServers = loadMcpServersFromFile(globalMcpPath);
7955
- const projectMcpPath = join5(cwd, ".supatest", "mcp.json");
7373
+ const projectMcpPath = join4(cwd, ".supatest", "mcp.json");
7956
7374
  const projectServers = loadMcpServersFromFile(projectMcpPath);
7957
7375
  return { ...globalServers, ...projectServers };
7958
7376
  }
@@ -7964,11 +7382,11 @@ var init_mcp_loader = __esm({
7964
7382
 
7965
7383
  // src/utils/project-instructions.ts
7966
7384
  import { existsSync as existsSync4, readFileSync as readFileSync3 } from "fs";
7967
- import { join as join6 } from "path";
7385
+ import { join as join5 } from "path";
7968
7386
  function loadProjectInstructions(cwd) {
7969
7387
  const paths = [
7970
- join6(cwd, "SUPATEST.md"),
7971
- join6(cwd, ".supatest", "SUPATEST.md")
7388
+ join5(cwd, "SUPATEST.md"),
7389
+ join5(cwd, ".supatest", "SUPATEST.md")
7972
7390
  ];
7973
7391
  for (const path6 of paths) {
7974
7392
  if (existsSync4(path6)) {
@@ -7988,8 +7406,8 @@ var init_project_instructions = __esm({
7988
7406
 
7989
7407
  // src/core/agent.ts
7990
7408
  import { createRequire } from "module";
7991
- import { homedir as homedir4 } from "os";
7992
- import { dirname as dirname2, join as join7 } from "path";
7409
+ import { homedir as homedir3 } from "os";
7410
+ import { dirname, join as join6 } from "path";
7993
7411
  import { query } from "@anthropic-ai/claude-agent-sdk";
7994
7412
  var CoreAgent;
7995
7413
  var init_agent = __esm({
@@ -8119,7 +7537,7 @@ ${projectInstructions}`,
8119
7537
  this.presenter.onLog(`Auth: Using Claude Max (default Claude Code credentials)`);
8120
7538
  logger.debug("[agent] Claude Max mode: Using default ~/.claude/ config, cleared provider credentials");
8121
7539
  } else {
8122
- const internalConfigDir = join7(homedir4(), ".supatest", "claude-internal");
7540
+ const internalConfigDir = join6(homedir3(), ".supatest", "claude-internal");
8123
7541
  cleanEnv.CLAUDE_CONFIG_DIR = internalConfigDir;
8124
7542
  cleanEnv.ANTHROPIC_API_KEY = config2.supatestApiKey || "";
8125
7543
  cleanEnv.ANTHROPIC_BASE_URL = process.env.ANTHROPIC_BASE_URL || "";
@@ -8449,7 +7867,7 @@ ${projectInstructions}`,
8449
7867
  let claudeCodePath;
8450
7868
  const require2 = createRequire(import.meta.url);
8451
7869
  const sdkPath = require2.resolve("@anthropic-ai/claude-agent-sdk/sdk.mjs");
8452
- claudeCodePath = join7(dirname2(sdkPath), "cli.js");
7870
+ claudeCodePath = join6(dirname(sdkPath), "cli.js");
8453
7871
  this.presenter.onLog(`Using SDK CLI: ${claudeCodePath}`);
8454
7872
  if (config.claudeCodeExecutablePath) {
8455
7873
  claudeCodePath = config.claudeCodeExecutablePath;
@@ -10143,642 +9561,1224 @@ function createInkStdio() {
10143
9561
  if (prop === "write") {
10144
9562
  return writeToStdout;
10145
9563
  }
10146
- const value = Reflect.get(target, prop, receiver);
10147
- if (typeof value === "function") {
10148
- return value.bind(target);
9564
+ const value = Reflect.get(target, prop, receiver);
9565
+ if (typeof value === "function") {
9566
+ return value.bind(target);
9567
+ }
9568
+ return value;
9569
+ }
9570
+ });
9571
+ const inkStderr = new Proxy(process.stderr, {
9572
+ get(target, prop, receiver) {
9573
+ if (prop === "write") {
9574
+ return writeToStderr;
9575
+ }
9576
+ const value = Reflect.get(target, prop, receiver);
9577
+ if (typeof value === "function") {
9578
+ return value.bind(target);
9579
+ }
9580
+ return value;
9581
+ }
9582
+ });
9583
+ return { stdout: inkStdout, stderr: inkStderr };
9584
+ }
9585
+ function clearTerminalViewportAndScrollback() {
9586
+ writeToStdout("\x1B[3J\x1B[H\x1B[2J");
9587
+ }
9588
+ var originalStdoutWrite, originalStderrWrite;
9589
+ var init_stdio = __esm({
9590
+ "src/utils/stdio.ts"() {
9591
+ "use strict";
9592
+ originalStdoutWrite = process.stdout.write.bind(process.stdout);
9593
+ originalStderrWrite = process.stderr.write.bind(process.stderr);
9594
+ }
9595
+ });
9596
+
9597
+ // src/utils/encryption.ts
9598
+ import crypto2 from "crypto";
9599
+ import { hostname, userInfo } from "os";
9600
+ function deriveEncryptionKey() {
9601
+ const host = hostname();
9602
+ const user = userInfo().username;
9603
+ const salt = `${host}-${user}-supatest-cli`;
9604
+ if (process.env.DEBUG_ENCRYPTION) {
9605
+ console.error(`[encryption] hostname=${host}, username=${user}, salt=${salt}`);
9606
+ }
9607
+ return crypto2.scryptSync("supatest-cli-token", salt, KEY_LENGTH);
9608
+ }
9609
+ function getEncryptionKey() {
9610
+ if (!cachedKey) {
9611
+ cachedKey = deriveEncryptionKey();
9612
+ }
9613
+ return cachedKey;
9614
+ }
9615
+ function encrypt(plaintext) {
9616
+ const key = getEncryptionKey();
9617
+ const iv = crypto2.randomBytes(IV_LENGTH);
9618
+ const cipher = crypto2.createCipheriv(ALGORITHM, key, iv);
9619
+ let encrypted = cipher.update(plaintext, "utf8", "hex");
9620
+ encrypted += cipher.final("hex");
9621
+ const authTag = cipher.getAuthTag();
9622
+ return `${iv.toString("hex")}:${authTag.toString("hex")}:${encrypted}`;
9623
+ }
9624
+ function decrypt(encryptedData) {
9625
+ const parts = encryptedData.split(":");
9626
+ if (parts.length !== 3) {
9627
+ throw new Error("Invalid encrypted data format");
9628
+ }
9629
+ const [ivHex, authTagHex, encrypted] = parts;
9630
+ const iv = Buffer.from(ivHex, "hex");
9631
+ const authTag = Buffer.from(authTagHex, "hex");
9632
+ const key = getEncryptionKey();
9633
+ const decipher = crypto2.createDecipheriv(ALGORITHM, key, iv);
9634
+ decipher.setAuthTag(authTag);
9635
+ let decrypted = decipher.update(encrypted, "hex", "utf8");
9636
+ decrypted += decipher.final("utf8");
9637
+ return decrypted;
9638
+ }
9639
+ var ALGORITHM, KEY_LENGTH, IV_LENGTH, cachedKey;
9640
+ var init_encryption = __esm({
9641
+ "src/utils/encryption.ts"() {
9642
+ "use strict";
9643
+ ALGORITHM = "aes-256-gcm";
9644
+ KEY_LENGTH = 32;
9645
+ IV_LENGTH = 16;
9646
+ cachedKey = null;
9647
+ }
9648
+ });
9649
+
9650
+ // src/utils/token-storage.ts
9651
+ import { existsSync as existsSync5, mkdirSync as mkdirSync2, readFileSync as readFileSync4, unlinkSync as unlinkSync2, writeFileSync } from "fs";
9652
+ import { homedir as homedir5 } from "os";
9653
+ import { join as join7 } from "path";
9654
+ function getTokenFilePath() {
9655
+ const apiUrl = process.env.SUPATEST_API_URL || PRODUCTION_API_URL;
9656
+ if (apiUrl === PRODUCTION_API_URL) {
9657
+ return join7(CONFIG_DIR, "token.json");
9658
+ }
9659
+ return join7(CONFIG_DIR, "token.local.json");
9660
+ }
9661
+ function isV2Format(stored) {
9662
+ return "version" in stored && stored.version === 2;
9663
+ }
9664
+ function ensureConfigDir() {
9665
+ if (!existsSync5(CONFIG_DIR)) {
9666
+ mkdirSync2(CONFIG_DIR, { recursive: true, mode: 448 });
9667
+ }
9668
+ }
9669
+ function saveToken(token, expiresAt) {
9670
+ ensureConfigDir();
9671
+ const payload = {
9672
+ token,
9673
+ expiresAt,
9674
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
9675
+ };
9676
+ const stored = {
9677
+ version: STORAGE_VERSION,
9678
+ encryptedData: encrypt(JSON.stringify(payload))
9679
+ };
9680
+ const tokenFile = getTokenFilePath();
9681
+ writeFileSync(tokenFile, JSON.stringify(stored, null, 2), { mode: 384 });
9682
+ }
9683
+ function loadToken() {
9684
+ const tokenFile = getTokenFilePath();
9685
+ if (!existsSync5(tokenFile)) {
9686
+ return null;
9687
+ }
9688
+ try {
9689
+ const data = readFileSync4(tokenFile, "utf8");
9690
+ const stored = JSON.parse(data);
9691
+ let payload;
9692
+ if (isV2Format(stored)) {
9693
+ payload = JSON.parse(decrypt(stored.encryptedData));
9694
+ } else {
9695
+ payload = stored;
9696
+ }
9697
+ if (payload.expiresAt) {
9698
+ const expiresAt = new Date(payload.expiresAt);
9699
+ if (expiresAt < /* @__PURE__ */ new Date()) {
9700
+ console.warn("CLI token has expired. Please run 'supatest login' again.");
9701
+ return null;
9702
+ }
9703
+ }
9704
+ return payload.token;
9705
+ } catch (error) {
9706
+ const err = error;
9707
+ if (err.message?.includes("Invalid encrypted data format") || err.message?.includes("Unsupported state or unable to authenticate data")) {
9708
+ try {
9709
+ unlinkSync2(tokenFile);
9710
+ } catch {
9711
+ }
9712
+ }
9713
+ return null;
9714
+ }
9715
+ }
9716
+ function removeToken() {
9717
+ const tokenFile = getTokenFilePath();
9718
+ if (existsSync5(tokenFile)) {
9719
+ unlinkSync2(tokenFile);
9720
+ }
9721
+ }
9722
+ var CONFIG_DIR, PRODUCTION_API_URL, STORAGE_VERSION, TOKEN_FILE;
9723
+ var init_token_storage = __esm({
9724
+ "src/utils/token-storage.ts"() {
9725
+ "use strict";
9726
+ init_encryption();
9727
+ CONFIG_DIR = join7(homedir5(), ".supatest");
9728
+ PRODUCTION_API_URL = "https://code-api.supatest.ai";
9729
+ STORAGE_VERSION = 2;
9730
+ TOKEN_FILE = join7(CONFIG_DIR, "token.json");
9731
+ }
9732
+ });
9733
+
9734
+ // src/core/message-bridge.ts
9735
+ var MessageBridge;
9736
+ var init_message_bridge = __esm({
9737
+ "src/core/message-bridge.ts"() {
9738
+ "use strict";
9739
+ MessageBridge = class {
9740
+ queue = [];
9741
+ resolvers = [];
9742
+ closed = false;
9743
+ sessionId;
9744
+ constructor(sessionId) {
9745
+ this.sessionId = sessionId;
9746
+ }
9747
+ /**
9748
+ * Update the session ID (useful when session is created after bridge).
9749
+ */
9750
+ setSessionId(sessionId) {
9751
+ this.sessionId = sessionId;
9752
+ }
9753
+ /**
9754
+ * Push a user message to be injected into the session.
9755
+ * Call this from the UI when user submits a message during agent execution.
9756
+ */
9757
+ push(text) {
9758
+ if (this.closed) {
9759
+ console.warn("[MessageBridge] Cannot push to closed bridge");
9760
+ return;
9761
+ }
9762
+ const message = {
9763
+ type: "user",
9764
+ message: {
9765
+ role: "user",
9766
+ content: [{ type: "text", text }]
9767
+ },
9768
+ parent_tool_use_id: null,
9769
+ session_id: this.sessionId
9770
+ };
9771
+ const resolver = this.resolvers.shift();
9772
+ if (resolver) {
9773
+ resolver({ value: message, done: false });
9774
+ } else {
9775
+ this.queue.push(message);
9776
+ }
10149
9777
  }
10150
- return value;
10151
- }
10152
- });
10153
- const inkStderr = new Proxy(process.stderr, {
10154
- get(target, prop, receiver) {
10155
- if (prop === "write") {
10156
- return writeToStderr;
9778
+ /**
9779
+ * Check if there are pending messages in the queue.
9780
+ */
9781
+ hasPending() {
9782
+ return this.queue.length > 0;
10157
9783
  }
10158
- const value = Reflect.get(target, prop, receiver);
10159
- if (typeof value === "function") {
10160
- return value.bind(target);
9784
+ /**
9785
+ * Close the bridge. No more messages will be accepted.
9786
+ * Any pending async iterators will receive done: true.
9787
+ */
9788
+ close() {
9789
+ if (this.closed) return;
9790
+ this.closed = true;
9791
+ for (const resolver of this.resolvers) {
9792
+ resolver({ value: void 0, done: true });
9793
+ }
9794
+ this.resolvers = [];
10161
9795
  }
10162
- return value;
10163
- }
10164
- });
10165
- return { stdout: inkStdout, stderr: inkStderr };
10166
- }
10167
- function clearTerminalViewportAndScrollback() {
10168
- writeToStdout("\x1B[3J\x1B[H\x1B[2J");
10169
- }
10170
- var originalStdoutWrite, originalStderrWrite;
10171
- var init_stdio = __esm({
10172
- "src/utils/stdio.ts"() {
10173
- "use strict";
10174
- originalStdoutWrite = process.stdout.write.bind(process.stdout);
10175
- originalStderrWrite = process.stderr.write.bind(process.stderr);
9796
+ /**
9797
+ * Check if the bridge is closed.
9798
+ */
9799
+ isClosed() {
9800
+ return this.closed;
9801
+ }
9802
+ [Symbol.asyncIterator]() {
9803
+ return {
9804
+ next: () => {
9805
+ const queued = this.queue.shift();
9806
+ if (queued) {
9807
+ return Promise.resolve({ value: queued, done: false });
9808
+ }
9809
+ if (this.closed) {
9810
+ return Promise.resolve({
9811
+ value: void 0,
9812
+ done: true
9813
+ });
9814
+ }
9815
+ return new Promise((resolve2) => {
9816
+ this.resolvers.push(resolve2);
9817
+ });
9818
+ }
9819
+ };
9820
+ }
9821
+ };
10176
9822
  }
10177
9823
  });
10178
9824
 
10179
- // src/utils/encryption.ts
10180
- import crypto2 from "crypto";
10181
- import { hostname, userInfo } from "os";
10182
- function deriveEncryptionKey() {
10183
- const host = hostname();
10184
- const user = userInfo().username;
10185
- const salt = `${host}-${user}-supatest-cli`;
10186
- if (process.env.DEBUG_ENCRYPTION) {
10187
- console.error(`[encryption] hostname=${host}, username=${user}, salt=${salt}`);
10188
- }
10189
- return crypto2.scryptSync("supatest-cli-token", salt, KEY_LENGTH);
10190
- }
10191
- function getEncryptionKey() {
10192
- if (!cachedKey) {
10193
- cachedKey = deriveEncryptionKey();
10194
- }
10195
- return cachedKey;
9825
+ // src/commands/login.ts
9826
+ import { spawn as spawn3 } from "child_process";
9827
+ import crypto3 from "crypto";
9828
+ import http from "http";
9829
+ import { platform } from "os";
9830
+ function escapeHtml(text) {
9831
+ return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
10196
9832
  }
10197
- function encrypt(plaintext) {
10198
- const key = getEncryptionKey();
10199
- const iv = crypto2.randomBytes(IV_LENGTH);
10200
- const cipher = crypto2.createCipheriv(ALGORITHM, key, iv);
10201
- let encrypted = cipher.update(plaintext, "utf8", "hex");
10202
- encrypted += cipher.final("hex");
10203
- const authTag = cipher.getAuthTag();
10204
- return `${iv.toString("hex")}:${authTag.toString("hex")}:${encrypted}`;
9833
+ function generateState() {
9834
+ return crypto3.randomBytes(STATE_LENGTH).toString("hex");
10205
9835
  }
10206
- function decrypt(encryptedData) {
10207
- const parts = encryptedData.split(":");
10208
- if (parts.length !== 3) {
10209
- throw new Error("Invalid encrypted data format");
9836
+ async function exchangeCodeForToken(code, state) {
9837
+ const response = await fetch(`${API_URL}/web/auth/cli-token-exchange`, {
9838
+ method: "POST",
9839
+ headers: { "Content-Type": "application/json" },
9840
+ body: JSON.stringify({ code, state })
9841
+ });
9842
+ if (!response.ok) {
9843
+ const error = await response.json();
9844
+ throw new Error(error.error || "Failed to exchange code for token");
10210
9845
  }
10211
- const [ivHex, authTagHex, encrypted] = parts;
10212
- const iv = Buffer.from(ivHex, "hex");
10213
- const authTag = Buffer.from(authTagHex, "hex");
10214
- const key = getEncryptionKey();
10215
- const decipher = crypto2.createDecipheriv(ALGORITHM, key, iv);
10216
- decipher.setAuthTag(authTag);
10217
- let decrypted = decipher.update(encrypted, "hex", "utf8");
10218
- decrypted += decipher.final("utf8");
10219
- return decrypted;
9846
+ const data = await response.json();
9847
+ return { token: data.token, expiresAt: data.expiresAt };
10220
9848
  }
10221
- var ALGORITHM, KEY_LENGTH, IV_LENGTH, cachedKey;
10222
- var init_encryption = __esm({
10223
- "src/utils/encryption.ts"() {
10224
- "use strict";
10225
- ALGORITHM = "aes-256-gcm";
10226
- KEY_LENGTH = 32;
10227
- IV_LENGTH = 16;
10228
- cachedKey = null;
10229
- }
10230
- });
10231
-
10232
- // src/utils/token-storage.ts
10233
- import { existsSync as existsSync5, mkdirSync as mkdirSync2, readFileSync as readFileSync4, unlinkSync as unlinkSync2, writeFileSync } from "fs";
10234
- import { homedir as homedir6 } from "os";
10235
- import { join as join8 } from "path";
10236
- function getTokenFilePath() {
10237
- const apiUrl = process.env.SUPATEST_API_URL || PRODUCTION_API_URL;
10238
- if (apiUrl === PRODUCTION_API_URL) {
10239
- return join8(CONFIG_DIR, "token.json");
9849
+ function openBrowser(url) {
9850
+ const os3 = platform();
9851
+ let command;
9852
+ let args;
9853
+ switch (os3) {
9854
+ case "darwin":
9855
+ command = "open";
9856
+ args = [url];
9857
+ break;
9858
+ case "win32":
9859
+ command = "start";
9860
+ args = ["", url];
9861
+ break;
9862
+ default:
9863
+ command = "xdg-open";
9864
+ args = [url];
10240
9865
  }
10241
- return join8(CONFIG_DIR, "token.local.json");
9866
+ const options = { detached: true, stdio: "ignore", shell: os3 === "win32" };
9867
+ spawn3(command, args, options).unref();
10242
9868
  }
10243
- function isV2Format(stored) {
10244
- return "version" in stored && stored.version === 2;
9869
+ function startCallbackServer(port, expectedState) {
9870
+ return new Promise((resolve2, reject) => {
9871
+ const server = http.createServer(async (req, res) => {
9872
+ if (req.method !== "GET" || !req.url?.startsWith("/callback")) {
9873
+ res.writeHead(404);
9874
+ res.end("Not Found");
9875
+ return;
9876
+ }
9877
+ const url = new URL(req.url, `http://localhost:${port}`);
9878
+ const code = url.searchParams.get("code");
9879
+ const state = url.searchParams.get("state");
9880
+ const error = url.searchParams.get("error");
9881
+ if (error) {
9882
+ res.writeHead(200, { "Content-Type": "text/html" });
9883
+ res.end(buildErrorPage(error));
9884
+ server.close();
9885
+ reject(new Error(error));
9886
+ return;
9887
+ }
9888
+ if (state !== expectedState) {
9889
+ const errorMsg = "Security error: state parameter mismatch";
9890
+ res.writeHead(200, { "Content-Type": "text/html" });
9891
+ res.end(buildErrorPage(errorMsg));
9892
+ server.close();
9893
+ reject(new Error(errorMsg));
9894
+ return;
9895
+ }
9896
+ if (!code) {
9897
+ const errorMsg = "No authorization code received";
9898
+ res.writeHead(400, { "Content-Type": "text/html" });
9899
+ res.end(buildErrorPage(errorMsg));
9900
+ server.close();
9901
+ reject(new Error(errorMsg));
9902
+ return;
9903
+ }
9904
+ try {
9905
+ const result = await exchangeCodeForToken(code, state);
9906
+ res.writeHead(200, { "Content-Type": "text/html" });
9907
+ res.end(buildSuccessPage());
9908
+ server.close();
9909
+ resolve2(result);
9910
+ } catch (err) {
9911
+ const errorMsg = err instanceof Error ? err.message : "Token exchange failed";
9912
+ res.writeHead(200, { "Content-Type": "text/html" });
9913
+ res.end(buildErrorPage(errorMsg));
9914
+ server.close();
9915
+ reject(err);
9916
+ }
9917
+ });
9918
+ server.on("error", (error) => {
9919
+ if (error.code === "EADDRINUSE") {
9920
+ const portError = new Error(
9921
+ "Something went wrong. Please restart the CLI and try again."
9922
+ );
9923
+ portError.code = "EADDRINUSE";
9924
+ reject(portError);
9925
+ } else {
9926
+ reject(error);
9927
+ }
9928
+ });
9929
+ const timeout = setTimeout(() => {
9930
+ server.close();
9931
+ reject(new Error("Login timeout - no response received after 5 minutes"));
9932
+ }, CALLBACK_TIMEOUT_MS);
9933
+ server.on("close", () => {
9934
+ clearTimeout(timeout);
9935
+ });
9936
+ server.listen(port, "127.0.0.1", () => {
9937
+ console.log(`Waiting for authentication callback on http://localhost:${port}/callback`);
9938
+ });
9939
+ });
10245
9940
  }
10246
- function ensureConfigDir() {
10247
- if (!existsSync5(CONFIG_DIR)) {
10248
- mkdirSync2(CONFIG_DIR, { recursive: true, mode: 448 });
10249
- }
9941
+ function buildSuccessPage() {
9942
+ return `
9943
+ <!DOCTYPE html>
9944
+ <html lang="en">
9945
+ <head>
9946
+ <meta charset="UTF-8" />
9947
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
9948
+ <title>Login Successful - Supatest CLI</title>
9949
+ <style>
9950
+ body {
9951
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
9952
+ display: flex;
9953
+ align-items: center;
9954
+ justify-content: center;
9955
+ height: 100vh;
9956
+ margin: 0;
9957
+ background: #fefefe;
9958
+ }
9959
+ .container {
9960
+ background: white;
9961
+ padding: 3rem 2rem;
9962
+ border-radius: 12px;
9963
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1), 0 1px 2px rgba(0, 0, 0, 0.06);
9964
+ border: 1px solid #e5e7eb;
9965
+ text-align: center;
9966
+ max-width: 400px;
9967
+ width: 90%;
9968
+ }
9969
+ .logo {
9970
+ margin: 0 auto 2rem;
9971
+ display: flex;
9972
+ align-items: center;
9973
+ justify-content: center;
9974
+ gap: 8px;
9975
+ }
9976
+ .logo img {
9977
+ width: 24px;
9978
+ height: 24px;
9979
+ border-radius: 6px;
9980
+ object-fit: contain;
9981
+ }
9982
+ .logo-text {
9983
+ font-weight: 600;
9984
+ font-size: 16px;
9985
+ color: inherit;
9986
+ }
9987
+ .success-icon {
9988
+ width: 64px;
9989
+ height: 64px;
9990
+ border-radius: 50%;
9991
+ background: #d1fae5;
9992
+ display: flex;
9993
+ align-items: center;
9994
+ justify-content: center;
9995
+ font-size: 32px;
9996
+ margin: 0 auto 1.5rem;
9997
+ }
9998
+ h1 {
9999
+ color: #10b981;
10000
+ margin: 0 0 1rem 0;
10001
+ font-size: 24px;
10002
+ font-weight: 600;
10003
+ }
10004
+ p {
10005
+ color: #666;
10006
+ margin: 0;
10007
+ line-height: 1.5;
10008
+ }
10009
+ @media (prefers-color-scheme: dark) {
10010
+ body {
10011
+ background: #1a1a1a;
10012
+ }
10013
+ .container {
10014
+ background: #1f1f1f;
10015
+ border-color: #333;
10016
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3), 0 1px 2px rgba(0, 0, 0, 0.2);
10017
+ }
10018
+ .success-icon {
10019
+ background: #064e3b;
10020
+ }
10021
+ h1 {
10022
+ color: #34d399;
10023
+ }
10024
+ p {
10025
+ color: #a3a3a3;
10026
+ }
10027
+ .logo-text {
10028
+ color: #e5e7eb;
10029
+ }
10030
+ }
10031
+ </style>
10032
+ </head>
10033
+ <body>
10034
+ <div class="container">
10035
+ <div class="logo">
10036
+ <img src="${FRONTEND_URL}/logo.png" alt="Supatest Logo" />
10037
+ <span class="logo-text">Supatest</span>
10038
+ </div>
10039
+ <div class="success-icon" role="img" aria-label="Success">\u2705</div>
10040
+ <h1>Login Successful!</h1>
10041
+ <p>You're now authenticated with Supatest CLI.</p>
10042
+ <p style="margin-top: 1rem;">You can close this window and return to your terminal.</p>
10043
+ </div>
10044
+ </body>
10045
+ </html>
10046
+ `;
10250
10047
  }
10251
- function saveToken(token, expiresAt) {
10252
- ensureConfigDir();
10253
- const payload = {
10254
- token,
10255
- expiresAt,
10256
- createdAt: (/* @__PURE__ */ new Date()).toISOString()
10257
- };
10258
- const stored = {
10259
- version: STORAGE_VERSION,
10260
- encryptedData: encrypt(JSON.stringify(payload))
10261
- };
10262
- const tokenFile = getTokenFilePath();
10263
- writeFileSync(tokenFile, JSON.stringify(stored, null, 2), { mode: 384 });
10048
+ function buildErrorPage(errorMessage) {
10049
+ return `
10050
+ <!DOCTYPE html>
10051
+ <html lang="en">
10052
+ <head>
10053
+ <meta charset="UTF-8" />
10054
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
10055
+ <title>Login Failed - Supatest CLI</title>
10056
+ <style>
10057
+ body {
10058
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
10059
+ display: flex;
10060
+ align-items: center;
10061
+ justify-content: center;
10062
+ height: 100vh;
10063
+ margin: 0;
10064
+ background: #fefefe;
10065
+ }
10066
+ .container {
10067
+ background: white;
10068
+ padding: 3rem 2rem;
10069
+ border-radius: 12px;
10070
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1), 0 1px 2px rgba(0, 0, 0, 0.06);
10071
+ border: 1px solid #e5e7eb;
10072
+ text-align: center;
10073
+ max-width: 400px;
10074
+ width: 90%;
10075
+ }
10076
+ .logo {
10077
+ width: 48px;
10078
+ height: 48px;
10079
+ margin: 0 auto 2rem;
10080
+ display: flex;
10081
+ align-items: center;
10082
+ justify-content: center;
10083
+ }
10084
+ .logo img {
10085
+ width: 48px;
10086
+ height: 48px;
10087
+ border-radius: 8px;
10088
+ object-fit: contain;
10089
+ }
10090
+ .error-icon {
10091
+ width: 64px;
10092
+ height: 64px;
10093
+ border-radius: 50%;
10094
+ background: #fee2e2;
10095
+ display: flex;
10096
+ align-items: center;
10097
+ justify-content: center;
10098
+ font-size: 32px;
10099
+ margin: 0 auto 1.5rem;
10100
+ }
10101
+ h1 {
10102
+ color: #dc2626;
10103
+ margin: 0 0 1rem 0;
10104
+ font-size: 24px;
10105
+ font-weight: 600;
10106
+ }
10107
+ p {
10108
+ color: #666;
10109
+ margin: 0;
10110
+ line-height: 1.5;
10111
+ }
10112
+ .brand {
10113
+ margin-top: 2rem;
10114
+ padding-top: 1.5rem;
10115
+ border-top: 1px solid #e5e7eb;
10116
+ color: #9333ff;
10117
+ font-weight: 600;
10118
+ font-size: 14px;
10119
+ }
10120
+ @media (prefers-color-scheme: dark) {
10121
+ body {
10122
+ background: #1a1a1a;
10123
+ }
10124
+ .container {
10125
+ background: #1f1f1f;
10126
+ border-color: #333;
10127
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3), 0 1px 2px rgba(0, 0, 0, 0.2);
10128
+ }
10129
+ .error-icon {
10130
+ background: #7f1d1d;
10131
+ }
10132
+ h1 {
10133
+ color: #f87171;
10134
+ }
10135
+ p {
10136
+ color: #a3a3a3;
10137
+ }
10138
+ .brand {
10139
+ border-top-color: #333;
10140
+ color: #a855f7;
10141
+ }
10142
+ }
10143
+ </style>
10144
+ </head>
10145
+ <body>
10146
+ <div class="container">
10147
+ <div class="logo">
10148
+ <img src="${FRONTEND_URL}/logo.png" alt="Supatest Logo" />
10149
+ <span class="logo-text">Supatest</span>
10150
+ </div>
10151
+ <div class="error-icon" role="img" aria-label="Error">\u274C</div>
10152
+ <h1>Login Failed</h1>
10153
+ <p>${escapeHtml(errorMessage)}</p>
10154
+ <p style="margin-top: 1rem;">You can close this window and try again.</p>
10155
+ </div>
10156
+ </body>
10157
+ </html>
10158
+ `;
10264
10159
  }
10265
- function loadToken() {
10266
- const tokenFile = getTokenFilePath();
10267
- if (!existsSync5(tokenFile)) {
10268
- return null;
10160
+ async function loginCommand() {
10161
+ console.log("\nAuthenticating with Supatest...\n");
10162
+ const state = generateState();
10163
+ const loginPromise = startCallbackServer(CLI_LOGIN_PORT, state);
10164
+ const loginUrl = `${FRONTEND_URL}/cli-login?port=${CLI_LOGIN_PORT}&state=${state}`;
10165
+ console.log(`Opening browser to: ${loginUrl}`);
10166
+ console.log("\nIf your browser doesn't open automatically, please visit the URL above.\n");
10167
+ try {
10168
+ openBrowser(loginUrl);
10169
+ } catch (error) {
10170
+ console.warn("Failed to open browser automatically:", error);
10171
+ console.log(`
10172
+ Please manually open this URL in your browser:
10173
+ ${loginUrl}
10174
+ `);
10269
10175
  }
10270
10176
  try {
10271
- const data = readFileSync4(tokenFile, "utf8");
10272
- const stored = JSON.parse(data);
10273
- let payload;
10274
- if (isV2Format(stored)) {
10275
- payload = JSON.parse(decrypt(stored.encryptedData));
10276
- } else {
10277
- payload = stored;
10278
- }
10279
- if (payload.expiresAt) {
10280
- const expiresAt = new Date(payload.expiresAt);
10281
- if (expiresAt < /* @__PURE__ */ new Date()) {
10282
- console.warn("CLI token has expired. Please run 'supatest login' again.");
10283
- return null;
10284
- }
10285
- }
10286
- return payload.token;
10177
+ const result = await loginPromise;
10178
+ console.log("\n\u2705 Login successful!\n");
10179
+ return result;
10287
10180
  } catch (error) {
10288
10181
  const err = error;
10289
- if (err.message?.includes("Invalid encrypted data format") || err.message?.includes("Unsupported state or unable to authenticate data")) {
10290
- try {
10291
- unlinkSync2(tokenFile);
10292
- } catch {
10293
- }
10182
+ if (err.code === "EADDRINUSE") {
10183
+ console.error("\n\u274C Login failed: Something went wrong.");
10184
+ console.error(" Please restart the CLI and try again.\n");
10185
+ } else {
10186
+ console.error("\n\u274C Login failed:", error.message, "\n");
10294
10187
  }
10295
- return null;
10296
- }
10297
- }
10298
- function removeToken() {
10299
- const tokenFile = getTokenFilePath();
10300
- if (existsSync5(tokenFile)) {
10301
- unlinkSync2(tokenFile);
10188
+ throw error;
10302
10189
  }
10303
10190
  }
10304
- var CONFIG_DIR, PRODUCTION_API_URL, STORAGE_VERSION, TOKEN_FILE;
10305
- var init_token_storage = __esm({
10306
- "src/utils/token-storage.ts"() {
10191
+ var CLI_LOGIN_PORT, FRONTEND_URL, API_URL, CALLBACK_TIMEOUT_MS, STATE_LENGTH;
10192
+ var init_login = __esm({
10193
+ "src/commands/login.ts"() {
10307
10194
  "use strict";
10308
- init_encryption();
10309
- CONFIG_DIR = join8(homedir6(), ".supatest");
10310
- PRODUCTION_API_URL = "https://code-api.supatest.ai";
10311
- STORAGE_VERSION = 2;
10312
- TOKEN_FILE = join8(CONFIG_DIR, "token.json");
10195
+ CLI_LOGIN_PORT = 8420;
10196
+ FRONTEND_URL = process.env.SUPATEST_FRONTEND_URL || "https://code.supatest.ai";
10197
+ API_URL = process.env.SUPATEST_API_URL || "https://code-api.supatest.ai";
10198
+ CALLBACK_TIMEOUT_MS = 3e5;
10199
+ STATE_LENGTH = 32;
10313
10200
  }
10314
10201
  });
10315
10202
 
10316
- // src/core/message-bridge.ts
10317
- var MessageBridge;
10318
- var init_message_bridge = __esm({
10319
- "src/core/message-bridge.ts"() {
10203
+ // src/utils/claude-oauth.ts
10204
+ var claude_oauth_exports = {};
10205
+ __export(claude_oauth_exports, {
10206
+ ClaudeOAuthService: () => ClaudeOAuthService
10207
+ });
10208
+ import { spawn as spawn4 } from "child_process";
10209
+ import { createHash, randomBytes } from "crypto";
10210
+ import http2 from "http";
10211
+ import { platform as platform2 } from "os";
10212
+ var OAUTH_CONFIG, CALLBACK_PORT, CALLBACK_TIMEOUT_MS2, ClaudeOAuthService;
10213
+ var init_claude_oauth = __esm({
10214
+ "src/utils/claude-oauth.ts"() {
10320
10215
  "use strict";
10321
- MessageBridge = class {
10322
- queue = [];
10323
- resolvers = [];
10324
- closed = false;
10325
- sessionId;
10326
- constructor(sessionId) {
10327
- this.sessionId = sessionId;
10216
+ OAUTH_CONFIG = {
10217
+ clientId: "9d1c250a-e61b-44d9-88ed-5944d1962f5e",
10218
+ // Claude Code's client ID
10219
+ authorizationEndpoint: "https://claude.ai/oauth/authorize",
10220
+ tokenEndpoint: "https://console.anthropic.com/v1/oauth/token",
10221
+ redirectUri: "http://localhost:8421/callback",
10222
+ // Local callback for CLI
10223
+ scopes: ["user:inference", "user:profile", "org:create_api_key"]
10224
+ };
10225
+ CALLBACK_PORT = 8421;
10226
+ CALLBACK_TIMEOUT_MS2 = 3e5;
10227
+ ClaudeOAuthService = class _ClaudeOAuthService {
10228
+ secretStorage;
10229
+ static TOKEN_REFRESH_BUFFER_MS = 5 * 60 * 1e3;
10230
+ // 5 minutes
10231
+ pendingCodeVerifier = null;
10232
+ // Store code verifier for PKCE
10233
+ constructor(secretStorage) {
10234
+ this.secretStorage = secretStorage;
10235
+ }
10236
+ /**
10237
+ * Starts the OAuth authorization flow
10238
+ * Opens the default browser for user authentication
10239
+ * Returns after successful authentication
10240
+ */
10241
+ async authorize() {
10242
+ try {
10243
+ const state = this.generateRandomState();
10244
+ const pkce = this.generatePKCEChallenge();
10245
+ this.pendingCodeVerifier = pkce.codeVerifier;
10246
+ const authUrl = this.buildAuthorizationUrl(state, pkce.codeChallenge);
10247
+ console.log("\nAuthenticating with Claude...\n");
10248
+ console.log(`Opening browser to: ${authUrl}
10249
+ `);
10250
+ const tokenPromise = this.startCallbackServer(CALLBACK_PORT, state);
10251
+ try {
10252
+ this.openBrowser(authUrl);
10253
+ } catch (error) {
10254
+ console.warn("Failed to open browser automatically:", error);
10255
+ console.log(`
10256
+ Please manually open this URL in your browser:
10257
+ ${authUrl}
10258
+ `);
10259
+ }
10260
+ await tokenPromise;
10261
+ console.log("\n\u2705 Successfully authenticated with Claude!\n");
10262
+ return { success: true };
10263
+ } catch (error) {
10264
+ this.pendingCodeVerifier = null;
10265
+ return {
10266
+ success: false,
10267
+ error: error instanceof Error ? error.message : "Authentication failed"
10268
+ };
10269
+ }
10270
+ }
10271
+ /**
10272
+ * Start local HTTP server to receive OAuth callback
10273
+ */
10274
+ startCallbackServer(port, expectedState) {
10275
+ return new Promise((resolve2, reject) => {
10276
+ const server = http2.createServer(async (req, res) => {
10277
+ if (req.method !== "GET" || !req.url?.startsWith("/callback")) {
10278
+ res.writeHead(404);
10279
+ res.end("Not Found");
10280
+ return;
10281
+ }
10282
+ const url = new URL(req.url, `http://localhost:${port}`);
10283
+ const code = url.searchParams.get("code");
10284
+ const returnedState = url.searchParams.get("state");
10285
+ const error = url.searchParams.get("error");
10286
+ if (error) {
10287
+ res.writeHead(200, { "Content-Type": "text/html" });
10288
+ res.end(this.buildErrorPage(error));
10289
+ server.close();
10290
+ reject(new Error(`OAuth error: ${error}`));
10291
+ return;
10292
+ }
10293
+ if (returnedState !== expectedState) {
10294
+ const errorMsg = "Security error: state parameter mismatch";
10295
+ res.writeHead(200, { "Content-Type": "text/html" });
10296
+ res.end(this.buildErrorPage(errorMsg));
10297
+ server.close();
10298
+ reject(new Error(errorMsg));
10299
+ return;
10300
+ }
10301
+ if (!code) {
10302
+ const errorMsg = "No authorization code received";
10303
+ res.writeHead(400, { "Content-Type": "text/html" });
10304
+ res.end(this.buildErrorPage(errorMsg));
10305
+ server.close();
10306
+ reject(new Error(errorMsg));
10307
+ return;
10308
+ }
10309
+ try {
10310
+ await this.submitAuthCode(code, returnedState);
10311
+ res.writeHead(200, { "Content-Type": "text/html" });
10312
+ res.end(this.buildSuccessPage());
10313
+ server.close();
10314
+ resolve2();
10315
+ } catch (err) {
10316
+ const errorMsg = err instanceof Error ? err.message : "Token exchange failed";
10317
+ res.writeHead(200, { "Content-Type": "text/html" });
10318
+ res.end(this.buildErrorPage(errorMsg));
10319
+ server.close();
10320
+ reject(err);
10321
+ }
10322
+ });
10323
+ server.on("error", (error) => {
10324
+ if (error.code === "EADDRINUSE") {
10325
+ reject(new Error("Port already in use. Please try again."));
10326
+ } else {
10327
+ reject(error);
10328
+ }
10329
+ });
10330
+ const timeout = setTimeout(() => {
10331
+ server.close();
10332
+ reject(new Error("Authentication timeout - no response received after 5 minutes"));
10333
+ }, CALLBACK_TIMEOUT_MS2);
10334
+ server.on("close", () => {
10335
+ clearTimeout(timeout);
10336
+ });
10337
+ server.listen(port, "127.0.0.1", () => {
10338
+ console.log(`Waiting for authentication callback on http://localhost:${port}/callback`);
10339
+ });
10340
+ });
10341
+ }
10342
+ /**
10343
+ * Submits the authorization code and exchanges it for tokens
10344
+ */
10345
+ async submitAuthCode(code, state) {
10346
+ const tokens = await this.exchangeCodeForTokens(code, state);
10347
+ await this.saveTokens(tokens);
10348
+ return tokens;
10349
+ }
10350
+ /**
10351
+ * Exchanges authorization code for access and refresh tokens
10352
+ */
10353
+ async exchangeCodeForTokens(code, state) {
10354
+ if (!this.pendingCodeVerifier) {
10355
+ throw new Error("No PKCE code verifier found. Please start the auth flow first.");
10356
+ }
10357
+ const body = {
10358
+ grant_type: "authorization_code",
10359
+ code,
10360
+ state,
10361
+ // Non-standard: state in body
10362
+ redirect_uri: OAUTH_CONFIG.redirectUri,
10363
+ client_id: OAUTH_CONFIG.clientId,
10364
+ code_verifier: this.pendingCodeVerifier
10365
+ // PKCE verifier
10366
+ };
10367
+ const response = await fetch(OAUTH_CONFIG.tokenEndpoint, {
10368
+ method: "POST",
10369
+ headers: {
10370
+ "Content-Type": "application/json"
10371
+ // Non-standard: JSON instead of form-encoded
10372
+ },
10373
+ body: JSON.stringify(body)
10374
+ });
10375
+ this.pendingCodeVerifier = null;
10376
+ if (!response.ok) {
10377
+ const error = await response.text();
10378
+ throw new Error(`Token exchange failed: ${error}`);
10379
+ }
10380
+ const data = await response.json();
10381
+ return {
10382
+ accessToken: data.access_token,
10383
+ refreshToken: data.refresh_token,
10384
+ expiresAt: Date.now() + data.expires_in * 1e3
10385
+ };
10386
+ }
10387
+ /**
10388
+ * Refreshes the access token using the refresh token
10389
+ */
10390
+ async refreshTokens() {
10391
+ const tokens = await this.getTokens();
10392
+ if (!tokens) {
10393
+ throw new Error("No tokens found to refresh");
10394
+ }
10395
+ const body = {
10396
+ grant_type: "refresh_token",
10397
+ refresh_token: tokens.refreshToken,
10398
+ client_id: OAUTH_CONFIG.clientId
10399
+ };
10400
+ const response = await fetch(OAUTH_CONFIG.tokenEndpoint, {
10401
+ method: "POST",
10402
+ headers: {
10403
+ "Content-Type": "application/json"
10404
+ },
10405
+ body: JSON.stringify(body)
10406
+ });
10407
+ if (!response.ok) {
10408
+ const error = await response.text();
10409
+ throw new Error(`Token refresh failed: ${error}`);
10410
+ }
10411
+ const data = await response.json();
10412
+ const newTokens = {
10413
+ accessToken: data.access_token,
10414
+ refreshToken: data.refresh_token,
10415
+ expiresAt: Date.now() + data.expires_in * 1e3
10416
+ };
10417
+ await this.saveTokens(newTokens);
10418
+ return newTokens;
10328
10419
  }
10329
10420
  /**
10330
- * Update the session ID (useful when session is created after bridge).
10421
+ * Gets the current access token, refreshing if necessary
10331
10422
  */
10332
- setSessionId(sessionId) {
10333
- this.sessionId = sessionId;
10423
+ async getAccessToken() {
10424
+ const tokens = await this.getTokens();
10425
+ if (!tokens) {
10426
+ return null;
10427
+ }
10428
+ if (Date.now() > tokens.expiresAt - _ClaudeOAuthService.TOKEN_REFRESH_BUFFER_MS) {
10429
+ try {
10430
+ const refreshedTokens = await this.refreshTokens();
10431
+ return refreshedTokens.accessToken;
10432
+ } catch (error) {
10433
+ console.warn("Token refresh failed:", error);
10434
+ return null;
10435
+ }
10436
+ }
10437
+ return tokens.accessToken;
10334
10438
  }
10335
10439
  /**
10336
- * Push a user message to be injected into the session.
10337
- * Call this from the UI when user submits a message during agent execution.
10440
+ * Gets the stored tokens
10338
10441
  */
10339
- push(text) {
10340
- if (this.closed) {
10341
- console.warn("[MessageBridge] Cannot push to closed bridge");
10342
- return;
10343
- }
10344
- const message = {
10345
- type: "user",
10346
- message: {
10347
- role: "user",
10348
- content: [{ type: "text", text }]
10349
- },
10350
- parent_tool_use_id: null,
10351
- session_id: this.sessionId
10352
- };
10353
- const resolver = this.resolvers.shift();
10354
- if (resolver) {
10355
- resolver({ value: message, done: false });
10356
- } else {
10357
- this.queue.push(message);
10442
+ async getTokens() {
10443
+ try {
10444
+ const accessToken = await this.secretStorage.getSecret("claude_oauth_access_token");
10445
+ const refreshToken = await this.secretStorage.getSecret("claude_oauth_refresh_token");
10446
+ const expiresAt = await this.secretStorage.getSecret("claude_oauth_expires_at");
10447
+ if (!accessToken || !refreshToken || !expiresAt) {
10448
+ return null;
10449
+ }
10450
+ return {
10451
+ accessToken,
10452
+ refreshToken,
10453
+ expiresAt: parseInt(expiresAt, 10)
10454
+ };
10455
+ } catch (error) {
10456
+ console.error("Failed to get OAuth tokens:", error);
10457
+ return null;
10358
10458
  }
10359
10459
  }
10360
10460
  /**
10361
- * Check if there are pending messages in the queue.
10461
+ * Saves OAuth tokens to secure storage
10362
10462
  */
10363
- hasPending() {
10364
- return this.queue.length > 0;
10463
+ async saveTokens(tokens) {
10464
+ await this.secretStorage.setSecret("claude_oauth_access_token", tokens.accessToken);
10465
+ await this.secretStorage.setSecret("claude_oauth_refresh_token", tokens.refreshToken);
10466
+ await this.secretStorage.setSecret("claude_oauth_expires_at", tokens.expiresAt.toString());
10365
10467
  }
10366
10468
  /**
10367
- * Close the bridge. No more messages will be accepted.
10368
- * Any pending async iterators will receive done: true.
10469
+ * Deletes stored OAuth tokens
10369
10470
  */
10370
- close() {
10371
- if (this.closed) return;
10372
- this.closed = true;
10373
- for (const resolver of this.resolvers) {
10374
- resolver({ value: void 0, done: true });
10375
- }
10376
- this.resolvers = [];
10471
+ async deleteTokens() {
10472
+ await this.secretStorage.deleteSecret("claude_oauth_access_token");
10473
+ await this.secretStorage.deleteSecret("claude_oauth_refresh_token");
10474
+ await this.secretStorage.deleteSecret("claude_oauth_expires_at");
10377
10475
  }
10378
10476
  /**
10379
- * Check if the bridge is closed.
10477
+ * Checks if user is authenticated via OAuth
10380
10478
  */
10381
- isClosed() {
10382
- return this.closed;
10479
+ async isAuthenticated() {
10480
+ const tokens = await this.getTokens();
10481
+ return tokens !== null;
10383
10482
  }
10384
- [Symbol.asyncIterator]() {
10385
- return {
10386
- next: () => {
10387
- const queued = this.queue.shift();
10388
- if (queued) {
10389
- return Promise.resolve({ value: queued, done: false });
10390
- }
10391
- if (this.closed) {
10392
- return Promise.resolve({
10393
- value: void 0,
10394
- done: true
10395
- });
10396
- }
10397
- return new Promise((resolve2) => {
10398
- this.resolvers.push(resolve2);
10399
- });
10483
+ /**
10484
+ * Gets the current OAuth authentication status
10485
+ */
10486
+ async getStatus() {
10487
+ try {
10488
+ const tokens = await this.getTokens();
10489
+ if (!tokens) {
10490
+ return { isAuthenticated: false };
10400
10491
  }
10401
- };
10402
- }
10403
- };
10404
- }
10405
- });
10406
-
10407
- // src/commands/login.ts
10408
- import { spawn as spawn4 } from "child_process";
10409
- import crypto3 from "crypto";
10410
- import http2 from "http";
10411
- import { platform as platform2 } from "os";
10412
- function escapeHtml(text) {
10413
- return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
10414
- }
10415
- function generateState() {
10416
- return crypto3.randomBytes(STATE_LENGTH).toString("hex");
10417
- }
10418
- async function exchangeCodeForToken(code, state) {
10419
- const response = await fetch(`${API_URL}/web/auth/cli-token-exchange`, {
10420
- method: "POST",
10421
- headers: { "Content-Type": "application/json" },
10422
- body: JSON.stringify({ code, state })
10423
- });
10424
- if (!response.ok) {
10425
- const error = await response.json();
10426
- throw new Error(error.error || "Failed to exchange code for token");
10427
- }
10428
- const data = await response.json();
10429
- return { token: data.token, expiresAt: data.expiresAt };
10430
- }
10431
- function openBrowser(url) {
10432
- const os3 = platform2();
10433
- let command;
10434
- let args;
10435
- switch (os3) {
10436
- case "darwin":
10437
- command = "open";
10438
- args = [url];
10439
- break;
10440
- case "win32":
10441
- command = "start";
10442
- args = ["", url];
10443
- break;
10444
- default:
10445
- command = "xdg-open";
10446
- args = [url];
10447
- }
10448
- const options = { detached: true, stdio: "ignore", shell: os3 === "win32" };
10449
- spawn4(command, args, options).unref();
10450
- }
10451
- function startCallbackServer(port, expectedState) {
10452
- return new Promise((resolve2, reject) => {
10453
- const server = http2.createServer(async (req, res) => {
10454
- if (req.method !== "GET" || !req.url?.startsWith("/callback")) {
10455
- res.writeHead(404);
10456
- res.end("Not Found");
10457
- return;
10458
- }
10459
- const url = new URL(req.url, `http://localhost:${port}`);
10460
- const code = url.searchParams.get("code");
10461
- const state = url.searchParams.get("state");
10462
- const error = url.searchParams.get("error");
10463
- if (error) {
10464
- res.writeHead(200, { "Content-Type": "text/html" });
10465
- res.end(buildErrorPage(error));
10466
- server.close();
10467
- reject(new Error(error));
10468
- return;
10492
+ return {
10493
+ isAuthenticated: true,
10494
+ expiresAt: tokens.expiresAt
10495
+ };
10496
+ } catch (error) {
10497
+ return {
10498
+ isAuthenticated: false,
10499
+ error: error instanceof Error ? error.message : "Failed to get OAuth status"
10500
+ };
10501
+ }
10469
10502
  }
10470
- if (state !== expectedState) {
10471
- const errorMsg = "Security error: state parameter mismatch";
10472
- res.writeHead(200, { "Content-Type": "text/html" });
10473
- res.end(buildErrorPage(errorMsg));
10474
- server.close();
10475
- reject(new Error(errorMsg));
10476
- return;
10503
+ /**
10504
+ * Builds the authorization URL with all required parameters
10505
+ */
10506
+ buildAuthorizationUrl(state, codeChallenge) {
10507
+ const params = new URLSearchParams({
10508
+ response_type: "code",
10509
+ client_id: OAUTH_CONFIG.clientId,
10510
+ redirect_uri: OAUTH_CONFIG.redirectUri,
10511
+ scope: OAUTH_CONFIG.scopes.join(" "),
10512
+ state
10513
+ });
10514
+ if (codeChallenge) {
10515
+ params.set("code_challenge", codeChallenge);
10516
+ params.set("code_challenge_method", "S256");
10517
+ }
10518
+ return `${OAUTH_CONFIG.authorizationEndpoint}?${params.toString()}`;
10477
10519
  }
10478
- if (!code) {
10479
- const errorMsg = "No authorization code received";
10480
- res.writeHead(400, { "Content-Type": "text/html" });
10481
- res.end(buildErrorPage(errorMsg));
10482
- server.close();
10483
- reject(new Error(errorMsg));
10484
- return;
10520
+ /**
10521
+ * Generates a random state string for CSRF protection
10522
+ */
10523
+ generateRandomState() {
10524
+ return Array.from(crypto.getRandomValues(new Uint8Array(32))).map((b) => b.toString(16).padStart(2, "0")).join("");
10485
10525
  }
10486
- try {
10487
- const result = await exchangeCodeForToken(code, state);
10488
- res.writeHead(200, { "Content-Type": "text/html" });
10489
- res.end(buildSuccessPage());
10490
- server.close();
10491
- resolve2(result);
10492
- } catch (err) {
10493
- const errorMsg = err instanceof Error ? err.message : "Token exchange failed";
10494
- res.writeHead(200, { "Content-Type": "text/html" });
10495
- res.end(buildErrorPage(errorMsg));
10496
- server.close();
10497
- reject(err);
10526
+ /**
10527
+ * Generates PKCE code verifier and challenge
10528
+ * PKCE (Proof Key for Code Exchange) adds security to OAuth for public clients
10529
+ */
10530
+ generatePKCEChallenge() {
10531
+ const codeVerifier = randomBytes(32).toString("base64url");
10532
+ const hash = createHash("sha256").update(codeVerifier).digest("base64url");
10533
+ return {
10534
+ codeVerifier,
10535
+ codeChallenge: hash
10536
+ };
10498
10537
  }
10499
- });
10500
- server.on("error", (error) => {
10501
- if (error.code === "EADDRINUSE") {
10502
- const portError = new Error(
10503
- "Something went wrong. Please restart the CLI and try again."
10504
- );
10505
- portError.code = "EADDRINUSE";
10506
- reject(portError);
10507
- } else {
10508
- reject(error);
10538
+ /**
10539
+ * Open a URL in the default browser cross-platform
10540
+ */
10541
+ openBrowser(url) {
10542
+ const os3 = platform2();
10543
+ let command;
10544
+ let args;
10545
+ switch (os3) {
10546
+ case "darwin":
10547
+ command = "open";
10548
+ args = [url];
10549
+ break;
10550
+ case "win32":
10551
+ command = "start";
10552
+ args = ["", url];
10553
+ break;
10554
+ default:
10555
+ command = "xdg-open";
10556
+ args = [url];
10557
+ break;
10558
+ }
10559
+ const options = { detached: true, stdio: "ignore", shell: os3 === "win32" };
10560
+ spawn4(command, args, options).unref();
10509
10561
  }
10510
- });
10511
- const timeout = setTimeout(() => {
10512
- server.close();
10513
- reject(new Error("Login timeout - no response received after 5 minutes"));
10514
- }, CALLBACK_TIMEOUT_MS2);
10515
- server.on("close", () => {
10516
- clearTimeout(timeout);
10517
- });
10518
- server.listen(port, "127.0.0.1", () => {
10519
- console.log(`Waiting for authentication callback on http://localhost:${port}/callback`);
10520
- });
10521
- });
10522
- }
10523
- function buildSuccessPage() {
10524
- return `
10525
- <!DOCTYPE html>
10526
- <html lang="en">
10527
- <head>
10528
- <meta charset="UTF-8" />
10529
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
10530
- <title>Login Successful - Supatest CLI</title>
10531
- <style>
10532
- body {
10533
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
10534
- display: flex;
10535
- align-items: center;
10536
- justify-content: center;
10537
- height: 100vh;
10538
- margin: 0;
10539
- background: #fefefe;
10540
- }
10541
- .container {
10542
- background: white;
10543
- padding: 3rem 2rem;
10544
- border-radius: 12px;
10545
- box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1), 0 1px 2px rgba(0, 0, 0, 0.06);
10546
- border: 1px solid #e5e7eb;
10547
- text-align: center;
10548
- max-width: 400px;
10549
- width: 90%;
10550
- }
10551
- .logo {
10552
- margin: 0 auto 2rem;
10553
- display: flex;
10554
- align-items: center;
10555
- justify-content: center;
10556
- gap: 8px;
10557
- }
10558
- .logo img {
10559
- width: 24px;
10560
- height: 24px;
10561
- border-radius: 6px;
10562
- object-fit: contain;
10563
- }
10564
- .logo-text {
10565
- font-weight: 600;
10566
- font-size: 16px;
10567
- color: inherit;
10568
- }
10569
- .success-icon {
10570
- width: 64px;
10571
- height: 64px;
10572
- border-radius: 50%;
10573
- background: #d1fae5;
10574
- display: flex;
10575
- align-items: center;
10576
- justify-content: center;
10577
- font-size: 32px;
10578
- margin: 0 auto 1.5rem;
10579
- }
10580
- h1 {
10581
- color: #10b981;
10582
- margin: 0 0 1rem 0;
10583
- font-size: 24px;
10584
- font-weight: 600;
10585
- }
10586
- p {
10587
- color: #666;
10588
- margin: 0;
10589
- line-height: 1.5;
10590
- }
10591
- @media (prefers-color-scheme: dark) {
10562
+ /**
10563
+ * Build success HTML page
10564
+ */
10565
+ buildSuccessPage() {
10566
+ return `
10567
+ <!DOCTYPE html>
10568
+ <html lang="en">
10569
+ <head>
10570
+ <meta charset="UTF-8" />
10571
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
10572
+ <title>Authentication Successful - Supatest CLI</title>
10573
+ <style>
10592
10574
  body {
10593
- background: #1a1a1a;
10575
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
10576
+ display: flex;
10577
+ align-items: center;
10578
+ justify-content: center;
10579
+ height: 100vh;
10580
+ margin: 0;
10581
+ background: #fefefe;
10594
10582
  }
10595
10583
  .container {
10596
- background: #1f1f1f;
10597
- border-color: #333;
10598
- box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3), 0 1px 2px rgba(0, 0, 0, 0.2);
10584
+ background: white;
10585
+ padding: 3rem 2rem;
10586
+ border-radius: 12px;
10587
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
10588
+ border: 1px solid #e5e7eb;
10589
+ text-align: center;
10590
+ max-width: 400px;
10599
10591
  }
10600
10592
  .success-icon {
10601
- background: #064e3b;
10593
+ font-size: 48px;
10594
+ margin-bottom: 1rem;
10602
10595
  }
10603
10596
  h1 {
10604
- color: #34d399;
10597
+ color: #10b981;
10598
+ margin: 0 0 1rem 0;
10599
+ font-size: 24px;
10605
10600
  }
10606
10601
  p {
10607
- color: #a3a3a3;
10608
- }
10609
- .logo-text {
10610
- color: #e5e7eb;
10602
+ color: #666;
10603
+ margin: 0;
10604
+ line-height: 1.5;
10611
10605
  }
10612
- }
10613
- </style>
10614
- </head>
10615
- <body>
10616
- <div class="container">
10617
- <div class="logo">
10618
- <img src="${FRONTEND_URL}/logo.png" alt="Supatest Logo" />
10619
- <span class="logo-text">Supatest</span>
10620
- </div>
10621
- <div class="success-icon" role="img" aria-label="Success">\u2705</div>
10622
- <h1>Login Successful!</h1>
10623
- <p>You're now authenticated with Supatest CLI.</p>
10624
- <p style="margin-top: 1rem;">You can close this window and return to your terminal.</p>
10625
- </div>
10626
- </body>
10627
- </html>
10628
- `;
10629
- }
10630
- function buildErrorPage(errorMessage) {
10631
- return `
10632
- <!DOCTYPE html>
10633
- <html lang="en">
10634
- <head>
10635
- <meta charset="UTF-8" />
10636
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
10637
- <title>Login Failed - Supatest CLI</title>
10638
- <style>
10639
- body {
10640
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
10641
- display: flex;
10642
- align-items: center;
10643
- justify-content: center;
10644
- height: 100vh;
10645
- margin: 0;
10646
- background: #fefefe;
10647
- }
10648
- .container {
10649
- background: white;
10650
- padding: 3rem 2rem;
10651
- border-radius: 12px;
10652
- box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1), 0 1px 2px rgba(0, 0, 0, 0.06);
10653
- border: 1px solid #e5e7eb;
10654
- text-align: center;
10655
- max-width: 400px;
10656
- width: 90%;
10657
- }
10658
- .logo {
10659
- width: 48px;
10660
- height: 48px;
10661
- margin: 0 auto 2rem;
10662
- display: flex;
10663
- align-items: center;
10664
- justify-content: center;
10665
- }
10666
- .logo img {
10667
- width: 48px;
10668
- height: 48px;
10669
- border-radius: 8px;
10670
- object-fit: contain;
10671
- }
10672
- .error-icon {
10673
- width: 64px;
10674
- height: 64px;
10675
- border-radius: 50%;
10676
- background: #fee2e2;
10677
- display: flex;
10678
- align-items: center;
10679
- justify-content: center;
10680
- font-size: 32px;
10681
- margin: 0 auto 1.5rem;
10682
- }
10683
- h1 {
10684
- color: #dc2626;
10685
- margin: 0 0 1rem 0;
10686
- font-size: 24px;
10687
- font-weight: 600;
10688
- }
10689
- p {
10690
- color: #666;
10691
- margin: 0;
10692
- line-height: 1.5;
10693
- }
10694
- .brand {
10695
- margin-top: 2rem;
10696
- padding-top: 1.5rem;
10697
- border-top: 1px solid #e5e7eb;
10698
- color: #9333ff;
10699
- font-weight: 600;
10700
- font-size: 14px;
10701
- }
10702
- @media (prefers-color-scheme: dark) {
10606
+ </style>
10607
+ </head>
10608
+ <body>
10609
+ <div class="container">
10610
+ <div class="success-icon">\u2705</div>
10611
+ <h1>Authentication Successful!</h1>
10612
+ <p>You're now authenticated with Claude.</p>
10613
+ <p style="margin-top: 1rem;">You can close this window and return to your terminal.</p>
10614
+ </div>
10615
+ </body>
10616
+ </html>
10617
+ `;
10618
+ }
10619
+ /**
10620
+ * Build error HTML page
10621
+ */
10622
+ buildErrorPage(errorMessage) {
10623
+ const escapedError = errorMessage.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
10624
+ return `
10625
+ <!DOCTYPE html>
10626
+ <html lang="en">
10627
+ <head>
10628
+ <meta charset="UTF-8" />
10629
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
10630
+ <title>Authentication Failed - Supatest CLI</title>
10631
+ <style>
10703
10632
  body {
10704
- background: #1a1a1a;
10633
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
10634
+ display: flex;
10635
+ align-items: center;
10636
+ justify-content: center;
10637
+ height: 100vh;
10638
+ margin: 0;
10639
+ background: #fefefe;
10705
10640
  }
10706
10641
  .container {
10707
- background: #1f1f1f;
10708
- border-color: #333;
10709
- box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3), 0 1px 2px rgba(0, 0, 0, 0.2);
10642
+ background: white;
10643
+ padding: 3rem 2rem;
10644
+ border-radius: 12px;
10645
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
10646
+ border: 1px solid #e5e7eb;
10647
+ text-align: center;
10648
+ max-width: 400px;
10710
10649
  }
10711
10650
  .error-icon {
10712
- background: #7f1d1d;
10651
+ font-size: 48px;
10652
+ margin-bottom: 1rem;
10713
10653
  }
10714
10654
  h1 {
10715
- color: #f87171;
10655
+ color: #dc2626;
10656
+ margin: 0 0 1rem 0;
10657
+ font-size: 24px;
10716
10658
  }
10717
10659
  p {
10718
- color: #a3a3a3;
10719
- }
10720
- .brand {
10721
- border-top-color: #333;
10722
- color: #a855f7;
10660
+ color: #666;
10661
+ margin: 0;
10662
+ line-height: 1.5;
10723
10663
  }
10724
- }
10725
- </style>
10726
- </head>
10727
- <body>
10728
- <div class="container">
10729
- <div class="logo">
10730
- <img src="${FRONTEND_URL}/logo.png" alt="Supatest Logo" />
10731
- <span class="logo-text">Supatest</span>
10664
+ </style>
10665
+ </head>
10666
+ <body>
10667
+ <div class="container">
10668
+ <div class="error-icon">\u274C</div>
10669
+ <h1>Authentication Failed</h1>
10670
+ <p>${escapedError}</p>
10671
+ <p style="margin-top: 1rem;">You can close this window and try again.</p>
10732
10672
  </div>
10733
- <div class="error-icon" role="img" aria-label="Error">\u274C</div>
10734
- <h1>Login Failed</h1>
10735
- <p>${escapeHtml(errorMessage)}</p>
10736
- <p style="margin-top: 1rem;">You can close this window and try again.</p>
10737
- </div>
10738
- </body>
10739
- </html>
10740
- `;
10741
- }
10742
- async function loginCommand() {
10743
- console.log("\nAuthenticating with Supatest...\n");
10744
- const state = generateState();
10745
- const loginPromise = startCallbackServer(CLI_LOGIN_PORT, state);
10746
- const loginUrl = `${FRONTEND_URL}/cli-login?port=${CLI_LOGIN_PORT}&state=${state}`;
10747
- console.log(`Opening browser to: ${loginUrl}`);
10748
- console.log("\nIf your browser doesn't open automatically, please visit the URL above.\n");
10749
- try {
10750
- openBrowser(loginUrl);
10751
- } catch (error) {
10752
- console.warn("Failed to open browser automatically:", error);
10753
- console.log(`
10754
- Please manually open this URL in your browser:
10755
- ${loginUrl}
10756
- `);
10757
- }
10758
- try {
10759
- const result = await loginPromise;
10760
- console.log("\n\u2705 Login successful!\n");
10761
- return result;
10762
- } catch (error) {
10763
- const err = error;
10764
- if (err.code === "EADDRINUSE") {
10765
- console.error("\n\u274C Login failed: Something went wrong.");
10766
- console.error(" Please restart the CLI and try again.\n");
10767
- } else {
10768
- console.error("\n\u274C Login failed:", error.message, "\n");
10769
- }
10770
- throw error;
10673
+ </body>
10674
+ </html>
10675
+ `;
10676
+ }
10677
+ };
10771
10678
  }
10679
+ });
10680
+
10681
+ // src/utils/secret-storage.ts
10682
+ var secret_storage_exports = {};
10683
+ __export(secret_storage_exports, {
10684
+ deleteSecret: () => deleteSecret,
10685
+ getSecret: () => getSecret,
10686
+ getSecretStorage: () => getSecretStorage,
10687
+ listSecrets: () => listSecrets,
10688
+ setSecret: () => setSecret
10689
+ });
10690
+ import { promises as fs4 } from "fs";
10691
+ import { homedir as homedir6 } from "os";
10692
+ import { dirname as dirname2, join as join8 } from "path";
10693
+ async function getSecret(key) {
10694
+ return storage.getSecret(key);
10772
10695
  }
10773
- var CLI_LOGIN_PORT, FRONTEND_URL, API_URL, CALLBACK_TIMEOUT_MS2, STATE_LENGTH;
10774
- var init_login = __esm({
10775
- "src/commands/login.ts"() {
10696
+ async function setSecret(key, value) {
10697
+ await storage.setSecret(key, value);
10698
+ }
10699
+ async function deleteSecret(key) {
10700
+ return storage.deleteSecret(key);
10701
+ }
10702
+ async function listSecrets() {
10703
+ return storage.listSecrets();
10704
+ }
10705
+ function getSecretStorage() {
10706
+ return storage;
10707
+ }
10708
+ var SECRET_FILE_NAME, FileSecretStorage, storage;
10709
+ var init_secret_storage = __esm({
10710
+ "src/utils/secret-storage.ts"() {
10776
10711
  "use strict";
10777
- CLI_LOGIN_PORT = 8420;
10778
- FRONTEND_URL = process.env.SUPATEST_FRONTEND_URL || "https://code.supatest.ai";
10779
- API_URL = process.env.SUPATEST_API_URL || "https://code-api.supatest.ai";
10780
- CALLBACK_TIMEOUT_MS2 = 3e5;
10781
- STATE_LENGTH = 32;
10712
+ SECRET_FILE_NAME = "secrets.json";
10713
+ FileSecretStorage = class {
10714
+ secretFilePath;
10715
+ constructor() {
10716
+ const rootDirName = process.env.NODE_ENV === "development" ? ".supatest-dev" : ".supatest";
10717
+ const secretsDir = join8(homedir6(), rootDirName, "claude-auth");
10718
+ this.secretFilePath = join8(secretsDir, SECRET_FILE_NAME);
10719
+ }
10720
+ async ensureDirectoryExists() {
10721
+ const dir = dirname2(this.secretFilePath);
10722
+ await fs4.mkdir(dir, { recursive: true, mode: 448 });
10723
+ }
10724
+ async loadSecrets() {
10725
+ try {
10726
+ const data = await fs4.readFile(this.secretFilePath, "utf-8");
10727
+ const secrets = JSON.parse(data);
10728
+ return new Map(Object.entries(secrets));
10729
+ } catch (error) {
10730
+ const err = error;
10731
+ if (err.code === "ENOENT") {
10732
+ return /* @__PURE__ */ new Map();
10733
+ }
10734
+ try {
10735
+ await fs4.unlink(this.secretFilePath);
10736
+ } catch {
10737
+ }
10738
+ return /* @__PURE__ */ new Map();
10739
+ }
10740
+ }
10741
+ async saveSecrets(secrets) {
10742
+ await this.ensureDirectoryExists();
10743
+ const data = Object.fromEntries(secrets);
10744
+ const json = JSON.stringify(data, null, 2);
10745
+ await fs4.writeFile(this.secretFilePath, json, { mode: 384 });
10746
+ }
10747
+ async getSecret(key) {
10748
+ const secrets = await this.loadSecrets();
10749
+ return secrets.get(key) ?? null;
10750
+ }
10751
+ async setSecret(key, value) {
10752
+ const secrets = await this.loadSecrets();
10753
+ secrets.set(key, value);
10754
+ await this.saveSecrets(secrets);
10755
+ }
10756
+ async deleteSecret(key) {
10757
+ const secrets = await this.loadSecrets();
10758
+ if (!secrets.has(key)) {
10759
+ return false;
10760
+ }
10761
+ secrets.delete(key);
10762
+ if (secrets.size === 0) {
10763
+ try {
10764
+ await fs4.unlink(this.secretFilePath);
10765
+ } catch (error) {
10766
+ const err = error;
10767
+ if (err.code !== "ENOENT") {
10768
+ throw error;
10769
+ }
10770
+ }
10771
+ } else {
10772
+ await this.saveSecrets(secrets);
10773
+ }
10774
+ return true;
10775
+ }
10776
+ async listSecrets() {
10777
+ const secrets = await this.loadSecrets();
10778
+ return Array.from(secrets.keys());
10779
+ }
10780
+ };
10781
+ storage = new FileSecretStorage();
10782
10782
  }
10783
10783
  });
10784
10784
 
@@ -13321,7 +13321,7 @@ var init_InputPrompt = __esm({
13321
13321
  }
13322
13322
  return /* @__PURE__ */ React24.createElement(Text19, { color: theme.text.primary, key: idx }, line);
13323
13323
  })), !hasContent && disabled && /* @__PURE__ */ React24.createElement(Text19, { color: theme.text.dim, italic: true }, "Waiting for agent to complete...")))
13324
- ), /* @__PURE__ */ React24.createElement(Box21, { justifyContent: "space-between", paddingX: 1 }, /* @__PURE__ */ React24.createElement(Box21, { gap: 2 }, /* @__PURE__ */ React24.createElement(Box21, null, /* @__PURE__ */ React24.createElement(Text19, { color: agentMode === "plan" ? theme.status.inProgress : theme.text.dim }, agentMode === "plan" ? "\u23F8 plan" : "\u25B6 build"), /* @__PURE__ */ React24.createElement(Text19, { color: theme.text.dim }, " (shift+tab)")), /* @__PURE__ */ React24.createElement(Box21, null, /* @__PURE__ */ React24.createElement(Text19, { color: theme.text.dim }, "model:"), /* @__PURE__ */ React24.createElement(Text19, { color: theme.text.info }, getModelDisplayName(selectedModel)), /* @__PURE__ */ React24.createElement(Text19, { color: theme.text.dim }, " (Cost: "), /* @__PURE__ */ React24.createElement(Text19, { color: theme.text.info }, getModelCostLabel(selectedModel)), /* @__PURE__ */ React24.createElement(Text19, { color: theme.text.dim }, ") (ctrl+shift+m)"))), /* @__PURE__ */ React24.createElement(Box21, null, /* @__PURE__ */ React24.createElement(Text19, { color: usageStats && usageStats.contextPct >= 90 ? theme.text.error : usageStats && usageStats.contextPct >= 75 ? theme.text.warning : theme.text.dim }, usageStats?.contextPct ?? 0, "% context used"), /* @__PURE__ */ React24.createElement(Text19, { color: theme.text.dim }, " ", "(", usageStats ? usageStats.inputTokens >= 1e3 ? `${(usageStats.inputTokens / 1e3).toFixed(1)}K` : usageStats.inputTokens : 0, " / ", usageStats ? usageStats.contextWindow >= 1e3 ? `${(usageStats.contextWindow / 1e3).toFixed(0)}K` : usageStats.contextWindow : "200K", ")"))));
13324
+ ), /* @__PURE__ */ React24.createElement(Box21, { justifyContent: "space-between", paddingX: 1 }, /* @__PURE__ */ React24.createElement(Box21, { gap: 2 }, /* @__PURE__ */ React24.createElement(Box21, null, /* @__PURE__ */ React24.createElement(Text19, { color: agentMode === "plan" ? theme.status.inProgress : theme.text.dim }, agentMode === "plan" ? "\u23F8 plan" : "\u25B6 build"), /* @__PURE__ */ React24.createElement(Text19, { color: theme.text.dim }, " (shift+tab)")), /* @__PURE__ */ React24.createElement(Box21, null, /* @__PURE__ */ React24.createElement(Text19, { color: theme.text.dim }, "model:"), /* @__PURE__ */ React24.createElement(Text19, { color: theme.text.info }, getModelDisplayName(selectedModel)), /* @__PURE__ */ React24.createElement(Text19, { color: theme.text.dim }, " (Cost: "), /* @__PURE__ */ React24.createElement(Text19, { color: theme.text.info }, getModelCostLabel(selectedModel)), /* @__PURE__ */ React24.createElement(Text19, { color: theme.text.dim }, ") (ctrl+shift+m)")))));
13325
13325
  });
13326
13326
  InputPromptInner.displayName = "InputPromptInner";
13327
13327
  InputPrompt = memo3(InputPromptInner);
@@ -13616,12 +13616,9 @@ var init_ProviderSelector = __esm({
13616
13616
  ProviderSelector = ({
13617
13617
  currentProvider,
13618
13618
  onSelect,
13619
- onCancel,
13620
- claudeMaxAvailable
13619
+ onCancel
13621
13620
  }) => {
13622
- const availableProviders = PROVIDERS.filter(
13623
- (p) => p.id !== "claude-max" || claudeMaxAvailable
13624
- );
13621
+ const availableProviders = PROVIDERS;
13625
13622
  const currentIndex = availableProviders.findIndex(
13626
13623
  (p) => p.id === currentProvider
13627
13624
  );
@@ -14168,7 +14165,6 @@ var init_App = __esm({
14168
14165
  return;
14169
14166
  }
14170
14167
  if (command === "/provider") {
14171
- isClaudeMaxAvailable().then(setClaudeMaxAvailable);
14172
14168
  setShowProviderSelector(true);
14173
14169
  return;
14174
14170
  }
@@ -14683,7 +14679,6 @@ var init_App = __esm({
14683
14679
  showProviderSelector && /* @__PURE__ */ React31.createElement(
14684
14680
  ProviderSelector,
14685
14681
  {
14686
- claudeMaxAvailable,
14687
14682
  currentProvider: llmProvider,
14688
14683
  onCancel: handleProviderSelectorCancel,
14689
14684
  onSelect: handleProviderSelect
@@ -15351,61 +15346,9 @@ var init_interactive = __esm({
15351
15346
  // src/index.ts
15352
15347
  await init_config();
15353
15348
  init_shared_es();
15354
- import { Command } from "commander";
15355
-
15356
- // src/commands/claude-login.ts
15357
- init_claude_oauth();
15358
- init_secret_storage();
15359
- async function claudeLoginCommand() {
15360
- const secretStorage = getSecretStorage();
15361
- const oauthService = new ClaudeOAuthService(secretStorage);
15362
- const isAuth = await oauthService.isAuthenticated();
15363
- if (isAuth) {
15364
- console.log("\u2705 You're already authenticated with Claude.");
15365
- console.log("\nTo re-authenticate, first run: supatest claude-logout\n");
15366
- return;
15367
- }
15368
- const result = await oauthService.authorize();
15369
- if (!result.success) {
15370
- console.error(`
15371
- \u274C Authentication failed: ${result.error}
15372
- `);
15373
- process.exit(1);
15374
- }
15375
- }
15376
- async function claudeLogoutCommand() {
15377
- const secretStorage = getSecretStorage();
15378
- const oauthService = new ClaudeOAuthService(secretStorage);
15379
- const isAuth = await oauthService.isAuthenticated();
15380
- if (!isAuth) {
15381
- console.log("You're not currently authenticated with Claude.\n");
15382
- return;
15383
- }
15384
- await oauthService.deleteTokens();
15385
- console.log("\u2705 Successfully logged out from Claude.\n");
15386
- }
15387
- async function claudeStatusCommand() {
15388
- const secretStorage = getSecretStorage();
15389
- const oauthService = new ClaudeOAuthService(secretStorage);
15390
- const status = await oauthService.getStatus();
15391
- if (status.isAuthenticated) {
15392
- console.log("\u2705 Authenticated with Claude");
15393
- if (status.expiresAt) {
15394
- const expiresDate = new Date(status.expiresAt);
15395
- console.log(` Token expires: ${expiresDate.toLocaleString()}`);
15396
- }
15397
- } else {
15398
- console.log("\u274C Not authenticated with Claude");
15399
- if (status.error) {
15400
- console.log(` Error: ${status.error}`);
15401
- }
15402
- }
15403
- console.log();
15404
- }
15405
-
15406
- // src/index.ts
15407
15349
  init_setup();
15408
15350
  await init_config();
15351
+ import { Command } from "commander";
15409
15352
 
15410
15353
  // src/modes/headless.ts
15411
15354
  init_api_client();
@@ -15419,7 +15362,7 @@ init_react();
15419
15362
  init_MessageList();
15420
15363
  init_SessionContext();
15421
15364
  import { execSync as execSync2 } from "child_process";
15422
- import { homedir as homedir5 } from "os";
15365
+ import { homedir as homedir4 } from "os";
15423
15366
  import { Box as Box13, useApp } from "ink";
15424
15367
  import React14, { useEffect as useEffect2, useRef as useRef4, useState as useState3 } from "react";
15425
15368
  var getGitBranch = () => {
@@ -15431,7 +15374,7 @@ var getGitBranch = () => {
15431
15374
  };
15432
15375
  var getCurrentFolder = () => {
15433
15376
  const cwd = process.cwd();
15434
- const home = homedir5();
15377
+ const home = homedir4();
15435
15378
  if (cwd.startsWith(home)) {
15436
15379
  return `~${cwd.slice(home.length)}`;
15437
15380
  }
@@ -15678,7 +15621,7 @@ async function runAgent(config2) {
15678
15621
  // src/utils/auto-update.ts
15679
15622
  init_version();
15680
15623
  init_error_logger();
15681
- import { execSync as execSync3, spawn as spawn3 } from "child_process";
15624
+ import { execSync as execSync3, spawn as spawn2 } from "child_process";
15682
15625
  import latestVersion from "latest-version";
15683
15626
  import { gt } from "semver";
15684
15627
  var UPDATE_CHECK_TIMEOUT = 3e3;
@@ -15719,8 +15662,24 @@ Updating Supatest CLI ${CLI_VERSION} \u2192 ${latest}...`);
15719
15662
  timeout: INSTALL_TIMEOUT
15720
15663
  });
15721
15664
  }
15665
+ let updateVerified = false;
15666
+ try {
15667
+ const installedVersion = execSync3("npm ls -g @supatest/cli --json 2>/dev/null", {
15668
+ encoding: "utf-8"
15669
+ });
15670
+ const parsed = JSON.parse(installedVersion);
15671
+ const newVersion = parsed.dependencies?.["@supatest/cli"]?.version;
15672
+ if (newVersion && gt(newVersion, CLI_VERSION)) {
15673
+ updateVerified = true;
15674
+ }
15675
+ } catch {
15676
+ }
15677
+ if (!updateVerified) {
15678
+ console.log("Update completed but could not verify new version. Continuing with current version...\n");
15679
+ return;
15680
+ }
15722
15681
  console.log("\u2713 Updated successfully\n");
15723
- const child = spawn3(process.argv[0], process.argv.slice(1), {
15682
+ const child = spawn2(process.argv[0], process.argv.slice(1), {
15724
15683
  stdio: "inherit",
15725
15684
  detached: false
15726
15685
  });
@@ -16005,33 +15964,6 @@ program.command("setup").description("Check prerequisites and set up required to
16005
15964
  process.exit(1);
16006
15965
  }
16007
15966
  });
16008
- program.command("claude-login").description("Authenticate with Claude using OAuth (uses your Claude Pro/Max subscription)").action(async () => {
16009
- try {
16010
- await claudeLoginCommand();
16011
- } catch (error) {
16012
- logError(error, { source: "claude-login" });
16013
- logger.error(`Claude login failed: ${error instanceof Error ? error.message : String(error)}`);
16014
- process.exit(1);
16015
- }
16016
- });
16017
- program.command("claude-logout").description("Sign out from Claude OAuth").action(async () => {
16018
- try {
16019
- await claudeLogoutCommand();
16020
- } catch (error) {
16021
- logError(error, { source: "claude-logout" });
16022
- logger.error(`Claude logout failed: ${error instanceof Error ? error.message : String(error)}`);
16023
- process.exit(1);
16024
- }
16025
- });
16026
- program.command("claude-status").description("Show Claude OAuth authentication status").action(async () => {
16027
- try {
16028
- await claudeStatusCommand();
16029
- } catch (error) {
16030
- logError(error, { source: "claude-status" });
16031
- logger.error(`Claude status check failed: ${error instanceof Error ? error.message : String(error)}`);
16032
- process.exit(1);
16033
- }
16034
- });
16035
15967
  var filteredArgv = process.argv.filter((arg, index) => {
16036
15968
  return !(arg === "--" && index > 1);
16037
15969
  });