@juspay/neurolink 9.14.0 → 9.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/README.md +15 -15
  3. package/dist/auth/anthropicOAuth.d.ts +377 -0
  4. package/dist/auth/anthropicOAuth.js +914 -0
  5. package/dist/auth/index.d.ts +20 -0
  6. package/dist/auth/index.js +29 -0
  7. package/dist/auth/tokenStore.d.ts +225 -0
  8. package/dist/auth/tokenStore.js +521 -0
  9. package/dist/cli/commands/auth.d.ts +50 -0
  10. package/dist/cli/commands/auth.js +1115 -0
  11. package/dist/cli/factories/authCommandFactory.d.ts +52 -0
  12. package/dist/cli/factories/authCommandFactory.js +146 -0
  13. package/dist/cli/factories/commandFactory.d.ts +6 -0
  14. package/dist/cli/factories/commandFactory.js +92 -2
  15. package/dist/cli/parser.js +11 -2
  16. package/dist/constants/enums.d.ts +20 -0
  17. package/dist/constants/enums.js +30 -0
  18. package/dist/constants/index.d.ts +3 -1
  19. package/dist/constants/index.js +11 -1
  20. package/dist/index.d.ts +1 -1
  21. package/dist/lib/auth/anthropicOAuth.d.ts +377 -0
  22. package/dist/lib/auth/anthropicOAuth.js +915 -0
  23. package/dist/lib/auth/index.d.ts +20 -0
  24. package/dist/lib/auth/index.js +30 -0
  25. package/dist/lib/auth/tokenStore.d.ts +225 -0
  26. package/dist/lib/auth/tokenStore.js +522 -0
  27. package/dist/lib/constants/enums.d.ts +20 -0
  28. package/dist/lib/constants/enums.js +30 -0
  29. package/dist/lib/constants/index.d.ts +3 -1
  30. package/dist/lib/constants/index.js +11 -1
  31. package/dist/lib/index.d.ts +1 -1
  32. package/dist/lib/models/anthropicModels.d.ts +267 -0
  33. package/dist/lib/models/anthropicModels.js +528 -0
  34. package/dist/lib/providers/anthropic.d.ts +123 -2
  35. package/dist/lib/providers/anthropic.js +800 -10
  36. package/dist/lib/types/errors.d.ts +62 -0
  37. package/dist/lib/types/errors.js +107 -0
  38. package/dist/lib/types/index.d.ts +2 -1
  39. package/dist/lib/types/index.js +2 -0
  40. package/dist/lib/types/providers.d.ts +107 -0
  41. package/dist/lib/types/providers.js +69 -0
  42. package/dist/lib/types/subscriptionTypes.d.ts +893 -0
  43. package/dist/lib/types/subscriptionTypes.js +8 -0
  44. package/dist/lib/utils/providerConfig.d.ts +167 -0
  45. package/dist/lib/utils/providerConfig.js +619 -9
  46. package/dist/models/anthropicModels.d.ts +267 -0
  47. package/dist/models/anthropicModels.js +527 -0
  48. package/dist/providers/anthropic.d.ts +123 -2
  49. package/dist/providers/anthropic.js +800 -10
  50. package/dist/types/errors.d.ts +62 -0
  51. package/dist/types/errors.js +107 -0
  52. package/dist/types/index.d.ts +2 -1
  53. package/dist/types/index.js +2 -0
  54. package/dist/types/providers.d.ts +107 -0
  55. package/dist/types/providers.js +69 -0
  56. package/dist/types/subscriptionTypes.d.ts +893 -0
  57. package/dist/types/subscriptionTypes.js +7 -0
  58. package/dist/utils/providerConfig.d.ts +167 -0
  59. package/dist/utils/providerConfig.js +619 -9
  60. package/package.json +2 -1
@@ -0,0 +1,1115 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * NeuroLink Auth Command
4
+ *
5
+ * Unified authentication command for AI providers supporting:
6
+ * - API key authentication (traditional)
7
+ * - OAuth 2.1 authentication with PKCE (for Claude subscription)
8
+ *
9
+ * Subcommands:
10
+ * - login: Authenticate with a provider
11
+ * - logout: Clear stored credentials
12
+ * - status: Show authentication status
13
+ * - refresh: Manually refresh OAuth tokens
14
+ *
15
+ * Currently supports:
16
+ * - Anthropic (API key + OAuth)
17
+ */
18
+ import fs from "fs";
19
+ import path from "path";
20
+ import { execFile } from "child_process";
21
+ import { randomBytes, createHash } from "crypto";
22
+ import inquirer from "inquirer";
23
+ import chalk from "chalk";
24
+ import ora from "ora";
25
+ import { logger } from "../../lib/utils/logger.js";
26
+ import { defaultTokenStore } from "../../lib/auth/tokenStore.js";
27
+ import { CLAUDE_CODE_CLIENT_ID, ANTHROPIC_AUTH_URL, ANTHROPIC_TOKEN_URL, ANTHROPIC_REDIRECT_URI, CLAUDE_CLI_USER_AGENT, OAUTH_BETA_HEADERS, } from "../../lib/auth/anthropicOAuth.js";
28
+ // =============================================================================
29
+ // CONSTANTS
30
+ // =============================================================================
31
+ const NEUROLINK_CONFIG_DIR = path.join(process.env.HOME || process.env.USERPROFILE || ".", ".neurolink");
32
+ const ENV_FILE_PATH = path.join(process.cwd(), ".env");
33
+ // Anthropic OAuth Configuration (Claude Code Official) - For direct OAuth usage
34
+ // Uses claude.ai/oauth/authorize for Claude Pro/Max subscription access
35
+ const ANTHROPIC_OAUTH_CONFIG = {
36
+ clientId: CLAUDE_CODE_CLIENT_ID,
37
+ // NOTE: Uses claude.ai NOT console.anthropic.com for direct OAuth
38
+ authorizationUrl: ANTHROPIC_AUTH_URL,
39
+ tokenUrl: ANTHROPIC_TOKEN_URL,
40
+ redirectUri: ANTHROPIC_REDIRECT_URI,
41
+ // Scopes for direct OAuth (no API key creation needed)
42
+ scope: "user:profile user:inference",
43
+ userAgent: CLAUDE_CLI_USER_AGENT,
44
+ betaHeaders: OAUTH_BETA_HEADERS,
45
+ };
46
+ // Anthropic Console OAuth Configuration - For API key creation flow
47
+ // This uses console.anthropic.com for authorization which grants org:create_api_key scope
48
+ const ANTHROPIC_CONSOLE_OAUTH_CONFIG = {
49
+ clientId: CLAUDE_CODE_CLIENT_ID,
50
+ // Authorization URL for console (required for API key creation scope)
51
+ authorizationUrl: "https://console.anthropic.com/oauth/authorize",
52
+ tokenUrl: ANTHROPIC_TOKEN_URL,
53
+ redirectUri: ANTHROPIC_REDIRECT_URI,
54
+ // Required scopes - org:create_api_key is needed for API key creation
55
+ scope: "org:create_api_key user:profile user:inference",
56
+ userAgent: CLAUDE_CLI_USER_AGENT,
57
+ // API key creation endpoint
58
+ createApiKeyUrl: "https://api.anthropic.com/api/oauth/claude_cli/create_api_key",
59
+ };
60
+ // Supported providers
61
+ const SUPPORTED_PROVIDERS = ["anthropic"];
62
+ // =============================================================================
63
+ // SUBCOMMAND HANDLERS
64
+ // =============================================================================
65
+ /**
66
+ * Handle the login subcommand
67
+ * `neurolink auth login <provider>`
68
+ */
69
+ export async function handleLogin(argv) {
70
+ try {
71
+ const provider = argv.provider?.toLowerCase();
72
+ // Validate provider
73
+ if (!SUPPORTED_PROVIDERS.includes(provider)) {
74
+ logger.error(chalk.red(`Unsupported provider: ${provider}. Supported: ${SUPPORTED_PROVIDERS.join(", ")}`));
75
+ process.exit(1);
76
+ }
77
+ // If method is specified, use it directly
78
+ if (argv.method) {
79
+ if (argv.method === "api-key") {
80
+ await handleApiKeyAuth(provider, !argv.nonInteractive);
81
+ }
82
+ else if (argv.method === "oauth") {
83
+ await handleOAuthAuth(provider);
84
+ }
85
+ else if (argv.method === "create-api-key") {
86
+ await handleCreateApiKeyOAuth(provider);
87
+ }
88
+ }
89
+ else {
90
+ // Interactive mode - ask user which method they prefer
91
+ await handleInteractiveAuth(provider);
92
+ }
93
+ }
94
+ catch (error) {
95
+ logger.error(chalk.red("Authentication failed:"));
96
+ logger.error(chalk.red(error instanceof Error ? error.message : "Unknown error"));
97
+ process.exit(1);
98
+ }
99
+ }
100
+ /**
101
+ * Handle the logout subcommand
102
+ * `neurolink auth logout <provider>`
103
+ */
104
+ export async function handleLogout(argv) {
105
+ try {
106
+ const provider = argv.provider?.toLowerCase();
107
+ // Validate provider
108
+ if (!SUPPORTED_PROVIDERS.includes(provider)) {
109
+ logger.error(chalk.red(`Unsupported provider: ${provider}. Supported: ${SUPPORTED_PROVIDERS.join(", ")}`));
110
+ process.exit(1);
111
+ }
112
+ logger.always(chalk.blue(`\nClearing ${provider} credentials...\n`));
113
+ const spinner = argv.quiet
114
+ ? null
115
+ : ora("Removing stored credentials...").start();
116
+ try {
117
+ // Clear stored credentials file
118
+ const credentialsFile = path.join(NEUROLINK_CONFIG_DIR, `${provider}-credentials.json`);
119
+ if (fs.existsSync(credentialsFile)) {
120
+ fs.unlinkSync(credentialsFile);
121
+ if (spinner) {
122
+ spinner.succeed("Stored credentials removed");
123
+ }
124
+ }
125
+ else {
126
+ if (spinner) {
127
+ spinner.info("No stored credentials found");
128
+ }
129
+ }
130
+ // Also clear from TokenStore if OAuth
131
+ try {
132
+ await defaultTokenStore.clearTokens(provider);
133
+ }
134
+ catch {
135
+ // Ignore if no tokens stored
136
+ }
137
+ // Check for environment variable
138
+ const envVar = getEnvVarName(provider);
139
+ const hasEnvKey = !!process.env[envVar];
140
+ if (hasEnvKey) {
141
+ logger.always("");
142
+ logger.always(chalk.yellow(`Note: ${envVar} is still set in your environment or .env file.`));
143
+ logger.always(chalk.yellow("You may need to manually remove it from your shell profile or .env file."));
144
+ // Offer to remove from .env if it exists
145
+ if (fs.existsSync(ENV_FILE_PATH)) {
146
+ const { removeFromEnv } = await inquirer.prompt([
147
+ {
148
+ type: "confirm",
149
+ name: "removeFromEnv",
150
+ message: `Remove ${envVar} from .env file?`,
151
+ default: false,
152
+ },
153
+ ]);
154
+ if (removeFromEnv) {
155
+ await removeFromEnvFile(envVar);
156
+ logger.always(chalk.green(`Removed ${envVar} from .env file`));
157
+ }
158
+ }
159
+ }
160
+ logger.always("");
161
+ logger.always(chalk.green(`${provider} credentials cleared successfully.`));
162
+ }
163
+ catch (error) {
164
+ if (spinner) {
165
+ spinner.fail("Failed to clear credentials");
166
+ }
167
+ throw error;
168
+ }
169
+ }
170
+ catch (error) {
171
+ logger.error(chalk.red("Logout failed:"));
172
+ logger.error(chalk.red(error instanceof Error ? error.message : "Unknown error"));
173
+ process.exit(1);
174
+ }
175
+ }
176
+ /**
177
+ * Handle the status subcommand
178
+ * `neurolink auth status [provider]`
179
+ */
180
+ export async function handleStatus(argv) {
181
+ try {
182
+ const provider = argv.provider?.toLowerCase();
183
+ // If provider specified, show just that provider
184
+ const providersToCheck = provider
185
+ ? [provider]
186
+ : [...SUPPORTED_PROVIDERS];
187
+ const results = [];
188
+ for (const p of providersToCheck) {
189
+ const status = await getAuthStatus(p);
190
+ results.push(status);
191
+ }
192
+ // Output results
193
+ if (argv.format === "json") {
194
+ logger.always(JSON.stringify(results, null, 2));
195
+ }
196
+ else {
197
+ logger.always(chalk.bold("\nAuthentication Status:\n"));
198
+ for (const status of results) {
199
+ const providerName = status.provider.charAt(0).toUpperCase() + status.provider.slice(1);
200
+ const statusIcon = status.isAuthenticated
201
+ ? chalk.green("[Authenticated]")
202
+ : chalk.yellow("[Not Authenticated]");
203
+ logger.always(`${chalk.cyan(providerName)} ${statusIcon}`);
204
+ if (status.isAuthenticated) {
205
+ logger.always(` Method: ${status.method}`);
206
+ if (status.subscriptionTier) {
207
+ logger.always(` Subscription: ${chalk.blue(status.subscriptionTier)}`);
208
+ }
209
+ if (status.method === "oauth") {
210
+ if (status.tokenExpiry) {
211
+ const isExpired = status.needsRefresh;
212
+ const expiryLabel = isExpired
213
+ ? chalk.red("Expired")
214
+ : chalk.green(status.tokenExpiry);
215
+ logger.always(` Token Expires: ${expiryLabel}`);
216
+ }
217
+ if (status.hasRefreshToken) {
218
+ logger.always(` Refresh Token: ${chalk.green("Available")}`);
219
+ }
220
+ else {
221
+ logger.always(` Refresh Token: ${chalk.yellow("Not available")}`);
222
+ }
223
+ if (status.needsRefresh && status.hasRefreshToken) {
224
+ logger.always(chalk.yellow(` Run 'neurolink auth refresh ${status.provider}' to refresh tokens`));
225
+ }
226
+ }
227
+ }
228
+ else {
229
+ logger.always(chalk.blue(` Run 'neurolink auth login ${status.provider}' to authenticate`));
230
+ }
231
+ logger.always("");
232
+ }
233
+ }
234
+ }
235
+ catch (error) {
236
+ logger.error(chalk.red("Status check failed:"));
237
+ logger.error(chalk.red(error instanceof Error ? error.message : "Unknown error"));
238
+ process.exit(1);
239
+ }
240
+ }
241
+ /**
242
+ * Handle the refresh subcommand
243
+ * `neurolink auth refresh <provider>`
244
+ */
245
+ export async function handleRefresh(argv) {
246
+ try {
247
+ const provider = argv.provider?.toLowerCase();
248
+ // Validate provider
249
+ if (!SUPPORTED_PROVIDERS.includes(provider)) {
250
+ logger.error(chalk.red(`Unsupported provider: ${provider}. Supported: ${SUPPORTED_PROVIDERS.join(", ")}`));
251
+ process.exit(1);
252
+ }
253
+ logger.always(chalk.blue(`\nRefreshing ${provider} OAuth tokens...\n`));
254
+ const spinner = argv.quiet ? null : ora("Reading stored tokens...").start();
255
+ try {
256
+ // Get stored credentials
257
+ const credentials = await getStoredCredentials(provider);
258
+ if (!credentials || credentials.type !== "oauth") {
259
+ if (spinner) {
260
+ spinner.fail("No OAuth credentials found");
261
+ }
262
+ logger.error(chalk.red(`No OAuth authentication found for ${provider}. Use 'neurolink auth login ${provider} --method oauth' first.`));
263
+ process.exit(1);
264
+ }
265
+ if (!credentials.oauth?.refreshToken) {
266
+ if (spinner) {
267
+ spinner.fail("No refresh token available");
268
+ }
269
+ logger.error(chalk.red("No refresh token available. Please re-authenticate with OAuth."));
270
+ process.exit(1);
271
+ }
272
+ if (spinner) {
273
+ spinner.text = "Refreshing access token...";
274
+ }
275
+ // Refresh the token with Claude CLI User-Agent
276
+ // IMPORTANT: Uses JSON body, not URLSearchParams
277
+ const tokenResponse = await fetch(ANTHROPIC_OAUTH_CONFIG.tokenUrl, {
278
+ method: "POST",
279
+ headers: {
280
+ "Content-Type": "application/json",
281
+ Accept: "application/json",
282
+ "User-Agent": ANTHROPIC_OAUTH_CONFIG.userAgent,
283
+ },
284
+ body: JSON.stringify({
285
+ grant_type: "refresh_token",
286
+ refresh_token: credentials.oauth.refreshToken,
287
+ client_id: ANTHROPIC_OAUTH_CONFIG.clientId,
288
+ }),
289
+ });
290
+ if (!tokenResponse.ok) {
291
+ const errorText = await tokenResponse.text();
292
+ throw new Error(`Token refresh failed: ${tokenResponse.status} - ${errorText}`);
293
+ }
294
+ const tokenData = (await tokenResponse.json());
295
+ // Update stored tokens
296
+ const newTokens = {
297
+ accessToken: tokenData.access_token,
298
+ refreshToken: tokenData.refresh_token || credentials.oauth.refreshToken,
299
+ expiresAt: tokenData.expires_in
300
+ ? Date.now() + tokenData.expires_in * 1000
301
+ : undefined,
302
+ tokenType: tokenData.token_type || "Bearer",
303
+ scope: tokenData.scope,
304
+ };
305
+ await saveStoredCredentials(provider, {
306
+ type: "oauth",
307
+ oauth: newTokens,
308
+ provider,
309
+ subscriptionTier: credentials.subscriptionTier,
310
+ createdAt: credentials.createdAt,
311
+ updatedAt: Date.now(),
312
+ });
313
+ if (spinner) {
314
+ spinner.succeed("Access token refreshed successfully!");
315
+ }
316
+ logger.always("");
317
+ logger.always(chalk.green("Token refresh complete!"));
318
+ if (newTokens.expiresAt) {
319
+ logger.always(` New expiry: ${new Date(newTokens.expiresAt).toLocaleString()}`);
320
+ }
321
+ }
322
+ catch (error) {
323
+ if (spinner) {
324
+ spinner.fail("Token refresh failed");
325
+ }
326
+ throw error;
327
+ }
328
+ }
329
+ catch (error) {
330
+ logger.error(chalk.red("Token refresh failed:"));
331
+ logger.error(chalk.red(error instanceof Error ? error.message : "Unknown error"));
332
+ process.exit(1);
333
+ }
334
+ }
335
+ // =============================================================================
336
+ // LEGACY HANDLER (for backward compatibility)
337
+ // =============================================================================
338
+ /**
339
+ * Legacy main auth command handler
340
+ * @deprecated Use subcommand handlers instead
341
+ */
342
+ export async function handleAuth(argv) {
343
+ // Map legacy flags to subcommands
344
+ if (argv.status) {
345
+ await handleStatus({
346
+ provider: argv.provider,
347
+ format: "text",
348
+ quiet: false,
349
+ debug: argv.debug,
350
+ });
351
+ }
352
+ else if (argv.logout) {
353
+ await handleLogout({
354
+ provider: argv.provider,
355
+ format: "text",
356
+ quiet: false,
357
+ debug: argv.debug,
358
+ });
359
+ }
360
+ else {
361
+ await handleLogin({
362
+ provider: argv.provider,
363
+ method: argv.method,
364
+ format: "text",
365
+ quiet: false,
366
+ nonInteractive: argv.nonInteractive,
367
+ debug: argv.debug,
368
+ });
369
+ }
370
+ }
371
+ // =============================================================================
372
+ // AUTHENTICATION METHODS
373
+ // =============================================================================
374
+ /**
375
+ * Interactive authentication - ask user which method they prefer
376
+ */
377
+ async function handleInteractiveAuth(provider) {
378
+ logger.always(chalk.blue(`\n${provider.charAt(0).toUpperCase() + provider.slice(1)} Authentication Setup\n`));
379
+ const currentStatus = await checkExistingAuth(provider);
380
+ if (currentStatus.hasValidAuth) {
381
+ logger.always(chalk.green("You already have valid authentication configured."));
382
+ logger.always(` Type: ${currentStatus.type}`);
383
+ if (currentStatus.type === "api-key") {
384
+ logger.always(` Key: ${maskCredential(currentStatus.credential || "")}`);
385
+ }
386
+ logger.always("");
387
+ const { reconfigure } = await inquirer.prompt([
388
+ {
389
+ type: "confirm",
390
+ name: "reconfigure",
391
+ message: "Would you like to reconfigure authentication?",
392
+ default: false,
393
+ },
394
+ ]);
395
+ if (!reconfigure) {
396
+ logger.always(chalk.blue("Keeping existing configuration."));
397
+ return;
398
+ }
399
+ }
400
+ // Show authentication method options
401
+ const { method } = await inquirer.prompt([
402
+ {
403
+ type: "list",
404
+ name: "method",
405
+ message: "Select authentication method:",
406
+ choices: [
407
+ {
408
+ name: "API Key - Traditional authentication with API key (pay-per-use)",
409
+ value: "api-key",
410
+ },
411
+ {
412
+ name: "Claude Pro/Max OAuth - Use subscription directly (Recommended for Pro/Max users)",
413
+ value: "oauth",
414
+ },
415
+ {
416
+ name: "Create API Key (via OAuth) - Creates a real API key using your account",
417
+ value: "create-api-key",
418
+ },
419
+ ],
420
+ },
421
+ ]);
422
+ if (method === "api-key") {
423
+ await handleApiKeyAuth(provider, true);
424
+ }
425
+ else if (method === "create-api-key") {
426
+ await handleCreateApiKeyOAuth(provider);
427
+ }
428
+ else {
429
+ await handleOAuthAuth(provider);
430
+ }
431
+ }
432
+ /**
433
+ * Handle API key authentication
434
+ */
435
+ async function handleApiKeyAuth(provider, interactive) {
436
+ logger.always(chalk.blue("\nAPI Key Authentication\n"));
437
+ if (provider === "anthropic") {
438
+ logger.always(chalk.yellow("To get your Anthropic API key:"));
439
+ logger.always("1. Visit: https://console.anthropic.com/");
440
+ logger.always("2. Sign in to your Anthropic account");
441
+ logger.always("3. Go to 'API Keys' section");
442
+ logger.always("4. Click 'Create Key' and copy the API key (starts with sk-ant-)");
443
+ logger.always("");
444
+ }
445
+ if (!interactive) {
446
+ const envKey = process.env.ANTHROPIC_API_KEY?.trim();
447
+ if (envKey) {
448
+ await saveStoredCredentials(provider, {
449
+ type: "api-key",
450
+ apiKey: envKey,
451
+ provider,
452
+ createdAt: Date.now(),
453
+ updatedAt: Date.now(),
454
+ });
455
+ logger.always(chalk.green("Using ANTHROPIC_API_KEY from environment."));
456
+ return;
457
+ }
458
+ throw new Error("Non-interactive mode requires ANTHROPIC_API_KEY environment variable when using --method api-key");
459
+ }
460
+ const { apiKey } = await inquirer.prompt([
461
+ {
462
+ type: "password",
463
+ name: "apiKey",
464
+ message: `Enter your ${provider} API key:`,
465
+ validate: (input) => {
466
+ if (!input.trim()) {
467
+ return "API key is required";
468
+ }
469
+ if (provider === "anthropic" && !input.startsWith("sk-ant-")) {
470
+ return "Anthropic API key should start with 'sk-ant-'";
471
+ }
472
+ if (input.trim().length < 20) {
473
+ return "API key seems too short";
474
+ }
475
+ return true;
476
+ },
477
+ },
478
+ ]);
479
+ const trimmedKey = apiKey.trim();
480
+ // Validate the API key
481
+ const spinner = ora("Validating API key...").start();
482
+ try {
483
+ const isValid = await validateApiKey(provider, trimmedKey);
484
+ if (!isValid) {
485
+ spinner.fail("API key validation failed");
486
+ logger.error(chalk.red("The API key could not be validated. Please check and try again."));
487
+ return;
488
+ }
489
+ spinner.succeed("API key validated successfully");
490
+ }
491
+ catch (error) {
492
+ spinner.fail("API key validation failed");
493
+ logger.error(chalk.red(error instanceof Error ? error.message : "Validation error"));
494
+ return;
495
+ }
496
+ // Ask where to store the key
497
+ const { storageOption } = await inquirer.prompt([
498
+ {
499
+ type: "list",
500
+ name: "storageOption",
501
+ message: "Where would you like to store the API key?",
502
+ choices: [
503
+ { name: ".env file (project-level)", value: "env" },
504
+ { name: "NeuroLink config (user-level)", value: "config" },
505
+ { name: "Both", value: "both" },
506
+ ],
507
+ },
508
+ ]);
509
+ const spinnerSave = ora("Saving API key...").start();
510
+ try {
511
+ if (storageOption === "env" || storageOption === "both") {
512
+ await saveToEnvFile(provider, trimmedKey);
513
+ }
514
+ if (storageOption === "config" || storageOption === "both") {
515
+ await saveStoredCredentials(provider, {
516
+ type: "api-key",
517
+ apiKey: trimmedKey,
518
+ provider,
519
+ createdAt: Date.now(),
520
+ updatedAt: Date.now(),
521
+ });
522
+ }
523
+ spinnerSave.succeed("API key saved successfully");
524
+ logger.always("");
525
+ logger.always(chalk.green("Authentication configured successfully!"));
526
+ showUsageExample(provider);
527
+ }
528
+ catch (error) {
529
+ spinnerSave.fail("Failed to save API key");
530
+ throw error;
531
+ }
532
+ }
533
+ /**
534
+ * Handle API key creation via OAuth flow
535
+ * This authenticates via console.anthropic.com, gets an OAuth token with org:create_api_key scope,
536
+ * then uses that to create a real API key. This is the recommended method for Claude Pro/Max users.
537
+ *
538
+ * Based on opencode-anthropic-auth@0.0.8 implementation.
539
+ * Uses manual code entry since localhost redirect is not registered with Anthropic OAuth.
540
+ */
541
+ async function handleCreateApiKeyOAuth(provider) {
542
+ logger.always(chalk.blue("\nCreate API Key (via OAuth) - Claude Pro/Max\n"));
543
+ if (provider === "anthropic") {
544
+ logger.always(chalk.cyan("This will authenticate using your Claude Pro or Max subscription"));
545
+ logger.always(chalk.cyan("and create an API key for use with the Anthropic API.\n"));
546
+ logger.always(chalk.yellow("Note: After signing in, you'll see an authorization code."));
547
+ logger.always(chalk.yellow("Copy that code and paste it back here.\n"));
548
+ }
549
+ const spinner = ora("Starting OAuth flow...").start();
550
+ // Generate PKCE challenge - OpenCode sets state = verifier
551
+ const codeVerifier = randomBytes(32).toString("base64url");
552
+ const codeChallenge = createHash("sha256")
553
+ .update(codeVerifier)
554
+ .digest("base64url");
555
+ // Build authorization URL using CONSOLE config (for API key creation scope)
556
+ // Based on opencode-anthropic-auth implementation
557
+ const authUrl = new URL(ANTHROPIC_CONSOLE_OAUTH_CONFIG.authorizationUrl);
558
+ authUrl.searchParams.set("code", "true"); // Required param
559
+ authUrl.searchParams.set("client_id", ANTHROPIC_CONSOLE_OAUTH_CONFIG.clientId);
560
+ authUrl.searchParams.set("response_type", "code");
561
+ authUrl.searchParams.set("redirect_uri", ANTHROPIC_CONSOLE_OAUTH_CONFIG.redirectUri);
562
+ authUrl.searchParams.set("scope", ANTHROPIC_CONSOLE_OAUTH_CONFIG.scope);
563
+ authUrl.searchParams.set("code_challenge", codeChallenge);
564
+ authUrl.searchParams.set("code_challenge_method", "S256");
565
+ // OpenCode sets state = verifier for simplicity
566
+ authUrl.searchParams.set("state", codeVerifier);
567
+ spinner.text = "Opening browser for authentication...";
568
+ // Open browser
569
+ try {
570
+ await openBrowser(authUrl.toString());
571
+ spinner.succeed("Browser opened for authentication");
572
+ }
573
+ catch {
574
+ spinner.warn("Could not open browser automatically");
575
+ logger.always("");
576
+ logger.always(chalk.yellow("Please open this URL manually:"));
577
+ logger.always(chalk.cyan(authUrl.toString()));
578
+ }
579
+ logger.always("");
580
+ logger.always(chalk.blue("═".repeat(60)));
581
+ logger.always(chalk.blue.bold(" Complete authentication in your browser"));
582
+ logger.always(chalk.blue("═".repeat(60)));
583
+ logger.always("");
584
+ logger.always("1. Sign in to your Anthropic account in the browser");
585
+ logger.always("2. Authorize the application");
586
+ logger.always("3. Copy the authorization code shown on the page");
587
+ logger.always("4. Paste the code below");
588
+ logger.always("");
589
+ // Prompt user to enter the authorization code
590
+ const { authCode } = await inquirer.prompt([
591
+ {
592
+ type: "input",
593
+ name: "authCode",
594
+ message: "Paste the authorization code:",
595
+ validate: (input) => {
596
+ if (!input.trim()) {
597
+ return "Authorization code is required";
598
+ }
599
+ if (input.trim().length < 10) {
600
+ return "Authorization code seems too short";
601
+ }
602
+ return true;
603
+ },
604
+ },
605
+ ]);
606
+ const exchangeSpinner = ora("Exchanging authorization code for tokens...").start();
607
+ // Parse code#state format (e.g., "abc123#xyz789")
608
+ // IMPORTANT: OpenCode sets state = verifier in the auth URL, so the state
609
+ // returned in code#state IS the original verifier used for the code challenge!
610
+ const trimmedCode = authCode.trim();
611
+ const splits = trimmedCode.split("#");
612
+ const actualCode = splits[0];
613
+ const codeState = splits[1] || codeVerifier;
614
+ // Use the state as the verifier (since we set state = verifier in the auth URL)
615
+ const actualVerifier = codeState;
616
+ // Exchange code for tokens using JSON body (per opencode-anthropic-auth)
617
+ const tokenResponse = await fetch(ANTHROPIC_CONSOLE_OAUTH_CONFIG.tokenUrl, {
618
+ method: "POST",
619
+ headers: {
620
+ "Content-Type": "application/json",
621
+ },
622
+ body: JSON.stringify({
623
+ code: actualCode,
624
+ state: codeState,
625
+ grant_type: "authorization_code",
626
+ client_id: ANTHROPIC_CONSOLE_OAUTH_CONFIG.clientId,
627
+ redirect_uri: ANTHROPIC_CONSOLE_OAUTH_CONFIG.redirectUri,
628
+ code_verifier: actualVerifier,
629
+ }),
630
+ });
631
+ if (!tokenResponse.ok) {
632
+ const errorText = await tokenResponse.text();
633
+ exchangeSpinner.fail("Token exchange failed");
634
+ throw new Error(`Token exchange failed: ${tokenResponse.status} - ${errorText}`);
635
+ }
636
+ const tokenData = (await tokenResponse.json());
637
+ exchangeSpinner.succeed("OAuth tokens obtained successfully");
638
+ // Now create an API key using the OAuth token
639
+ const apiKeySpinner = ora("Creating API key...").start();
640
+ try {
641
+ const apiKeyResponse = await fetch(ANTHROPIC_CONSOLE_OAUTH_CONFIG.createApiKeyUrl, {
642
+ method: "POST",
643
+ headers: {
644
+ "Content-Type": "application/json",
645
+ Authorization: `Bearer ${tokenData.access_token}`,
646
+ "User-Agent": ANTHROPIC_CONSOLE_OAUTH_CONFIG.userAgent,
647
+ },
648
+ });
649
+ if (!apiKeyResponse.ok) {
650
+ const errorText = await apiKeyResponse.text();
651
+ apiKeySpinner.fail("API key creation failed");
652
+ throw new Error(`API key creation failed: ${apiKeyResponse.status} - ${errorText}`);
653
+ }
654
+ const apiKeyData = (await apiKeyResponse.json());
655
+ if (!apiKeyData.raw_key) {
656
+ apiKeySpinner.fail("API key creation failed");
657
+ throw new Error("No API key returned from creation endpoint");
658
+ }
659
+ apiKeySpinner.succeed("API key created successfully!");
660
+ // Auto-save to both locations for convenience
661
+ const spinnerSave = ora("Saving API key...").start();
662
+ try {
663
+ // Save to .env file (project-level)
664
+ await saveToEnvFile(provider, apiKeyData.raw_key);
665
+ // Save to NeuroLink config (user-level)
666
+ await saveStoredCredentials(provider, {
667
+ type: "api-key",
668
+ apiKey: apiKeyData.raw_key,
669
+ provider,
670
+ subscriptionTier: "pro", // Default for OAuth API-key flow; user can override via CLI
671
+ createdAt: Date.now(),
672
+ updatedAt: Date.now(),
673
+ });
674
+ spinnerSave.succeed("API key saved to .env and NeuroLink config");
675
+ logger.always("");
676
+ logger.always(chalk.green("═".repeat(60)));
677
+ logger.always(chalk.green.bold(" API key created and saved successfully!"));
678
+ logger.always(chalk.green("═".repeat(60)));
679
+ logger.always("");
680
+ logger.always(` API Key: ${chalk.cyan(maskCredential(apiKeyData.raw_key))}`);
681
+ logger.always(` Created via: ${chalk.blue("Claude Pro/Max OAuth")}`);
682
+ showUsageExample(provider);
683
+ }
684
+ catch (error) {
685
+ spinnerSave.fail("Failed to save API key");
686
+ throw error;
687
+ }
688
+ }
689
+ catch (error) {
690
+ if (error instanceof Error) {
691
+ throw error;
692
+ }
693
+ throw new Error(`Failed to create API key: ${String(error)}`);
694
+ }
695
+ }
696
+ /**
697
+ * Handle OAuth authentication using code-based flow
698
+ * User authenticates in browser and copies the authorization code back to CLI
699
+ * Uses claude.ai/oauth/authorize for Claude Pro/Max subscription access
700
+ */
701
+ async function handleOAuthAuth(provider) {
702
+ logger.always(chalk.blue("\nClaude Pro/Max OAuth Authentication\n"));
703
+ if (provider === "anthropic") {
704
+ logger.always(chalk.cyan("This will authenticate using your Claude Pro or Max subscription."));
705
+ logger.always(chalk.cyan("Your subscription includes API access at no extra cost!\n"));
706
+ logger.always(chalk.yellow("Note: After signing in, you'll see an authorization code."));
707
+ logger.always(chalk.yellow("Copy that code and paste it back here.\n"));
708
+ }
709
+ const { proceed } = await inquirer.prompt([
710
+ {
711
+ type: "confirm",
712
+ name: "proceed",
713
+ message: "Continue with OAuth authentication?",
714
+ default: true,
715
+ },
716
+ ]);
717
+ if (!proceed) {
718
+ logger.always(chalk.yellow("OAuth authentication cancelled."));
719
+ return;
720
+ }
721
+ const spinner = ora("Starting OAuth flow...").start();
722
+ // Generate PKCE challenge - state = verifier (OpenCode's approach)
723
+ const codeVerifier = randomBytes(32).toString("base64url");
724
+ const codeChallenge = createHash("sha256")
725
+ .update(codeVerifier)
726
+ .digest("base64url");
727
+ // Build authorization URL using claude.ai (NOT console.anthropic.com)
728
+ // This is the direct OAuth flow for Claude Pro/Max
729
+ const authUrl = new URL(ANTHROPIC_OAUTH_CONFIG.authorizationUrl);
730
+ authUrl.searchParams.set("code", "true");
731
+ authUrl.searchParams.set("client_id", ANTHROPIC_OAUTH_CONFIG.clientId);
732
+ authUrl.searchParams.set("response_type", "code");
733
+ authUrl.searchParams.set("redirect_uri", ANTHROPIC_OAUTH_CONFIG.redirectUri);
734
+ authUrl.searchParams.set("scope", ANTHROPIC_OAUTH_CONFIG.scope);
735
+ authUrl.searchParams.set("code_challenge", codeChallenge);
736
+ authUrl.searchParams.set("code_challenge_method", "S256");
737
+ // OpenCode sets state = verifier for simplicity
738
+ authUrl.searchParams.set("state", codeVerifier);
739
+ spinner.text = "Opening browser for authentication...";
740
+ // Open browser
741
+ try {
742
+ await openBrowser(authUrl.toString());
743
+ spinner.succeed("Browser opened for authentication");
744
+ }
745
+ catch {
746
+ spinner.warn("Could not open browser automatically");
747
+ logger.always("");
748
+ logger.always(chalk.yellow("Please open this URL manually:"));
749
+ logger.always(chalk.cyan(authUrl.toString()));
750
+ }
751
+ logger.always("");
752
+ logger.always(chalk.blue("═".repeat(60)));
753
+ logger.always(chalk.blue.bold(" Complete authentication in your browser"));
754
+ logger.always(chalk.blue("═".repeat(60)));
755
+ logger.always("");
756
+ logger.always("1. Sign in to your Claude account in the browser");
757
+ logger.always("2. Authorize the application");
758
+ logger.always("3. Copy the authorization code shown on the page");
759
+ logger.always("4. Paste the code below");
760
+ logger.always("");
761
+ // Prompt user to enter the authorization code
762
+ const { authCode } = await inquirer.prompt([
763
+ {
764
+ type: "input",
765
+ name: "authCode",
766
+ message: "Paste the authorization code:",
767
+ validate: (input) => {
768
+ if (!input.trim()) {
769
+ return "Authorization code is required";
770
+ }
771
+ if (input.trim().length < 10) {
772
+ return "Authorization code seems too short";
773
+ }
774
+ return true;
775
+ },
776
+ },
777
+ ]);
778
+ const trimmedCode = authCode.trim();
779
+ const exchangeSpinner = ora("Exchanging authorization code for tokens...").start();
780
+ // Parse code#state format (e.g., "abc123#xyz789")
781
+ // OpenCode sets state = verifier in the auth URL, so the state
782
+ // returned in code#state IS the original verifier used for the code challenge
783
+ const codeParts = trimmedCode.split("#");
784
+ const actualCode = codeParts[0];
785
+ const codeState = codeParts[1] || codeVerifier;
786
+ // Use the state as the verifier (since we set state = verifier in the auth URL)
787
+ const actualVerifier = codeState;
788
+ // Exchange code for tokens with Claude CLI User-Agent
789
+ // IMPORTANT: Uses JSON body, not URLSearchParams
790
+ const tokenResponse = await fetch(ANTHROPIC_OAUTH_CONFIG.tokenUrl, {
791
+ method: "POST",
792
+ headers: {
793
+ "Content-Type": "application/json",
794
+ },
795
+ body: JSON.stringify({
796
+ code: actualCode,
797
+ state: codeState,
798
+ grant_type: "authorization_code",
799
+ client_id: ANTHROPIC_OAUTH_CONFIG.clientId,
800
+ redirect_uri: ANTHROPIC_OAUTH_CONFIG.redirectUri,
801
+ code_verifier: actualVerifier,
802
+ }),
803
+ });
804
+ if (!tokenResponse.ok) {
805
+ const errorText = await tokenResponse.text();
806
+ exchangeSpinner.fail("Token exchange failed");
807
+ throw new Error(`Token exchange failed: ${tokenResponse.status} - ${errorText}`);
808
+ }
809
+ const tokenData = (await tokenResponse.json());
810
+ // Save tokens
811
+ const tokens = {
812
+ accessToken: tokenData.access_token,
813
+ refreshToken: tokenData.refresh_token,
814
+ expiresAt: tokenData.expires_in
815
+ ? Date.now() + tokenData.expires_in * 1000
816
+ : undefined,
817
+ tokenType: tokenData.token_type || "Bearer",
818
+ scope: tokenData.scope,
819
+ };
820
+ // Detect subscription tier if possible
821
+ const subscriptionTier = await detectSubscriptionTier(tokens.accessToken);
822
+ await saveStoredCredentials(provider, {
823
+ type: "oauth",
824
+ oauth: tokens,
825
+ provider,
826
+ subscriptionTier,
827
+ createdAt: Date.now(),
828
+ updatedAt: Date.now(),
829
+ });
830
+ exchangeSpinner.succeed("OAuth authentication successful!");
831
+ logger.always("");
832
+ logger.always(chalk.green("═".repeat(60)));
833
+ logger.always(chalk.green.bold(" Authentication configured successfully!"));
834
+ logger.always(chalk.green("═".repeat(60)));
835
+ logger.always("");
836
+ if (subscriptionTier) {
837
+ logger.always(` Subscription Tier: ${chalk.blue(subscriptionTier)}`);
838
+ }
839
+ logger.always(` Token expires: ${tokens.expiresAt ? new Date(tokens.expiresAt).toLocaleString() : "Never"}`);
840
+ logger.always(` Refresh token: ${tokens.refreshToken ? chalk.green("Available") : chalk.yellow("Not available")}`);
841
+ showUsageExample(provider);
842
+ }
843
+ // =============================================================================
844
+ // HELPER FUNCTIONS
845
+ // =============================================================================
846
+ /**
847
+ * Get authentication status for a provider
848
+ * Priority: OAuth > stored API key > environment API key
849
+ */
850
+ async function getAuthStatus(provider) {
851
+ const result = {
852
+ provider,
853
+ isAuthenticated: false,
854
+ method: "none",
855
+ };
856
+ // Check stored credentials FIRST (OAuth takes priority over API key)
857
+ const stored = await getStoredCredentials(provider);
858
+ if (stored) {
859
+ // OAuth credentials take highest priority
860
+ if (stored.type === "oauth" && stored.oauth) {
861
+ result.isAuthenticated = true;
862
+ result.method = "oauth";
863
+ result.subscriptionTier = stored.subscriptionTier;
864
+ result.hasRefreshToken = !!stored.oauth.refreshToken;
865
+ if (stored.oauth.expiresAt) {
866
+ result.tokenExpiry = new Date(stored.oauth.expiresAt).toLocaleString();
867
+ result.needsRefresh = Date.now() >= stored.oauth.expiresAt;
868
+ }
869
+ return result;
870
+ }
871
+ // Stored API key is second priority
872
+ if (stored.type === "api-key" && stored.apiKey) {
873
+ result.isAuthenticated = true;
874
+ result.method = "api-key";
875
+ return result;
876
+ }
877
+ }
878
+ // Fall back to environment API key
879
+ const envKey = getEnvApiKey(provider);
880
+ if (envKey) {
881
+ result.isAuthenticated = true;
882
+ result.method = "api-key";
883
+ return result;
884
+ }
885
+ return result;
886
+ }
887
+ /**
888
+ * Detect subscription tier from token (if API supports it)
889
+ */
890
+ async function detectSubscriptionTier(accessToken) {
891
+ try {
892
+ // Attempt to call an endpoint that returns user info
893
+ // This is a placeholder - actual implementation depends on Anthropic's API
894
+ const response = await fetch("https://api.anthropic.com/v1/me", {
895
+ headers: {
896
+ Authorization: `Bearer ${accessToken}`,
897
+ "Content-Type": "application/json",
898
+ },
899
+ });
900
+ if (response.ok) {
901
+ const data = (await response.json());
902
+ if (data.subscription) {
903
+ return data.subscription;
904
+ }
905
+ }
906
+ }
907
+ catch {
908
+ // Ignore errors - subscription detection is optional
909
+ }
910
+ return undefined;
911
+ }
912
+ /**
913
+ * Open URL in the default browser (cross-platform)
914
+ */
915
+ async function openBrowser(url) {
916
+ return new Promise((resolve, reject) => {
917
+ const platform = process.platform;
918
+ let command;
919
+ let args;
920
+ switch (platform) {
921
+ case "darwin":
922
+ command = "open";
923
+ args = [url];
924
+ break;
925
+ case "win32":
926
+ command = "cmd";
927
+ args = ["/c", "start", "", url];
928
+ break;
929
+ default:
930
+ // Linux and other Unix-like systems
931
+ command = "xdg-open";
932
+ args = [url];
933
+ }
934
+ // Use execFile instead of exec to prevent command injection
935
+ execFile(command, args, (error) => {
936
+ if (error) {
937
+ reject(error);
938
+ }
939
+ else {
940
+ resolve();
941
+ }
942
+ });
943
+ });
944
+ }
945
+ /**
946
+ * Get environment variable name for a provider
947
+ */
948
+ function getEnvVarName(provider) {
949
+ switch (provider) {
950
+ case "anthropic":
951
+ return "ANTHROPIC_API_KEY";
952
+ default:
953
+ return `${provider.toUpperCase()}_API_KEY`;
954
+ }
955
+ }
956
+ /**
957
+ * Get API key from environment
958
+ */
959
+ function getEnvApiKey(provider) {
960
+ return process.env[getEnvVarName(provider)];
961
+ }
962
+ /**
963
+ * Get stored credentials from file
964
+ */
965
+ async function getStoredCredentials(provider) {
966
+ const credentialsFile = path.join(NEUROLINK_CONFIG_DIR, `${provider}-credentials.json`);
967
+ try {
968
+ if (fs.existsSync(credentialsFile)) {
969
+ const data = fs.readFileSync(credentialsFile, "utf-8");
970
+ return JSON.parse(data);
971
+ }
972
+ }
973
+ catch (error) {
974
+ logger.debug(`Failed to read credentials: ${error}`);
975
+ }
976
+ return null;
977
+ }
978
+ /**
979
+ * Save credentials to file
980
+ */
981
+ async function saveStoredCredentials(provider, credentials) {
982
+ // Ensure config directory exists
983
+ if (!fs.existsSync(NEUROLINK_CONFIG_DIR)) {
984
+ fs.mkdirSync(NEUROLINK_CONFIG_DIR, { recursive: true });
985
+ }
986
+ const credentialsFile = path.join(NEUROLINK_CONFIG_DIR, `${provider}-credentials.json`);
987
+ fs.writeFileSync(credentialsFile, JSON.stringify(credentials, null, 2), {
988
+ mode: 0o600, // Restrict permissions
989
+ });
990
+ }
991
+ /**
992
+ * Check existing authentication status
993
+ */
994
+ async function checkExistingAuth(provider) {
995
+ const envKey = getEnvApiKey(provider);
996
+ if (envKey) {
997
+ return { hasValidAuth: true, type: "api-key", credential: envKey };
998
+ }
999
+ const stored = await getStoredCredentials(provider);
1000
+ if (stored) {
1001
+ if (stored.type === "api-key" && stored.apiKey) {
1002
+ return { hasValidAuth: true, type: "api-key", credential: stored.apiKey };
1003
+ }
1004
+ if (stored.type === "oauth" && stored.oauth) {
1005
+ // Check if token is still valid
1006
+ if (stored.oauth.expiresAt) {
1007
+ const isExpired = Date.now() >= stored.oauth.expiresAt;
1008
+ if (!isExpired || stored.oauth.refreshToken) {
1009
+ return { hasValidAuth: true, type: "oauth" };
1010
+ }
1011
+ }
1012
+ else {
1013
+ return { hasValidAuth: true, type: "oauth" };
1014
+ }
1015
+ }
1016
+ }
1017
+ return { hasValidAuth: false };
1018
+ }
1019
+ /**
1020
+ * Validate API key by making a test request
1021
+ */
1022
+ async function validateApiKey(provider, apiKey) {
1023
+ if (provider === "anthropic") {
1024
+ try {
1025
+ // Simple validation - check message API
1026
+ const response = await fetch("https://api.anthropic.com/v1/messages", {
1027
+ method: "POST",
1028
+ headers: {
1029
+ "Content-Type": "application/json",
1030
+ "x-api-key": apiKey,
1031
+ "anthropic-version": "2023-06-01",
1032
+ },
1033
+ body: JSON.stringify({
1034
+ model: "claude-3-haiku-20240307",
1035
+ max_tokens: 1,
1036
+ messages: [{ role: "user", content: "Hi" }],
1037
+ }),
1038
+ });
1039
+ // 200 means valid, 401 means invalid key
1040
+ // Other errors (rate limit, etc.) still mean the key format is valid
1041
+ return response.status !== 401;
1042
+ }
1043
+ catch {
1044
+ // Network error - assume key format is valid
1045
+ return true;
1046
+ }
1047
+ }
1048
+ return true;
1049
+ }
1050
+ /**
1051
+ * Save API key to .env file
1052
+ */
1053
+ async function saveToEnvFile(provider, apiKey) {
1054
+ const envVar = getEnvVarName(provider);
1055
+ let content = "";
1056
+ if (fs.existsSync(ENV_FILE_PATH)) {
1057
+ content = fs.readFileSync(ENV_FILE_PATH, "utf-8");
1058
+ }
1059
+ // Check if variable already exists
1060
+ const regex = new RegExp(`^${envVar}=.*$`, "m");
1061
+ if (regex.test(content)) {
1062
+ // Replace existing
1063
+ content = content.replace(regex, `${envVar}=${apiKey}`);
1064
+ }
1065
+ else {
1066
+ // Add new
1067
+ if (content && !content.endsWith("\n")) {
1068
+ content += "\n";
1069
+ }
1070
+ content += `${envVar}=${apiKey}\n`;
1071
+ }
1072
+ fs.writeFileSync(ENV_FILE_PATH, content);
1073
+ }
1074
+ /**
1075
+ * Remove variable from .env file
1076
+ */
1077
+ async function removeFromEnvFile(envVar) {
1078
+ if (!fs.existsSync(ENV_FILE_PATH)) {
1079
+ return;
1080
+ }
1081
+ let content = fs.readFileSync(ENV_FILE_PATH, "utf-8");
1082
+ const regex = new RegExp(`^${envVar}=.*\n?`, "m");
1083
+ content = content.replace(regex, "");
1084
+ fs.writeFileSync(ENV_FILE_PATH, content);
1085
+ }
1086
+ /**
1087
+ * Mask credential for display
1088
+ */
1089
+ function maskCredential(credential) {
1090
+ if (!credential || credential.length < 8) {
1091
+ return "****";
1092
+ }
1093
+ const knownPrefixes = ["sk-ant-", "sk-"];
1094
+ const prefix = knownPrefixes.find((p) => credential.startsWith(p)) ??
1095
+ credential.slice(0, 4);
1096
+ const end = credential.slice(-4);
1097
+ const stars = "*".repeat(Math.max(4, credential.length - prefix.length - 4));
1098
+ return `${prefix}${stars}${end}`;
1099
+ }
1100
+ /**
1101
+ * Show usage example after successful authentication
1102
+ */
1103
+ function showUsageExample(provider) {
1104
+ logger.always("");
1105
+ logger.always(chalk.green("You can now use the NeuroLink CLI with this provider:"));
1106
+ logger.always(chalk.cyan(` neurolink generate "Hello!" --provider ${provider}`));
1107
+ logger.always(chalk.cyan(` neurolink generate "Explain quantum computing" --provider ${provider}`));
1108
+ logger.always("");
1109
+ logger.always(chalk.blue("To check authentication status:"));
1110
+ logger.always(chalk.cyan(` neurolink auth status ${provider}`));
1111
+ logger.always("");
1112
+ logger.always(chalk.blue("To logout:"));
1113
+ logger.always(chalk.cyan(` neurolink auth logout ${provider}`));
1114
+ }
1115
+ //# sourceMappingURL=auth.js.map