dexto 1.6.20 → 1.6.22

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.
@@ -18,6 +18,7 @@
18
18
  * - Agent capability requirements (requires: { vision: true, toolUse: true })
19
19
  * - Merge strategy configuration for non-LLM fields
20
20
  */
21
+ import { EnvExpandedString, LLM_PROVIDERS, getDefaultModelForProvider, requiresApiKey, requiresBaseURL, resolveApiKeyForProvider, } from '@dexto/core';
21
22
  /**
22
23
  * Applies CLI overrides to an agent configuration
23
24
  * This merges CLI arguments into the base config without validation.
@@ -96,6 +97,62 @@ export function applyUserPreferences(baseConfig, preferences) {
96
97
  }
97
98
  return mergedConfig;
98
99
  }
100
+ export function applyStartupLLMFallback(baseConfig, options) {
101
+ const mergedConfig = JSON.parse(JSON.stringify(baseConfig));
102
+ if (options.hasCompletedSetup ||
103
+ options.hasExplicitProviderOverride ||
104
+ options.hasExplicitModelOverride ||
105
+ options.hasExplicitApiKeyOverride) {
106
+ return mergedConfig;
107
+ }
108
+ if (hasUsableCredentials(mergedConfig.llm.provider, mergedConfig.llm)) {
109
+ return mergedConfig;
110
+ }
111
+ for (const provider of LLM_PROVIDERS) {
112
+ if (!hasExplicitStartupFallbackConfiguration(provider)) {
113
+ continue;
114
+ }
115
+ const model = getDefaultModelForProvider(provider);
116
+ if (!model) {
117
+ continue;
118
+ }
119
+ mergedConfig.llm.provider = provider;
120
+ mergedConfig.llm.model = model;
121
+ delete mergedConfig.llm.baseURL;
122
+ const apiKey = resolveApiKeyForProvider(provider);
123
+ if (apiKey) {
124
+ mergedConfig.llm.apiKey = apiKey;
125
+ }
126
+ else {
127
+ delete mergedConfig.llm.apiKey;
128
+ }
129
+ return mergedConfig;
130
+ }
131
+ return mergedConfig;
132
+ }
133
+ function hasExplicitStartupFallbackConfiguration(provider) {
134
+ if (resolveApiKeyForProvider(provider)?.trim()) {
135
+ return true;
136
+ }
137
+ if (requiresBaseURL(provider) && process.env.OPENAI_BASE_URL?.trim()) {
138
+ return true;
139
+ }
140
+ return false;
141
+ }
142
+ export function hasUsableCredentials(provider, llmConfig) {
143
+ const baseURL = llmConfig?.baseURL ? EnvExpandedString().parse(llmConfig.baseURL) : undefined;
144
+ if (requiresBaseURL(provider) && !baseURL?.trim()) {
145
+ return false;
146
+ }
147
+ if (!requiresApiKey(provider)) {
148
+ return true;
149
+ }
150
+ const inlineApiKey = llmConfig?.apiKey ? EnvExpandedString().parse(llmConfig.apiKey) : '';
151
+ if (inlineApiKey.trim()) {
152
+ return true;
153
+ }
154
+ return Boolean(resolveApiKeyForProvider(provider)?.trim());
155
+ }
99
156
  /**
100
157
  * Check if user's current setup is compatible with an agent's requirements.
101
158
  * Used when switching to non-default agents to warn users about potential issues.
@@ -48,7 +48,7 @@ import { applyImageDefaults, cleanNullValues, AgentConfigSchema, loadImage, reso
48
48
  import { getDextoPackageRoot, resolveAgentPath, loadAgentConfig, findDextoProjectRoot, globalPreferencesExist, loadGlobalPreferences, resolveBundledScript, enrichAgentConfig, isDextoAuthEnabled, } from '@dexto/agent-management';
49
49
  import { validateCliOptions, handleCliOptionsError } from './cli/utils/options.js';
50
50
  import { validateAgentConfig } from './cli/utils/config-validation.js';
51
- import { applyCLIOverrides, applyUserPreferences } from './config/cli-overrides.js';
51
+ import { applyCLIOverrides, applyStartupLLMFallback, applyUserPreferences, } from './config/cli-overrides.js';
52
52
  import { registerRunCommand } from './cli/commands/run/register.js';
53
53
  import { registerSessionCommand } from './cli/commands/session/register.js';
54
54
  import { registerSearchCommand } from './cli/commands/search/register.js';
@@ -112,7 +112,7 @@ program
112
112
  .option('--no-elicitation', 'Disable elicitation (agent cannot prompt user for input)')
113
113
  .option('-c, --continue', 'Continue most recent session (CLI mode)')
114
114
  .option('-r, --resume <sessionId>', 'Resume a session by ID (CLI mode)')
115
- .option('--mode <mode>', 'The application in which dexto should talk to you - web | cli | server | mcp', 'web')
115
+ .option('--mode <mode>', 'The application in which dexto should talk to you - web | cli | server | mcp', 'cli')
116
116
  .option('--port <port>', 'port for the server (default: 3000 for web, 3001 for server mode)')
117
117
  .option('--no-auto-install', 'Disable automatic installation of missing agents from registry')
118
118
  .option('--image <package>', 'Image package to load (e.g., @dexto/image-local). Overrides config image field.')
@@ -387,8 +387,8 @@ program
387
387
  // Main customer facing description
388
388
  .description('Dexto CLI - AI-powered assistant with session management.\n\n' +
389
389
  'Basic Usage:\n' +
390
- ' dexto Start web UI (default)\n' +
391
- ' dexto --mode cli Start interactive CLI\n' +
390
+ ' dexto or dexto --mode cli Start interactive CLI (default)\n' +
391
+ ' dexto --mode web Start web UI\n' +
392
392
  ' dexto --prompt "query" Start interactive CLI and run the prompt\n' +
393
393
  ' dexto run "query" Run one-off headless task\n\n' +
394
394
  'Session Management Commands:\n' +
@@ -451,8 +451,8 @@ program
451
451
  // the first positional argument as an agent name to avoid ambiguity with prompts.
452
452
  // ——— FORCE CLI MODE FOR PROMPT/SESSION FLAGS ———
453
453
  // If a prompt or session flag was provided, force CLI mode.
454
- if ((initialPrompt || opts.continue || opts.resume || opts.cloudAgent) &&
455
- opts.mode !== 'cli') {
454
+ const modeForcedByChatFlags = Boolean(initialPrompt || opts.continue || opts.resume || opts.cloudAgent);
455
+ if (modeForcedByChatFlags && opts.mode !== 'cli') {
456
456
  console.error(`ℹ️ Forcing CLI mode due to --prompt/--continue/--resume/--cloud-agent.`);
457
457
  console.error(` Original mode: ${opts.mode} → Overridden to: cli`);
458
458
  opts.mode = 'cli';
@@ -524,7 +524,34 @@ program
524
524
  // Determine validation mode early - used throughout config loading and agent creation
525
525
  // Use relaxed validation for interactive modes (web/cli) where users can configure later
526
526
  // Use strict validation for non-interactive modes (server/mcp) that need full config upfront
527
- const isInteractiveMode = opts.mode === 'web' || opts.mode === 'cli';
527
+ let isInteractiveMode = opts.mode === 'web' || opts.mode === 'cli';
528
+ const shouldCheckSetupState = !opts.skipSetup && !(opts.agent && isPath(opts.agent));
529
+ let setupRequired = false;
530
+ if (shouldCheckSetupState) {
531
+ const { requiresSetup } = await import('./cli/utils/setup-utils.js');
532
+ setupRequired = await requiresSetup();
533
+ }
534
+ const canRunInteractiveSetup = isInteractiveMode && opts.interactive !== false;
535
+ if (setupRequired && !canRunInteractiveSetup) {
536
+ console.error('❌ Setup required before starting in this mode.');
537
+ console.error('💡 Run `dexto setup` first, or use --skip-setup to bypass global setup.');
538
+ safeExit('main', 1, 'setup-required-non-interactive');
539
+ }
540
+ const shouldRunSetupBeforeStartup = setupRequired && canRunInteractiveSetup && !opts.provider && !opts.model;
541
+ if (shouldRunSetupBeforeStartup) {
542
+ const { handleSetupCommand } = await import('./cli/commands/setup.js');
543
+ await handleSetupCommand({
544
+ interactive: true,
545
+ defaultMode: opts.mode === 'cli' ? 'cli' : undefined,
546
+ });
547
+ if (!explicitModeProvided && !modeForcedByChatFlags) {
548
+ const preferences = await loadGlobalPreferences();
549
+ if (preferences.defaults.defaultMode) {
550
+ opts.mode = preferences.defaults.defaultMode;
551
+ }
552
+ isInteractiveMode = opts.mode === 'web' || opts.mode === 'cli';
553
+ }
554
+ }
528
555
  try {
529
556
  // Case 1: File path - skip all validation and setup
530
557
  if (opts.agent && isPath(opts.agent)) {
@@ -559,31 +586,9 @@ program
559
586
  // Continue anyway - resolver will handle it
560
587
  }
561
588
  }
562
- // Check setup state and auto-trigger if needed
563
- // Skip if --skip-setup flag is set (for MCP mode, automation, etc.)
564
- const { requiresSetup } = await import('./cli/utils/setup-utils.js');
565
- if (!opts.skipSetup && (await requiresSetup())) {
566
- if (opts.interactive === false) {
567
- console.error('❌ Setup required but --no-interactive flag is set.');
568
- console.error('💡 Run `dexto setup` first, or use --skip-setup to bypass global setup.');
569
- safeExit('main', 1, 'setup-required-non-interactive');
570
- }
571
- const { handleSetupCommand } = await import('./cli/commands/setup.js');
572
- await handleSetupCommand({ interactive: true });
573
- // Reload preferences after setup to get the newly selected default mode
574
- // (setup may have just saved a different mode than the default 'web')
575
- try {
576
- const newPreferences = await loadGlobalPreferences();
577
- if (newPreferences.defaults?.defaultMode) {
578
- opts.mode = newPreferences.defaults.defaultMode;
579
- logger.debug(`Updated mode from setup preferences: ${opts.mode}`);
580
- }
581
- }
582
- catch {
583
- // Ignore errors - will use default mode
584
- }
585
- }
586
- // Now resolve agent (will auto-install since setup is complete)
589
+ // Resolve the configured agent after setup has been established for
590
+ // interactive startup. This keeps provider selection generic instead of
591
+ // letting bundled agent defaults decide first-run behavior.
587
592
  resolvedPath = await resolveAgentPath(opts.agent, opts.autoInstall !== false);
588
593
  if (opts.interactive !== false) {
589
594
  const { getBundledSyncTargetForAgentPath, shouldPromptForSync, handleSyncAgentsCommand, } = await import('./cli/commands/agents/sync.js');
@@ -611,9 +616,11 @@ program
611
616
  // See feature-plans/auto-update.md section 8.11 - Three-Layer LLM Resolution
612
617
  const agentId = opts.agent ?? 'coding-agent';
613
618
  let preferences = null;
619
+ let hasCompletedSetup = false;
614
620
  if (globalPreferencesExist()) {
615
621
  try {
616
622
  preferences = await loadGlobalPreferences();
623
+ hasCompletedSetup = preferences.setup.completed;
617
624
  }
618
625
  catch {
619
626
  // Preferences exist but couldn't load - continue without them
@@ -646,6 +653,7 @@ program
646
653
  await handleSetupCommand({ interactive: true, force: true });
647
654
  // Reload preferences after setup
648
655
  preferences = await loadGlobalPreferences();
656
+ hasCompletedSetup = preferences.setup.completed;
649
657
  }
650
658
  else {
651
659
  // User cancelled
@@ -689,7 +697,15 @@ program
689
697
  // See feature-plans/auto-update.md section 8.11 - Three-Layer LLM Resolution:
690
698
  // local.llm ?? preferences.llm ?? bundled.llm
691
699
  // The preferences.llm acts as a "global .local.yml" for LLM settings
692
- if (preferences?.llm?.provider && preferences?.llm?.model) {
700
+ mergedConfig = applyStartupLLMFallback(mergedConfig, {
701
+ hasCompletedSetup,
702
+ hasExplicitProviderOverride: Boolean(opts.provider),
703
+ hasExplicitModelOverride: Boolean(opts.model),
704
+ hasExplicitApiKeyOverride: Boolean(opts.apiKey),
705
+ });
706
+ if (hasCompletedSetup &&
707
+ preferences?.llm?.provider &&
708
+ preferences?.llm?.model) {
693
709
  mergedConfig = applyUserPreferences(mergedConfig, preferences);
694
710
  logger.debug(`Applied user preferences to ${agentId}`, {
695
711
  provider: preferences.llm.provider,