dexto 1.8.1 → 1.8.2

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.
@@ -1,16 +1,12 @@
1
1
  // packages/cli/src/cli/commands/setup.ts
2
- import { promises as fs } from 'node:fs';
3
- import path from 'node:path';
4
2
  import chalk from 'chalk';
5
3
  import { z } from 'zod';
6
- import open from 'open';
7
4
  import { acceptsAnyModel, getDefaultModelForProvider, getReasoningProfile, getSupportedModels, isValidProviderModel, LLM_PROVIDERS, LLM_REGISTRY, requiresApiKey, supportsCustomModels, } from '@dexto/llm';
8
- import { CodexAppServerClient, createCodexBaseURL, getCodexAuthModeLabel, getCodexProviderDisplayName, getCuratedModelsForProvider, isCodexBaseURL, logger, parseCodexBaseURL, resolveApiKeyForProvider, } from '@dexto/core';
9
- import { createInitialPreferences, saveGlobalPreferences, loadGlobalPreferences, getGlobalPreferencesPath, updateGlobalPreferences, setActiveModel, isDextoAuthEnabled, loadCustomModels, saveCustomModel, deleteCustomModel, globalPreferencesExist, getDextoGlobalPath, } from '@dexto/agent-management';
5
+ import { getCuratedModelsForProvider, logger, resolveApiKeyForProvider } from '@dexto/core';
6
+ import { createInitialPreferences, saveGlobalPreferences, loadGlobalPreferences, getGlobalPreferencesPath, updateGlobalPreferences, setActiveModel, isDextoAuthEnabled, loadCustomModels, saveCustomModel, deleteCustomModel, globalPreferencesExist, getDefaultModelAuthProfile, loadModelAuthProfilesSync, } from '@dexto/agent-management';
10
7
  import { interactiveApiKeySetup, hasApiKeyConfigured } from '../utils/api-key-setup.js';
11
8
  import { selectProvider, getProviderDisplayName, getProviderEnvVar, providerRequiresBaseURL, getDefaultModel, } from '../utils/provider-setup.js';
12
9
  import { setupLocalModels, setupOllamaModels, hasSelectedModel, getModelFromResult, } from '../utils/local-model-setup.js';
13
- import { executeCommand } from '../utils/self-management.js';
14
10
  import { requiresSetup } from '../utils/setup-utils.js';
15
11
  import { canUseDextoProvider } from '../utils/dexto-setup.js';
16
12
  import { handleAutoLogin } from './auth/login.js';
@@ -77,13 +73,6 @@ const REASONING_VARIANT_HINTS = {
77
73
  max: 'Maximum reasoning within provider limits',
78
74
  xhigh: 'Extra high reasoning',
79
75
  };
80
- const OPENAI_CODEX_PACKAGE = '@openai/codex';
81
- const DEXTO_DEPS_PACKAGE_JSON = {
82
- name: 'dexto-deps',
83
- version: '1.0.0',
84
- private: true,
85
- description: 'Managed dependencies for Dexto',
86
- };
87
76
  function toReasoningVariantLabel(variant, defaultVariant) {
88
77
  const normalized = variant.toLowerCase();
89
78
  const withKnownCasing = normalized === 'xhigh'
@@ -104,6 +93,16 @@ function getReasoningVariantSelectOptions(variants, defaultVariant) {
104
93
  hint: REASONING_VARIANT_HINTS[variant] ?? 'Model/provider-native reasoning variant',
105
94
  }));
106
95
  }
96
+ function hasUsableModelAuthProfile(provider) {
97
+ const profile = getDefaultModelAuthProfile(loadModelAuthProfilesSync(), provider);
98
+ if (!profile) {
99
+ return false;
100
+ }
101
+ if (profile.credential.type === 'api_key_env') {
102
+ return Boolean(process.env[profile.credential.envVar]?.trim());
103
+ }
104
+ return true;
105
+ }
107
106
  /**
108
107
  * Get the steps to display for the current provider and model.
109
108
  * - Local/Ollama providers skip the API Key step.
@@ -358,7 +357,7 @@ async function handleQuickStart(options = { onCancel: 'exit' }) {
358
357
  const apiKeyVar = getProviderEnvVar(provider);
359
358
  let apiKeySkipped = false;
360
359
  // Check if API key exists
361
- const hasKey = hasApiKeyConfigured(provider);
360
+ const hasKey = hasApiKeyConfigured(provider) || hasUsableModelAuthProfile(provider);
362
361
  if (!hasKey) {
363
362
  const providerName = getProviderDisplayName(provider);
364
363
  p.note(`${providerName} is ${chalk.green('free')} to use!\n\n` +
@@ -411,7 +410,7 @@ async function handleQuickStart(options = { onCancel: 'exit' }) {
411
410
  apiKeyPending: apiKeySkipped,
412
411
  };
413
412
  // Only include apiKeyVar if not skipped
414
- if (!apiKeySkipped) {
413
+ if (!apiKeySkipped && hasApiKeyConfigured(provider)) {
415
414
  preferencesOptions.apiKeyVar = apiKeyVar;
416
415
  }
417
416
  const preferences = createInitialPreferences(preferencesOptions);
@@ -428,316 +427,6 @@ async function handleQuickStart(options = { onCancel: 'exit' }) {
428
427
  return 'completed';
429
428
  }
430
429
  }
431
- function getConfiguredProviderDisplayName(provider, baseURL) {
432
- if (provider === 'openai-compatible') {
433
- const codex = parseCodexBaseURL(baseURL);
434
- if (codex) {
435
- return getCodexProviderDisplayName(codex.authMode);
436
- }
437
- }
438
- return getProviderDisplayName(provider);
439
- }
440
- function isCodexConfigured(provider, baseURL) {
441
- return provider === 'openai-compatible' && isCodexBaseURL(baseURL);
442
- }
443
- async function ensureDextoDepsPackageJson() {
444
- const depsDir = getDextoGlobalPath('deps');
445
- await fs.mkdir(depsDir, { recursive: true });
446
- const packageJsonPath = path.join(depsDir, 'package.json');
447
- try {
448
- await fs.access(packageJsonPath);
449
- }
450
- catch (error) {
451
- if (error.code !== 'ENOENT') {
452
- throw error;
453
- }
454
- await fs.writeFile(packageJsonPath, JSON.stringify(DEXTO_DEPS_PACKAGE_JSON, null, 2), 'utf-8');
455
- }
456
- return depsDir;
457
- }
458
- function isMissingCodexCliError(error) {
459
- if (!(error instanceof Error)) {
460
- return false;
461
- }
462
- const code = error.code;
463
- return (error.message.includes('Codex CLI not found on PATH') ||
464
- error.message.includes('spawn codex ENOENT') ||
465
- (code === 'ENOENT' && error.message.includes('spawn')));
466
- }
467
- function getCodexSetupErrorMessage(error) {
468
- if (isMissingCodexCliError(error)) {
469
- return 'Codex CLI not found on PATH. Install Codex to use ChatGPT Login in Dexto.';
470
- }
471
- return error instanceof Error ? error.message : String(error);
472
- }
473
- async function resolveCodexInstaller() {
474
- const candidates = [
475
- {
476
- command: 'npm',
477
- args: ['install', OPENAI_CODEX_PACKAGE, '--no-audit', '--no-fund'],
478
- label: 'npm',
479
- },
480
- {
481
- command: 'pnpm',
482
- args: ['add', OPENAI_CODEX_PACKAGE],
483
- label: 'pnpm',
484
- },
485
- {
486
- command: 'bun',
487
- args: ['add', OPENAI_CODEX_PACKAGE],
488
- label: 'bun',
489
- },
490
- ];
491
- for (const candidate of candidates) {
492
- const probe = await executeCommand(candidate.command, ['--version']);
493
- if (probe.code === 0) {
494
- return candidate;
495
- }
496
- }
497
- return null;
498
- }
499
- function getCodexInstallerFailureMessage(installer, result) {
500
- const details = `${result.stderr}\n${result.stdout}`
501
- .split(/\r?\n/)
502
- .map((line) => line.trim())
503
- .filter((line) => line.length > 0);
504
- const lastLine = details.at(-1);
505
- return lastLine
506
- ? `Failed to install the OpenAI Codex CLI via ${installer.label}: ${lastLine}`
507
- : `Failed to install the OpenAI Codex CLI via ${installer.label}.`;
508
- }
509
- async function installManagedCodexCli() {
510
- const depsDir = await ensureDextoDepsPackageJson();
511
- const installer = await resolveCodexInstaller();
512
- if (!installer) {
513
- throw new Error('Could not find npm, pnpm, or bun to install the OpenAI Codex CLI automatically.');
514
- }
515
- const result = await executeCommand(installer.command, installer.args, { cwd: depsDir });
516
- if (result.code !== 0) {
517
- throw new Error(getCodexInstallerFailureMessage(installer, result));
518
- }
519
- }
520
- async function createCodexClientForSetup() {
521
- try {
522
- return await CodexAppServerClient.create();
523
- }
524
- catch (error) {
525
- if (!isMissingCodexCliError(error)) {
526
- throw error;
527
- }
528
- const spinner = p.spinner();
529
- spinner.start('Installing OpenAI Codex CLI...');
530
- try {
531
- await installManagedCodexCli();
532
- spinner.stop('OpenAI Codex CLI installed');
533
- }
534
- catch (installError) {
535
- spinner.stop('OpenAI Codex CLI installation failed');
536
- throw installError;
537
- }
538
- return await CodexAppServerClient.create();
539
- }
540
- }
541
- async function ensureCodexChatGPTLogin(client) {
542
- const spinner = p.spinner();
543
- spinner.start('Starting ChatGPT login with Codex...');
544
- const login = await client.startLogin({ type: 'chatgpt' });
545
- if (login.type !== 'chatgpt') {
546
- spinner.stop('ChatGPT login failed');
547
- throw new Error('Codex did not return a ChatGPT login URL');
548
- }
549
- spinner.stop('ChatGPT login ready');
550
- try {
551
- await open(login.authUrl);
552
- p.log.success('Opened your browser for ChatGPT login');
553
- }
554
- catch (error) {
555
- const errorMessage = error instanceof Error ? error.message : String(error);
556
- p.log.warn(`Could not open browser automatically: ${errorMessage}`);
557
- }
558
- p.note(`Finish the ChatGPT login in your browser.\n\n${chalk.dim(login.authUrl)}`, 'ChatGPT Login');
559
- const waitSpinner = p.spinner();
560
- waitSpinner.start('Waiting for ChatGPT login to complete...');
561
- const completed = await client.waitForLoginCompleted(login.loginId, {
562
- timeoutMs: 5 * 60 * 1000,
563
- });
564
- if (!completed.success) {
565
- waitSpinner.stop('ChatGPT login failed');
566
- throw new Error(completed.error ?? 'Codex ChatGPT login failed');
567
- }
568
- waitSpinner.stop('ChatGPT login complete');
569
- return await client.readAccount(true);
570
- }
571
- async function ensureCodexChatGPTSession(client) {
572
- const current = await client.readAccount(false);
573
- if (current.account?.type === 'chatgpt') {
574
- return current;
575
- }
576
- if (current.account) {
577
- const currentLabel = 'OpenAI API key';
578
- const shouldSwitch = await p.confirm({
579
- message: `Codex is currently using ${currentLabel}. Switch to ChatGPT login?`,
580
- initialValue: true,
581
- });
582
- if (p.isCancel(shouldSwitch) || !shouldSwitch) {
583
- return null;
584
- }
585
- await client.logout();
586
- }
587
- return await ensureCodexChatGPTLogin(client);
588
- }
589
- function getCodexModelOptions(models) {
590
- const visibleModels = models.filter((model) => !model.hidden);
591
- const defaultModel = visibleModels.find((model) => model.isDefault) ?? visibleModels[0] ?? null;
592
- if (!defaultModel) {
593
- return [];
594
- }
595
- const secondaryModels = visibleModels
596
- .filter((model) => model.model !== defaultModel.model)
597
- .slice(0, 9);
598
- return [
599
- {
600
- value: defaultModel.model,
601
- label: defaultModel.displayName,
602
- hint: `${defaultModel.description || 'Recommended'} (recommended)`,
603
- },
604
- ...secondaryModels.map((model) => ({
605
- value: model.model,
606
- label: model.displayName,
607
- hint: model.description || model.model,
608
- })),
609
- ];
610
- }
611
- /**
612
- * ChatGPT Login setup flow - authenticate with ChatGPT through Codex, choose a model, save preferences
613
- *
614
- * Config storage:
615
- * - provider: 'openai-compatible'
616
- * - baseURL: special 'codex://chatgpt' URI resolved at runtime to Codex app-server
617
- * - model: model ID returned by Codex model/list
618
- */
619
- async function handleCodexProviderSetup(options = {}) {
620
- const exitOnCancel = options.exitOnCancel ?? true;
621
- const abort = (message, exitCode = 0) => {
622
- p.cancel(message);
623
- if (exitOnCancel) {
624
- process.exit(exitCode);
625
- }
626
- return false;
627
- };
628
- console.log(chalk.cyan('\nChatGPT Login Setup\n'));
629
- let client = null;
630
- try {
631
- client = await createCodexClientForSetup();
632
- const account = await ensureCodexChatGPTSession(client);
633
- if (!account || account.account?.type !== 'chatgpt') {
634
- return abort('Setup cancelled');
635
- }
636
- p.log.success(`Codex authenticated with ChatGPT as ${account.account.email} (${account.account.planType})`);
637
- const models = await client.listModels();
638
- const modelOptions = getCodexModelOptions(models);
639
- if (modelOptions.length === 0) {
640
- p.log.error('Codex did not return any available models.');
641
- return abort('Setup cancelled', 1);
642
- }
643
- const model = await p.select({
644
- message: 'Select a model to start with',
645
- options: modelOptions,
646
- });
647
- if (p.isCancel(model)) {
648
- return abort('Setup cancelled');
649
- }
650
- const selectedModel = model;
651
- const provider = 'openai-compatible';
652
- const baseURL = createCodexBaseURL('chatgpt');
653
- const codexReasoningProfile = getReasoningProfile(provider, selectedModel);
654
- let reasoningPreset;
655
- if (codexReasoningProfile.capable) {
656
- const selectedReasoning = await p.select({
657
- message: 'Select reasoning variant',
658
- options: getReasoningVariantSelectOptions(codexReasoningProfile.supportedVariants, codexReasoningProfile.defaultVariant),
659
- ...(codexReasoningProfile.defaultVariant
660
- ? { initialValue: codexReasoningProfile.defaultVariant }
661
- : {}),
662
- });
663
- if (p.isCancel(selectedReasoning)) {
664
- return abort('Setup cancelled');
665
- }
666
- if (selectedReasoning !== codexReasoningProfile.defaultVariant) {
667
- reasoningPreset = selectedReasoning;
668
- }
669
- }
670
- const defaultMode = await selectDefaultMode();
671
- if (defaultMode === null) {
672
- return abort('Setup cancelled');
673
- }
674
- const preferences = createInitialPreferences({
675
- provider,
676
- model: selectedModel,
677
- baseURL,
678
- defaultMode,
679
- setupCompleted: true,
680
- apiKeyPending: false,
681
- ...(reasoningPreset ? { reasoning: { variant: reasoningPreset } } : {}),
682
- });
683
- await saveGlobalPreferences(preferences);
684
- capture('dexto_setup', {
685
- provider,
686
- model: selectedModel,
687
- setupMode: 'interactive',
688
- setupVariant: 'codex-chatgpt',
689
- defaultMode,
690
- hasBaseURL: true,
691
- });
692
- await showSetupComplete(provider, selectedModel, defaultMode, false, {
693
- providerLabel: getCodexProviderDisplayName('chatgpt'),
694
- authLabel: getCodexAuthModeLabel('chatgpt'),
695
- baseURL,
696
- });
697
- return true;
698
- }
699
- catch (error) {
700
- const errorMessage = getCodexSetupErrorMessage(error);
701
- p.log.error(`ChatGPT Login setup failed: ${errorMessage}`);
702
- return abort('Setup cancelled', 1);
703
- }
704
- finally {
705
- if (client) {
706
- await client.close().catch(() => undefined);
707
- }
708
- }
709
- }
710
- async function handleCodexChatGPTLoginRefresh(options = {}) {
711
- const exitOnCancel = options.exitOnCancel ?? true;
712
- const abort = (message, exitCode = 0) => {
713
- p.cancel(message);
714
- if (exitOnCancel) {
715
- process.exit(exitCode);
716
- }
717
- return false;
718
- };
719
- console.log(chalk.cyan('\nChatGPT Login\n'));
720
- let client = null;
721
- try {
722
- client = await createCodexClientForSetup();
723
- const account = await ensureCodexChatGPTSession(client);
724
- if (!account || account.account?.type !== 'chatgpt') {
725
- return abort('ChatGPT login cancelled');
726
- }
727
- p.log.success(`Codex authenticated with ChatGPT as ${account.account.email} (${account.account.planType})`);
728
- return true;
729
- }
730
- catch (error) {
731
- const errorMessage = getCodexSetupErrorMessage(error);
732
- p.log.error(`ChatGPT Login failed: ${errorMessage}`);
733
- return abort('ChatGPT login cancelled', 1);
734
- }
735
- finally {
736
- if (client) {
737
- await client.close().catch(() => undefined);
738
- }
739
- }
740
- }
741
430
  /**
742
431
  * Dexto setup flow - login if needed, select model, save preferences
743
432
  *
@@ -990,10 +679,6 @@ async function wizardStepSetupType(state) {
990
679
  });
991
680
  }
992
681
  options.push({
993
- value: 'openai-codex',
994
- label: `${chalk.green('●')} ChatGPT Login`,
995
- hint: 'Use your ChatGPT account through Codex',
996
- }, {
997
682
  value: 'quick',
998
683
  label: `${chalk.blue('●')} Quick Start`,
999
684
  hint: 'Google Gemini (free) - no account needed',
@@ -1015,13 +700,6 @@ async function wizardStepSetupType(state) {
1015
700
  await handleDextoProviderSetup();
1016
701
  return { ...state, step: 'complete', quickStartHandled: true };
1017
702
  }
1018
- if (setupType === 'openai-codex') {
1019
- const completed = await handleCodexProviderSetup({ exitOnCancel: false });
1020
- if (!completed) {
1021
- return { ...state, step: 'setupType' };
1022
- }
1023
- return { ...state, step: 'complete', quickStartHandled: true };
1024
- }
1025
703
  if (setupType === 'quick') {
1026
704
  // Quick start bypasses the wizard - handle it directly
1027
705
  const result = await handleQuickStart({
@@ -1140,7 +818,7 @@ async function wizardStepApiKey(state) {
1140
818
  const provider = state.provider;
1141
819
  const model = state.model;
1142
820
  showStepProgress('apiKey', provider, model);
1143
- const hasKey = hasApiKeyConfigured(provider);
821
+ const hasKey = hasApiKeyConfigured(provider) || hasUsableModelAuthProfile(provider);
1144
822
  const needsApiKey = requiresApiKey(provider);
1145
823
  if (needsApiKey && !hasKey) {
1146
824
  const result = await interactiveApiKeySetup(provider, {
@@ -1184,7 +862,9 @@ async function wizardStepMode(state) {
1184
862
  defaultMode: undefined,
1185
863
  };
1186
864
  }
1187
- const canShowApiKeyStep = requiresApiKey(provider) && !hasApiKeyConfigured(provider);
865
+ const canShowApiKeyStep = requiresApiKey(provider) &&
866
+ !hasApiKeyConfigured(provider) &&
867
+ !hasUsableModelAuthProfile(provider);
1188
868
  let prevStep = 'model';
1189
869
  if (canShowApiKeyStep) {
1190
870
  prevStep = 'apiKey';
@@ -1611,7 +1291,7 @@ async function saveWizardPreferences(state) {
1611
1291
  setupCompleted: true,
1612
1292
  apiKeyPending: apiKeySkipped,
1613
1293
  };
1614
- if (needsApiKey && !apiKeySkipped) {
1294
+ if (needsApiKey && !apiKeySkipped && hasApiKeyConfigured(provider)) {
1615
1295
  preferencesOptions.apiKeyVar = apiKeyVar;
1616
1296
  }
1617
1297
  if (state.baseURL) {
@@ -1632,17 +1312,7 @@ async function saveWizardPreferences(state) {
1632
1312
  hasBaseURL: Boolean(state.baseURL),
1633
1313
  apiKeySkipped,
1634
1314
  });
1635
- const codex = parseCodexBaseURL(state.baseURL);
1636
- const codexSetupOptions = codex && typeof state.baseURL === 'string'
1637
- ? {
1638
- providerLabel: getCodexProviderDisplayName(codex.authMode),
1639
- authLabel: getCodexAuthModeLabel(codex.authMode),
1640
- baseURL: state.baseURL,
1641
- }
1642
- : {};
1643
- await showSetupComplete(provider, model, defaultMode, apiKeySkipped, {
1644
- ...codexSetupOptions,
1645
- });
1315
+ await showSetupComplete(provider, model, defaultMode, apiKeySkipped);
1646
1316
  }
1647
1317
  /**
1648
1318
  * Non-interactive setup with CLI options
@@ -1709,14 +1379,10 @@ async function showSettingsMenu() {
1709
1379
  }
1710
1380
  // Show current configuration
1711
1381
  if (currentPrefs) {
1712
- const codex = parseCodexBaseURL(currentPrefs.llm.baseURL);
1713
1382
  const currentConfig = [
1714
- `Provider: ${chalk.cyan(getConfiguredProviderDisplayName(currentPrefs.llm.provider, currentPrefs.llm.baseURL))}`,
1383
+ `Provider: ${chalk.cyan(getProviderDisplayName(currentPrefs.llm.provider))}`,
1715
1384
  `Model: ${chalk.cyan(currentPrefs.llm.model)}`,
1716
1385
  `Default Mode: ${chalk.cyan(currentPrefs.defaults.defaultMode)}`,
1717
- ...(codex
1718
- ? [`Authentication: ${chalk.cyan(getCodexAuthModeLabel(codex.authMode))}`]
1719
- : []),
1720
1386
  ...(currentPrefs.llm.baseURL
1721
1387
  ? [`Base URL: ${chalk.cyan(currentPrefs.llm.baseURL)}`]
1722
1388
  : []),
@@ -1732,14 +1398,9 @@ async function showSettingsMenu() {
1732
1398
  p.note(currentConfig, 'Current Configuration');
1733
1399
  }
1734
1400
  const currentProviderLabel = currentPrefs
1735
- ? getConfiguredProviderDisplayName(currentPrefs.llm.provider, currentPrefs.llm.baseURL)
1401
+ ? getProviderDisplayName(currentPrefs.llm.provider)
1736
1402
  : 'not set';
1737
1403
  const currentModelLabel = currentPrefs?.llm.model || 'not set';
1738
- const currentCodex = currentPrefs ? parseCodexBaseURL(currentPrefs.llm.baseURL) : null;
1739
- const authActionLabel = currentCodex ? 'Manage ChatGPT login' : 'Update API key';
1740
- const authActionHint = currentCodex
1741
- ? 'Verify or reconnect your ChatGPT login for Codex'
1742
- : 'Re-enter your API key';
1743
1404
  const options = [
1744
1405
  {
1745
1406
  value: 'model',
@@ -1753,8 +1414,8 @@ async function showSettingsMenu() {
1753
1414
  },
1754
1415
  {
1755
1416
  value: 'auth',
1756
- label: authActionLabel,
1757
- hint: authActionHint,
1417
+ label: 'Update API key',
1418
+ hint: 'Re-enter your API key',
1758
1419
  },
1759
1420
  {
1760
1421
  value: 'reset',
@@ -1800,7 +1461,7 @@ async function showSettingsMenu() {
1800
1461
  await openCreditsPage();
1801
1462
  break;
1802
1463
  case 'auth':
1803
- await updateApiKey(currentPrefs?.llm.provider, currentPrefs?.llm.baseURL);
1464
+ await updateApiKey(currentPrefs?.llm.provider);
1804
1465
  break;
1805
1466
  case 'reset': {
1806
1467
  // Reset exits the menu after completion, but returns to menu if cancelled
@@ -1821,15 +1482,8 @@ async function showSettingsMenu() {
1821
1482
  /**
1822
1483
  * Change model setting (includes provider selection)
1823
1484
  */
1824
- async function changeModel(currentProvider, currentBaseURL) {
1485
+ async function changeModel(currentProvider) {
1825
1486
  let provider = currentProvider ?? null;
1826
- if (isCodexConfigured(provider ?? undefined, currentBaseURL)) {
1827
- const completed = await handleCodexProviderSetup({ exitOnCancel: false });
1828
- if (!completed) {
1829
- p.log.warn('Model change cancelled');
1830
- }
1831
- return;
1832
- }
1833
1487
  // If no provider specified, show selection
1834
1488
  if (!provider) {
1835
1489
  const sourceOptions = [];
@@ -1841,10 +1495,6 @@ async function changeModel(currentProvider, currentBaseURL) {
1841
1495
  });
1842
1496
  }
1843
1497
  sourceOptions.push({
1844
- value: 'openai-codex',
1845
- label: `${chalk.green('●')} ChatGPT Login`,
1846
- hint: 'Use your ChatGPT account through Codex',
1847
- }, {
1848
1498
  value: 'other',
1849
1499
  label: `${chalk.blue('●')} Other providers`,
1850
1500
  hint: 'OpenAI, Anthropic, Gemini, Ollama, etc.',
@@ -1865,13 +1515,6 @@ async function changeModel(currentProvider, currentBaseURL) {
1865
1515
  }
1866
1516
  return;
1867
1517
  }
1868
- if (providerChoice === 'openai-codex') {
1869
- const completed = await handleCodexProviderSetup({ exitOnCancel: false });
1870
- if (!completed) {
1871
- return;
1872
- }
1873
- return;
1874
- }
1875
1518
  // 'other' - fall through to normal provider selection
1876
1519
  }
1877
1520
  // Get provider if not already set
@@ -1950,7 +1593,7 @@ async function changeModel(currentProvider, currentBaseURL) {
1950
1593
  }
1951
1594
  const apiKeyVar = getProviderEnvVar(provider);
1952
1595
  const needsApiKey = requiresApiKey(provider);
1953
- const hasKey = hasApiKeyConfigured(provider);
1596
+ const hasKey = hasApiKeyConfigured(provider) || hasUsableModelAuthProfile(provider);
1954
1597
  // Check if API key is needed and missing - prompt for it
1955
1598
  if (needsApiKey && !hasKey) {
1956
1599
  const result = await interactiveApiKeySetup(provider, {
@@ -1971,7 +1614,7 @@ async function changeModel(currentProvider, currentBaseURL) {
1971
1614
  model,
1972
1615
  };
1973
1616
  // Only include apiKey for providers that need it
1974
- if (needsApiKey) {
1617
+ if (needsApiKey && hasApiKeyConfigured(provider)) {
1975
1618
  llmUpdate.apiKey = `$${apiKeyVar}`;
1976
1619
  }
1977
1620
  // Ask for reasoning variant if applicable
@@ -2007,20 +1650,13 @@ async function changeDefaultMode() {
2007
1650
  /**
2008
1651
  * Update authentication for current provider
2009
1652
  */
2010
- async function updateApiKey(currentProvider, currentBaseURL) {
1653
+ async function updateApiKey(currentProvider) {
2011
1654
  const provider = currentProvider || (await selectProvider());
2012
1655
  // Handle cancellation or back from selectProvider
2013
1656
  if (provider === null || provider === '_back') {
2014
1657
  p.log.warn('API key update cancelled');
2015
1658
  return;
2016
1659
  }
2017
- if (isCodexConfigured(provider, currentBaseURL)) {
2018
- const completed = await handleCodexChatGPTLoginRefresh({ exitOnCancel: false });
2019
- if (!completed) {
2020
- p.log.warn('ChatGPT login update cancelled');
2021
- }
2022
- return;
2023
- }
2024
1660
  // Handle providers that use non-API-key authentication
2025
1661
  if (provider === 'vertex') {
2026
1662
  p.note(`Google Vertex AI uses Application Default Credentials (ADC).\n\n` +
@@ -2305,7 +1941,7 @@ async function promptForBaseURL(provider) {
2305
1941
  async function showSetupComplete(provider, model, defaultMode, apiKeySkipped = false, options = {}) {
2306
1942
  const modeCommand = defaultMode === 'web' ? 'dexto' : `dexto --mode ${defaultMode}`;
2307
1943
  const isLocalProvider = provider === 'local' || provider === 'ollama';
2308
- const providerLabel = options.providerLabel ?? getConfiguredProviderDisplayName(provider, options.baseURL);
1944
+ const providerLabel = options.providerLabel ?? getProviderDisplayName(provider);
2309
1945
  if (apiKeySkipped) {
2310
1946
  console.log(chalk.rgb(255, 165, 0)('\n⚠️ Setup complete (API key pending)\n'));
2311
1947
  }
@@ -46,7 +46,7 @@ process.env.DEXTO_CLI_VERSION = cliVersion;
46
46
  import { logger, startLlmRegistryAutoUpdate, DextoAgent, isPath } from '@dexto/core';
47
47
  import { getAllSupportedModels, getProviderFromModel } from '@dexto/llm';
48
48
  import { applyImageDefaults, cleanNullValues, AgentConfigSchema, loadImage, resolveServicesFromConfig, toDextoAgentOptions, } from '@dexto/agent-config';
49
- import { getDextoPackageRoot, resolveAgentPath, loadAgentConfig, findDextoProjectRoot, globalPreferencesExist, loadGlobalPreferences, resolveBundledScript, enrichAgentConfig, isDextoAuthEnabled, resolveApiKeyForProvider, getPrimaryApiKeyEnvVar, } from '@dexto/agent-management';
49
+ import { getDextoPackageRoot, resolveAgentPath, loadAgentConfig, findDextoProjectRoot, globalPreferencesExist, loadGlobalPreferences, resolveBundledScript, enrichAgentConfig, isDextoAuthEnabled, createModelAuthResolver, resolveApiKeyForProvider, getPrimaryApiKeyEnvVar, } from '@dexto/agent-management';
50
50
  import { validateCliOptions, handleCliOptionsError } from './cli/utils/options.js';
51
51
  import { validateAgentConfig } from './cli/utils/config-validation.js';
52
52
  import { applyCLIOverrides, applyStartupLLMFallback, applyUserPreferences, } from './config/cli-overrides.js';
@@ -208,6 +208,26 @@ program
208
208
  safeExit('setup', 1, 'error');
209
209
  }
210
210
  }));
211
+ program
212
+ .command('connect')
213
+ .description('Connect a model provider auth method')
214
+ .option('--provider <provider>', 'Model provider id')
215
+ .option('--method <method>', 'Auth method id')
216
+ .option('--action <action>', 'Existing profile action: use, replace, or delete')
217
+ .option('--no-interactive', 'Require provider and method flags')
218
+ .action(withAnalytics('connect', async (options) => {
219
+ try {
220
+ const { handleConnectCommand } = await import('./cli/commands/connect.js');
221
+ await handleConnectCommand(options);
222
+ safeExit('connect', 0);
223
+ }
224
+ catch (err) {
225
+ if (err instanceof ExitSignal)
226
+ throw err;
227
+ console.error(`❌ dexto connect command failed: ${err}`);
228
+ safeExit('connect', 1, 'error');
229
+ }
230
+ }));
211
231
  registerAgentsCommand({ program });
212
232
  // 7) `upgrade` SUB-COMMAND
213
233
  program
@@ -357,6 +377,9 @@ async function bootstrapAgentFromGlobalOpts(options) {
357
377
  services,
358
378
  image,
359
379
  hostContext: { workspaceRoot },
380
+ overrides: {
381
+ authResolver: createModelAuthResolver(),
382
+ },
360
383
  }));
361
384
  await agent.start();
362
385
  await (await import('./utils/workspace.js')).applyWorkspaceToAgent(agent, workspaceRoot);
@@ -842,6 +865,7 @@ program
842
865
  overrides: {
843
866
  sessionLoggerFactory,
844
867
  mcpAuthProviderFactory,
868
+ authResolver: createModelAuthResolver(),
845
869
  },
846
870
  }));
847
871
  // Start the agent (initialize async services)