dexto 1.5.8 → 1.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (224) hide show
  1. package/README.md +3 -3
  2. package/dist/agents/agent-template.yml +2 -2
  3. package/dist/agents/coding-agent/README.md +10 -10
  4. package/dist/agents/coding-agent/coding-agent.yml +84 -83
  5. package/dist/agents/default-agent.yml +32 -47
  6. package/dist/agents/explore-agent/explore-agent.yml +3 -6
  7. package/dist/agents/image-editor-agent/image-editor-agent.yml +1 -1
  8. package/dist/agents/nano-banana-agent/nano-banana-agent.yml +1 -1
  9. package/dist/agents/podcast-agent/podcast-agent.yml +1 -1
  10. package/dist/agents/product-name-researcher/product-name-researcher.yml +1 -1
  11. package/dist/agents/sora-video-agent/sora-video-agent.yml +4 -6
  12. package/dist/agents/triage-demo/triage-agent.yml +1 -1
  13. package/dist/analytics/events.d.ts +1 -1
  14. package/dist/analytics/events.d.ts.map +1 -1
  15. package/dist/api/mcp/tool-aggregation-handler.d.ts +2 -2
  16. package/dist/api/server-hono.d.ts +2 -2
  17. package/dist/api/server-hono.d.ts.map +1 -1
  18. package/dist/api/server-hono.js +37 -60
  19. package/dist/cli/approval/cli-approval-handler.d.ts +10 -3
  20. package/dist/cli/approval/cli-approval-handler.d.ts.map +1 -1
  21. package/dist/cli/approval/cli-approval-handler.js +1 -1
  22. package/dist/cli/assets/sounds/SOURCES.md +35 -0
  23. package/dist/cli/assets/sounds/boot.wav +0 -0
  24. package/dist/cli/assets/sounds/chime.wav +0 -0
  25. package/dist/cli/assets/sounds/coin.wav +0 -0
  26. package/dist/cli/assets/sounds/confirm.wav +0 -0
  27. package/dist/cli/assets/sounds/levelup.wav +0 -0
  28. package/dist/cli/assets/sounds/ping.wav +0 -0
  29. package/dist/cli/assets/sounds/powerup.wav +0 -0
  30. package/dist/cli/assets/sounds/startup.wav +0 -0
  31. package/dist/cli/assets/sounds/success.wav +0 -0
  32. package/dist/cli/assets/sounds/treasure.wav +0 -0
  33. package/dist/cli/assets/sounds/win.wav +0 -0
  34. package/dist/cli/commands/create-app.d.ts +1 -11
  35. package/dist/cli/commands/create-app.d.ts.map +1 -1
  36. package/dist/cli/commands/create-app.js +21 -545
  37. package/dist/cli/commands/create-image.d.ts.map +1 -1
  38. package/dist/cli/commands/create-image.js +54 -53
  39. package/dist/cli/commands/image.d.ts +52 -0
  40. package/dist/cli/commands/image.d.ts.map +1 -0
  41. package/dist/cli/commands/image.js +118 -0
  42. package/dist/cli/commands/index.d.ts +2 -1
  43. package/dist/cli/commands/index.d.ts.map +1 -1
  44. package/dist/cli/commands/index.js +3 -1
  45. package/dist/cli/commands/init-app.d.ts +4 -8
  46. package/dist/cli/commands/init-app.d.ts.map +1 -1
  47. package/dist/cli/commands/init-app.js +37 -161
  48. package/dist/cli/commands/interactive-commands/command-parser.d.ts +2 -0
  49. package/dist/cli/commands/interactive-commands/command-parser.d.ts.map +1 -1
  50. package/dist/cli/commands/interactive-commands/commands.d.ts +1 -1
  51. package/dist/cli/commands/interactive-commands/commands.d.ts.map +1 -1
  52. package/dist/cli/commands/interactive-commands/commands.js +2 -2
  53. package/dist/cli/commands/interactive-commands/exit-handler.d.ts +12 -0
  54. package/dist/cli/commands/interactive-commands/exit-handler.d.ts.map +1 -0
  55. package/dist/cli/commands/interactive-commands/exit-handler.js +20 -0
  56. package/dist/cli/commands/interactive-commands/exit-stats.d.ts +24 -0
  57. package/dist/cli/commands/interactive-commands/exit-stats.d.ts.map +1 -0
  58. package/dist/cli/commands/interactive-commands/exit-stats.js +17 -0
  59. package/dist/cli/commands/interactive-commands/general-commands.d.ts.map +1 -1
  60. package/dist/cli/commands/interactive-commands/general-commands.js +55 -5
  61. package/dist/cli/commands/interactive-commands/prompt-commands.d.ts.map +1 -1
  62. package/dist/cli/commands/interactive-commands/prompt-commands.js +14 -74
  63. package/dist/cli/commands/interactive-commands/session/index.d.ts +2 -1
  64. package/dist/cli/commands/interactive-commands/session/index.d.ts.map +1 -1
  65. package/dist/cli/commands/interactive-commands/session/index.js +2 -1
  66. package/dist/cli/commands/interactive-commands/session/session-commands.d.ts +2 -2
  67. package/dist/cli/commands/interactive-commands/session/session-commands.d.ts.map +1 -1
  68. package/dist/cli/commands/interactive-commands/session/session-commands.js +2 -4
  69. package/dist/cli/commands/interactive-commands/system/system-commands.d.ts +1 -13
  70. package/dist/cli/commands/interactive-commands/system/system-commands.d.ts.map +1 -1
  71. package/dist/cli/commands/interactive-commands/system/system-commands.js +52 -83
  72. package/dist/cli/commands/plugin.d.ts +4 -4
  73. package/dist/cli/commands/sync-agents.d.ts +2 -12
  74. package/dist/cli/commands/sync-agents.d.ts.map +1 -1
  75. package/dist/cli/commands/sync-agents.js +2 -50
  76. package/dist/cli/ink-cli/InkCLIRefactored.d.ts +7 -1
  77. package/dist/cli/ink-cli/InkCLIRefactored.d.ts.map +1 -1
  78. package/dist/cli/ink-cli/InkCLIRefactored.js +138 -27
  79. package/dist/cli/ink-cli/components/ApprovalPrompt.d.ts +2 -2
  80. package/dist/cli/ink-cli/components/ApprovalPrompt.d.ts.map +1 -1
  81. package/dist/cli/ink-cli/components/ApprovalPrompt.js +85 -30
  82. package/dist/cli/ink-cli/components/BackgroundTasksPanel.js +1 -1
  83. package/dist/cli/ink-cli/components/ElicitationForm.d.ts +5 -3
  84. package/dist/cli/ink-cli/components/ElicitationForm.d.ts.map +1 -1
  85. package/dist/cli/ink-cli/components/ElicitationForm.js +414 -180
  86. package/dist/cli/ink-cli/components/Footer.d.ts.map +1 -1
  87. package/dist/cli/ink-cli/components/Footer.js +1 -2
  88. package/dist/cli/ink-cli/components/ResourceAutocomplete.d.ts.map +1 -1
  89. package/dist/cli/ink-cli/components/ResourceAutocomplete.js +20 -11
  90. package/dist/cli/ink-cli/components/SlashCommandAutocomplete.d.ts.map +1 -1
  91. package/dist/cli/ink-cli/components/SlashCommandAutocomplete.js +47 -67
  92. package/dist/cli/ink-cli/components/StatusBar.d.ts.map +1 -1
  93. package/dist/cli/ink-cli/components/StatusBar.js +20 -10
  94. package/dist/cli/ink-cli/components/TodoPanel.js +1 -1
  95. package/dist/cli/ink-cli/components/base/BaseSelector.d.ts +2 -1
  96. package/dist/cli/ink-cli/components/base/BaseSelector.d.ts.map +1 -1
  97. package/dist/cli/ink-cli/components/base/BaseSelector.js +37 -27
  98. package/dist/cli/ink-cli/components/chat/Header.d.ts.map +1 -1
  99. package/dist/cli/ink-cli/components/chat/Header.js +1 -1
  100. package/dist/cli/ink-cli/components/chat/MessageItem.d.ts.map +1 -1
  101. package/dist/cli/ink-cli/components/chat/MessageItem.js +3 -1
  102. package/dist/cli/ink-cli/components/chat/ToolIcon.d.ts.map +1 -1
  103. package/dist/cli/ink-cli/components/chat/ToolIcon.js +5 -15
  104. package/dist/cli/ink-cli/components/chat/styled-boxes/ConfigBox.js +1 -1
  105. package/dist/cli/ink-cli/components/chat/styled-boxes/LogConfigBox.d.ts.map +1 -1
  106. package/dist/cli/ink-cli/components/chat/styled-boxes/LogConfigBox.js +1 -1
  107. package/dist/cli/ink-cli/components/modes/AlternateBufferCLI.d.ts +3 -1
  108. package/dist/cli/ink-cli/components/modes/AlternateBufferCLI.d.ts.map +1 -1
  109. package/dist/cli/ink-cli/components/modes/AlternateBufferCLI.js +5 -3
  110. package/dist/cli/ink-cli/components/modes/StaticCLI.d.ts +3 -1
  111. package/dist/cli/ink-cli/components/modes/StaticCLI.d.ts.map +1 -1
  112. package/dist/cli/ink-cli/components/modes/StaticCLI.js +10 -3
  113. package/dist/cli/ink-cli/components/overlays/CommandOutputOverlay.d.ts +13 -0
  114. package/dist/cli/ink-cli/components/overlays/CommandOutputOverlay.d.ts.map +1 -0
  115. package/dist/cli/ink-cli/components/overlays/CommandOutputOverlay.js +60 -0
  116. package/dist/cli/ink-cli/components/overlays/LogLevelSelector.js +1 -1
  117. package/dist/cli/ink-cli/components/overlays/ModelSelectorRefactored.d.ts.map +1 -1
  118. package/dist/cli/ink-cli/components/overlays/ModelSelectorRefactored.js +213 -100
  119. package/dist/cli/ink-cli/components/overlays/PromptList.d.ts.map +1 -1
  120. package/dist/cli/ink-cli/components/overlays/PromptList.js +12 -16
  121. package/dist/cli/ink-cli/components/overlays/SoundsSelector.d.ts +21 -0
  122. package/dist/cli/ink-cli/components/overlays/SoundsSelector.d.ts.map +1 -0
  123. package/dist/cli/ink-cli/components/overlays/SoundsSelector.js +566 -0
  124. package/dist/cli/ink-cli/components/overlays/ToolBrowser.d.ts +1 -1
  125. package/dist/cli/ink-cli/components/overlays/ToolBrowser.d.ts.map +1 -1
  126. package/dist/cli/ink-cli/components/overlays/ToolBrowser.js +100 -45
  127. package/dist/cli/ink-cli/components/overlays/custom-model-wizard/LocalModelWizard.d.ts.map +1 -1
  128. package/dist/cli/ink-cli/components/overlays/custom-model-wizard/LocalModelWizard.js +8 -13
  129. package/dist/cli/ink-cli/components/renderers/FilePreviewRenderer.d.ts +3 -3
  130. package/dist/cli/ink-cli/components/renderers/FilePreviewRenderer.d.ts.map +1 -1
  131. package/dist/cli/ink-cli/components/renderers/FilePreviewRenderer.js +6 -5
  132. package/dist/cli/ink-cli/components/renderers/FileRenderer.d.ts +3 -1
  133. package/dist/cli/ink-cli/components/renderers/FileRenderer.d.ts.map +1 -1
  134. package/dist/cli/ink-cli/components/renderers/FileRenderer.js +18 -7
  135. package/dist/cli/ink-cli/components/renderers/ShellRenderer.d.ts.map +1 -1
  136. package/dist/cli/ink-cli/components/renderers/ShellRenderer.js +7 -17
  137. package/dist/cli/ink-cli/components/renderers/index.d.ts.map +1 -1
  138. package/dist/cli/ink-cli/components/renderers/index.js +1 -1
  139. package/dist/cli/ink-cli/components/shared/FocusOverlayFrame.d.ts +7 -0
  140. package/dist/cli/ink-cli/components/shared/FocusOverlayFrame.d.ts.map +1 -0
  141. package/dist/cli/ink-cli/components/shared/FocusOverlayFrame.js +8 -0
  142. package/dist/cli/ink-cli/components/shared/HintBar.d.ts +6 -0
  143. package/dist/cli/ink-cli/components/shared/HintBar.d.ts.map +1 -0
  144. package/dist/cli/ink-cli/components/shared/HintBar.js +6 -0
  145. package/dist/cli/ink-cli/constants/spinnerFrames.d.ts +2 -0
  146. package/dist/cli/ink-cli/constants/spinnerFrames.d.ts.map +1 -0
  147. package/dist/cli/ink-cli/constants/spinnerFrames.js +1 -0
  148. package/dist/cli/ink-cli/constants/tips.d.ts.map +1 -1
  149. package/dist/cli/ink-cli/constants/tips.js +2 -1
  150. package/dist/cli/ink-cli/containers/InputContainer.d.ts +4 -0
  151. package/dist/cli/ink-cli/containers/InputContainer.d.ts.map +1 -1
  152. package/dist/cli/ink-cli/containers/InputContainer.js +47 -21
  153. package/dist/cli/ink-cli/containers/OverlayContainer.d.ts +2 -0
  154. package/dist/cli/ink-cli/containers/OverlayContainer.d.ts.map +1 -1
  155. package/dist/cli/ink-cli/containers/OverlayContainer.js +101 -40
  156. package/dist/cli/ink-cli/hooks/useAgentEvents.d.ts.map +1 -1
  157. package/dist/cli/ink-cli/hooks/useAgentEvents.js +15 -16
  158. package/dist/cli/ink-cli/hooks/useAnimationTick.d.ts +11 -0
  159. package/dist/cli/ink-cli/hooks/useAnimationTick.d.ts.map +1 -0
  160. package/dist/cli/ink-cli/hooks/useAnimationTick.js +54 -0
  161. package/dist/cli/ink-cli/hooks/useCLIState.d.ts.map +1 -1
  162. package/dist/cli/ink-cli/hooks/useCLIState.js +1 -0
  163. package/dist/cli/ink-cli/hooks/useTokenCounter.d.ts.map +1 -1
  164. package/dist/cli/ink-cli/hooks/useTokenCounter.js +7 -4
  165. package/dist/cli/ink-cli/services/CommandService.d.ts +1 -1
  166. package/dist/cli/ink-cli/services/CommandService.d.ts.map +1 -1
  167. package/dist/cli/ink-cli/services/CommandService.js +2 -2
  168. package/dist/cli/ink-cli/services/processStream.d.ts +2 -2
  169. package/dist/cli/ink-cli/services/processStream.d.ts.map +1 -1
  170. package/dist/cli/ink-cli/services/processStream.js +27 -19
  171. package/dist/cli/ink-cli/state/initialState.d.ts.map +1 -1
  172. package/dist/cli/ink-cli/state/initialState.js +1 -0
  173. package/dist/cli/ink-cli/state/types.d.ts +15 -3
  174. package/dist/cli/ink-cli/state/types.d.ts.map +1 -1
  175. package/dist/cli/ink-cli/utils/commandOverlays.d.ts.map +1 -1
  176. package/dist/cli/ink-cli/utils/commandOverlays.js +1 -0
  177. package/dist/cli/ink-cli/utils/elicitationSchema.d.ts +11 -0
  178. package/dist/cli/ink-cli/utils/elicitationSchema.d.ts.map +1 -0
  179. package/dist/cli/ink-cli/utils/elicitationSchema.js +80 -0
  180. package/dist/cli/ink-cli/utils/index.d.ts +1 -1
  181. package/dist/cli/ink-cli/utils/index.d.ts.map +1 -1
  182. package/dist/cli/ink-cli/utils/index.js +1 -1
  183. package/dist/cli/ink-cli/utils/messageFormatting.d.ts +10 -19
  184. package/dist/cli/ink-cli/utils/messageFormatting.d.ts.map +1 -1
  185. package/dist/cli/ink-cli/utils/messageFormatting.js +43 -262
  186. package/dist/cli/ink-cli/utils/overlayPresentation.d.ts +19 -0
  187. package/dist/cli/ink-cli/utils/overlayPresentation.d.ts.map +1 -0
  188. package/dist/cli/ink-cli/utils/overlayPresentation.js +33 -0
  189. package/dist/cli/ink-cli/utils/overlaySizing.d.ts +19 -0
  190. package/dist/cli/ink-cli/utils/overlaySizing.d.ts.map +1 -0
  191. package/dist/cli/ink-cli/utils/overlaySizing.js +11 -0
  192. package/dist/cli/ink-cli/utils/soundNotification.d.ts +19 -13
  193. package/dist/cli/ink-cli/utils/soundNotification.d.ts.map +1 -1
  194. package/dist/cli/ink-cli/utils/soundNotification.js +120 -97
  195. package/dist/cli/ink-cli/utils/toolUtils.d.ts.map +1 -1
  196. package/dist/cli/ink-cli/utils/toolUtils.js +2 -9
  197. package/dist/cli/utils/config-validation.d.ts +11 -11
  198. package/dist/cli/utils/config-validation.d.ts.map +1 -1
  199. package/dist/cli/utils/config-validation.js +56 -290
  200. package/dist/cli/utils/image-store.d.ts +16 -0
  201. package/dist/cli/utils/image-store.d.ts.map +1 -0
  202. package/dist/cli/utils/image-store.js +289 -0
  203. package/dist/cli/utils/scaffolding-utils.d.ts +5 -0
  204. package/dist/cli/utils/scaffolding-utils.d.ts.map +1 -1
  205. package/dist/cli/utils/scaffolding-utils.js +46 -4
  206. package/dist/cli/utils/template-engine.d.ts +28 -16
  207. package/dist/cli/utils/template-engine.d.ts.map +1 -1
  208. package/dist/cli/utils/template-engine.js +339 -479
  209. package/dist/config/cli-overrides.d.ts +4 -3
  210. package/dist/config/cli-overrides.d.ts.map +1 -1
  211. package/dist/config/cli-overrides.js +7 -9
  212. package/dist/index-main.d.ts +2 -0
  213. package/dist/index-main.d.ts.map +1 -0
  214. package/dist/index-main.js +1554 -0
  215. package/dist/index.js +2 -1589
  216. package/dist/utils/session-logger-factory.d.ts +3 -0
  217. package/dist/utils/session-logger-factory.d.ts.map +1 -0
  218. package/dist/utils/session-logger-factory.js +34 -0
  219. package/dist/webui/assets/{index-Cz2z7NQ8.js → index-CKhumsZA.js} +231 -231
  220. package/dist/webui/index.html +1 -1
  221. package/package.json +11 -8
  222. package/dist/cli/cli-subscriber.d.ts +0 -45
  223. package/dist/cli/cli-subscriber.d.ts.map +0 -1
  224. package/dist/cli/cli-subscriber.js +0 -204
@@ -0,0 +1,1554 @@
1
+ import { existsSync, readFileSync } from 'fs';
2
+ import { createRequire } from 'module';
3
+ import path from 'path';
4
+ import { Command } from 'commander';
5
+ import * as p from '@clack/prompts';
6
+ import chalk from 'chalk';
7
+ import { initAnalytics, capture, getWebUIAnalyticsConfig } from './analytics/index.js';
8
+ import { withAnalytics, safeExit, ExitSignal } from './analytics/wrapper.js';
9
+ import { createFileSessionLoggerFactory } from './utils/session-logger-factory.js';
10
+ // Use createRequire to import package.json without experimental warning
11
+ const require = createRequire(import.meta.url);
12
+ const pkg = require('../package.json');
13
+ // Set CLI version for Dexto Gateway usage tracking
14
+ process.env.DEXTO_CLI_VERSION = pkg.version;
15
+ // Populate DEXTO_API_KEY for Dexto gateway routing
16
+ // Resolution order in getDextoApiKey():
17
+ // 1. Explicit env var (CI, testing, account override)
18
+ // 2. auth.json from `dexto login`
19
+ import { isDextoAuthEnabled } from '@dexto/agent-management';
20
+ if (isDextoAuthEnabled()) {
21
+ const { getDextoApiKey } = await import('./cli/auth/index.js');
22
+ const dextoApiKey = await getDextoApiKey();
23
+ if (dextoApiKey) {
24
+ process.env.DEXTO_API_KEY = dextoApiKey;
25
+ }
26
+ }
27
+ import { logger, getProviderFromModel, getAllSupportedModels, startLlmRegistryAutoUpdate, DextoAgent, isPath, resolveApiKeyForProvider, getPrimaryApiKeyEnvVar, } from '@dexto/core';
28
+ import { applyImageDefaults, cleanNullValues, AgentConfigSchema, loadImage, resolveServicesFromConfig, setImageImporter, toDextoAgentOptions, } from '@dexto/agent-config';
29
+ import { resolveAgentPath, loadAgentConfig, globalPreferencesExist, loadGlobalPreferences, resolveBundledScript, } from '@dexto/agent-management';
30
+ import { startHonoApiServer } from './api/server-hono.js';
31
+ import { validateCliOptions, handleCliOptionsError } from './cli/utils/options.js';
32
+ import { validateAgentConfig } from './cli/utils/config-validation.js';
33
+ import { applyCLIOverrides, applyUserPreferences } from './config/cli-overrides.js';
34
+ import { enrichAgentConfig } from '@dexto/agent-management';
35
+ import { getPort } from './utils/port-utils.js';
36
+ import { createDextoProject, createImage, getUserInputToInitDextoApp, initDexto, postInitDexto, } from './cli/commands/index.js';
37
+ import { handleSetupCommand, handleInstallCommand, handleUninstallCommand, handleImageDoctorCommand, handleImageInstallCommand, handleImageListCommand, handleImageRemoveCommand, handleImageUseCommand, handleListAgentsCommand, handleWhichCommand, handleSyncAgentsCommand, shouldPromptForSync, handleLoginCommand, handleLogoutCommand, handleStatusCommand, handleBillingStatusCommand, handlePluginListCommand, handlePluginInstallCommand, handlePluginUninstallCommand, handlePluginValidateCommand,
38
+ // Marketplace handlers
39
+ handleMarketplaceAddCommand, handleMarketplaceRemoveCommand, handleMarketplaceUpdateCommand, handleMarketplaceListCommand, handleMarketplacePluginsCommand, handleMarketplaceInstallCommand, } from './cli/commands/index.js';
40
+ import { handleSessionListCommand, handleSessionHistoryCommand, handleSessionDeleteCommand, handleSessionSearchCommand, } from './cli/commands/session-commands.js';
41
+ import { requiresSetup } from './cli/utils/setup-utils.js';
42
+ import { checkForFileInCurrentDirectory, FileNotFoundError } from './cli/utils/package-mgmt.js';
43
+ import { checkForUpdates, displayUpdateNotification } from './cli/utils/version-check.js';
44
+ import { resolveWebRoot } from './web.js';
45
+ import { initializeMcpServer, createMcpTransport } from '@dexto/server';
46
+ import { createAgentCard } from '@dexto/core';
47
+ import { initializeMcpToolAggregationServer } from './api/mcp/tool-aggregation-handler.js';
48
+ import { importImageModule } from './cli/utils/image-store.js';
49
+ const program = new Command();
50
+ // Resolve images via the Dexto image store when installed; fall back to host imports (pnpm-safe).
51
+ setImageImporter((specifier) => importImageModule(specifier));
52
+ // Initialize analytics early (no-op if disabled)
53
+ await initAnalytics({ appVersion: pkg.version });
54
+ // Start version check early (non-blocking)
55
+ // We'll check the result later and display notification for interactive modes
56
+ const versionCheckPromise = checkForUpdates(pkg.version);
57
+ // Start self-updating LLM registry refresh (models.dev + OpenRouter mapping).
58
+ // Uses a cached snapshot on disk and refreshes in the background.
59
+ startLlmRegistryAutoUpdate();
60
+ // 1) GLOBAL OPTIONS
61
+ program
62
+ .name('dexto')
63
+ .description('AI-powered CLI and WebUI for interacting with MCP servers.')
64
+ .version(pkg.version, '-v, --version', 'output the current version')
65
+ .option('-a, --agent <id|path>', 'Agent ID or path to agent config file')
66
+ .option('-p, --prompt <text>', 'Start the interactive CLI and immediately run the prompt')
67
+ .option('-s, --strict', 'Require all server connections to succeed')
68
+ .option('--no-verbose', 'Disable verbose output')
69
+ .option('--no-interactive', 'Disable interactive prompts and API key setup')
70
+ .option('--skip-setup', 'Skip global setup validation (useful for MCP mode, automation)')
71
+ .option('-m, --model <model>', 'Specify the LLM model to use')
72
+ .option('--auto-approve', 'Always approve tool executions without confirmation prompts')
73
+ .option('--no-elicitation', 'Disable elicitation (agent cannot prompt user for input)')
74
+ .option('-c, --continue', 'Continue most recent session (CLI mode)')
75
+ .option('-r, --resume <sessionId>', 'Resume a session by ID (CLI mode)')
76
+ .option('--mode <mode>', 'The application in which dexto should talk to you - web | cli | server | mcp', 'web')
77
+ .option('--port <port>', 'port for the server (default: 3000 for web, 3001 for server mode)')
78
+ .option('--no-auto-install', 'Disable automatic installation of missing agents from registry')
79
+ .option('--image <package>', 'Image package to load (e.g., @dexto/image-local). Overrides config image field.')
80
+ .option('--dev', '[maintainers] Use local ./agents instead of ~/.dexto (for dexto repo development)')
81
+ .enablePositionalOptions();
82
+ // 2) `create-app` SUB-COMMAND
83
+ program
84
+ .command('create-app [name]')
85
+ .description('Create a Dexto application (CLI, web, bot, etc.)')
86
+ .option('--from-image <package>', 'Use existing image (e.g., @dexto/image-local)')
87
+ .option('--type <type>', 'App type: script, webapp (default: script)')
88
+ .action(withAnalytics('create-app', async (name, options) => {
89
+ try {
90
+ p.intro(chalk.inverse('Create Dexto App'));
91
+ // Create the app project structure (fully self-contained)
92
+ await createDextoProject(name, options);
93
+ p.outro(chalk.greenBright('Dexto app created successfully!'));
94
+ safeExit('create-app', 0);
95
+ }
96
+ catch (err) {
97
+ if (err instanceof ExitSignal)
98
+ throw err;
99
+ console.error(`❌ dexto create-app command failed: ${err}`);
100
+ safeExit('create-app', 1, 'error');
101
+ }
102
+ }));
103
+ // 3) `create-image` SUB-COMMAND (hidden alias for `dexto image create`)
104
+ program
105
+ .command('create-image [name]', { hidden: true })
106
+ .description('Alias for `dexto image create`')
107
+ .action(withAnalytics('create-image', async (name) => {
108
+ try {
109
+ p.intro(chalk.inverse('Create Dexto Image'));
110
+ // Create the image project structure
111
+ const projectPath = await createImage(name);
112
+ p.outro(chalk.greenBright(`Dexto image created successfully at ${projectPath}!`));
113
+ safeExit('create-image', 0);
114
+ }
115
+ catch (err) {
116
+ if (err instanceof ExitSignal)
117
+ throw err;
118
+ console.error(`❌ dexto create-image command failed: ${err}`);
119
+ safeExit('create-image', 1, 'error');
120
+ }
121
+ }));
122
+ // 3b) `image` SUB-COMMAND
123
+ const imageCommand = program.command('image').description('Manage images');
124
+ imageCommand.addHelpText('after', `
125
+ Examples:
126
+ $ dexto image create my-image
127
+ $ dexto image install @dexto/image-local
128
+ $ dexto image install @myorg/my-image@1.2.3
129
+ $ dexto image list
130
+ $ dexto image use @myorg/my-image@1.2.3
131
+ $ dexto image remove @myorg/my-image@1.2.3
132
+ $ dexto image doctor
133
+ `);
134
+ imageCommand
135
+ .command('create [name]')
136
+ .description('Create a Dexto image project (scaffold)')
137
+ .action(withAnalytics('image create', async (name) => {
138
+ try {
139
+ p.intro(chalk.inverse('Create Dexto Image'));
140
+ // Create the image project structure
141
+ const projectPath = await createImage(name);
142
+ p.outro(chalk.greenBright(`Dexto image created successfully at ${projectPath}!`));
143
+ safeExit('image create', 0);
144
+ }
145
+ catch (err) {
146
+ if (err instanceof ExitSignal)
147
+ throw err;
148
+ console.error(`❌ dexto image create command failed: ${err}`);
149
+ safeExit('image create', 1, 'error');
150
+ }
151
+ }));
152
+ imageCommand
153
+ .command('install <image>')
154
+ .description('Install an image into the local Dexto image store')
155
+ .option('--force', 'Force reinstall if already installed')
156
+ .option('--no-activate', 'Do not set as the active version')
157
+ .addHelpText('after', `
158
+ Examples:
159
+ $ dexto image install @dexto/image-local
160
+ $ dexto image install @myorg/my-image@1.2.3
161
+ $ dexto image install ./my-image-1.0.0.tgz
162
+ `)
163
+ .action(withAnalytics('image install', async (image, options) => {
164
+ try {
165
+ await handleImageInstallCommand({ ...options, image });
166
+ safeExit('image install', 0);
167
+ }
168
+ catch (err) {
169
+ if (err instanceof ExitSignal)
170
+ throw err;
171
+ console.error(`❌ dexto image install command failed: ${err}`);
172
+ safeExit('image install', 1, 'error');
173
+ }
174
+ }));
175
+ imageCommand
176
+ .command('list')
177
+ .description('List installed images')
178
+ .action(withAnalytics('image list', async () => {
179
+ try {
180
+ await handleImageListCommand();
181
+ safeExit('image list', 0);
182
+ }
183
+ catch (err) {
184
+ if (err instanceof ExitSignal)
185
+ throw err;
186
+ console.error(`❌ dexto image list command failed: ${err}`);
187
+ safeExit('image list', 1, 'error');
188
+ }
189
+ }));
190
+ imageCommand
191
+ .command('use <image>')
192
+ .description('Set the active version for an installed image (image@version)')
193
+ .action(withAnalytics('image use', async (image) => {
194
+ try {
195
+ await handleImageUseCommand({ image });
196
+ safeExit('image use', 0);
197
+ }
198
+ catch (err) {
199
+ if (err instanceof ExitSignal)
200
+ throw err;
201
+ console.error(`❌ dexto image use command failed: ${err}`);
202
+ safeExit('image use', 1, 'error');
203
+ }
204
+ }));
205
+ imageCommand
206
+ .command('remove <image>')
207
+ .description('Remove an image from the store (image or image@version)')
208
+ .action(withAnalytics('image remove', async (image) => {
209
+ try {
210
+ await handleImageRemoveCommand({ image });
211
+ safeExit('image remove', 0);
212
+ }
213
+ catch (err) {
214
+ if (err instanceof ExitSignal)
215
+ throw err;
216
+ console.error(`❌ dexto image remove command failed: ${err}`);
217
+ safeExit('image remove', 1, 'error');
218
+ }
219
+ }));
220
+ imageCommand
221
+ .command('doctor')
222
+ .description('Print image store diagnostics')
223
+ .action(withAnalytics('image doctor', async () => {
224
+ try {
225
+ await handleImageDoctorCommand();
226
+ safeExit('image doctor', 0);
227
+ }
228
+ catch (err) {
229
+ if (err instanceof ExitSignal)
230
+ throw err;
231
+ console.error(`❌ dexto image doctor command failed: ${err}`);
232
+ safeExit('image doctor', 1, 'error');
233
+ }
234
+ }));
235
+ // 4) `init-app` SUB-COMMAND
236
+ program
237
+ .command('init-app')
238
+ .description('Initialize an existing Typescript app with Dexto')
239
+ .action(withAnalytics('init-app', async () => {
240
+ try {
241
+ // pre-condition: check that package.json and tsconfig.json exist in current directory to know that project is valid
242
+ await checkForFileInCurrentDirectory('package.json');
243
+ await checkForFileInCurrentDirectory('tsconfig.json');
244
+ // start intro
245
+ p.intro(chalk.inverse('Dexto Init App'));
246
+ const userInput = await getUserInputToInitDextoApp();
247
+ try {
248
+ capture('dexto_init', {
249
+ provider: userInput.llmProvider,
250
+ providedKey: Boolean(userInput.llmApiKey),
251
+ });
252
+ }
253
+ catch {
254
+ // Analytics failures should not block CLI execution.
255
+ }
256
+ await initDexto(userInput.directory, userInput.createExampleFile, userInput.llmProvider, userInput.llmApiKey);
257
+ p.outro(chalk.greenBright('Dexto app initialized successfully!'));
258
+ // add notes for users to get started with their new initialized Dexto project
259
+ await postInitDexto(userInput.directory);
260
+ safeExit('init-app', 0);
261
+ }
262
+ catch (err) {
263
+ if (err instanceof ExitSignal)
264
+ throw err;
265
+ // if the package.json or tsconfig.json is not found, we give instructions to create a new project
266
+ if (err instanceof FileNotFoundError) {
267
+ console.error(`❌ ${err.message} Run "dexto create-app" to create a new app`);
268
+ safeExit('init-app', 1, 'file-not-found');
269
+ }
270
+ console.error(`❌ Initialization failed: ${err}`);
271
+ safeExit('init-app', 1, 'error');
272
+ }
273
+ }));
274
+ // 5) `setup` SUB-COMMAND
275
+ program
276
+ .command('setup')
277
+ .description('Configure global Dexto preferences')
278
+ .option('--provider <provider>', 'LLM provider (openai, anthropic, google, groq)')
279
+ .option('--model <model>', 'Model name (uses provider default if not specified)')
280
+ .option('--default-agent <agent>', 'Default agent name (default: coding-agent)')
281
+ .option('--no-interactive', 'Skip interactive prompts and API key setup')
282
+ .option('--force', 'Overwrite existing setup without confirmation')
283
+ .action(withAnalytics('setup', async (options) => {
284
+ try {
285
+ await handleSetupCommand(options);
286
+ safeExit('setup', 0);
287
+ }
288
+ catch (err) {
289
+ if (err instanceof ExitSignal)
290
+ throw err;
291
+ console.error(`❌ dexto setup command failed: ${err}. Check logs in ~/.dexto/logs/dexto.log for more information`);
292
+ safeExit('setup', 1, 'error');
293
+ }
294
+ }));
295
+ // 6) `install` SUB-COMMAND
296
+ program
297
+ .command('install [agents...]')
298
+ .description('Install agents from registry or custom YAML files/directories')
299
+ .option('--all', 'Install all available agents from registry')
300
+ .option('--no-inject-preferences', 'Skip injecting global preferences into installed agents')
301
+ .option('--force', 'Force reinstall even if agent is already installed')
302
+ .addHelpText('after', `
303
+ Examples:
304
+ $ dexto install coding-agent Install agent from registry
305
+ $ dexto install agent1 agent2 Install multiple registry agents
306
+ $ dexto install --all Install all available registry agents
307
+ $ dexto install ./my-agent.yml Install custom agent from YAML file
308
+ $ dexto install ./my-agent-dir/ Install custom agent from directory (interactive)`)
309
+ .action(withAnalytics('install', async (agents = [], options) => {
310
+ try {
311
+ await handleInstallCommand(agents, options);
312
+ safeExit('install', 0);
313
+ }
314
+ catch (err) {
315
+ if (err instanceof ExitSignal)
316
+ throw err;
317
+ console.error(`❌ dexto install command failed: ${err}`);
318
+ safeExit('install', 1, 'error');
319
+ }
320
+ }));
321
+ // 7) `uninstall` SUB-COMMAND
322
+ program
323
+ .command('uninstall [agents...]')
324
+ .description('Uninstall agents from the local installation')
325
+ .option('--all', 'Uninstall all installed agents')
326
+ .option('--force', 'Force uninstall even if agent is protected (e.g., coding-agent)')
327
+ .action(withAnalytics('uninstall', async (agents, options) => {
328
+ try {
329
+ await handleUninstallCommand(agents, options);
330
+ safeExit('uninstall', 0);
331
+ }
332
+ catch (err) {
333
+ if (err instanceof ExitSignal)
334
+ throw err;
335
+ console.error(`❌ dexto uninstall command failed: ${err}`);
336
+ safeExit('uninstall', 1, 'error');
337
+ }
338
+ }));
339
+ // 8) `list-agents` SUB-COMMAND
340
+ program
341
+ .command('list-agents')
342
+ .description('List available and installed agents')
343
+ .option('--verbose', 'Show detailed agent information')
344
+ .option('--installed', 'Show only installed agents')
345
+ .option('--available', 'Show only available agents')
346
+ .action(withAnalytics('list-agents', async (options) => {
347
+ try {
348
+ await handleListAgentsCommand(options);
349
+ safeExit('list-agents', 0);
350
+ }
351
+ catch (err) {
352
+ if (err instanceof ExitSignal)
353
+ throw err;
354
+ console.error(`❌ dexto list-agents command failed: ${err}`);
355
+ safeExit('list-agents', 1, 'error');
356
+ }
357
+ }));
358
+ // 9) `which` SUB-COMMAND
359
+ program
360
+ .command('which <agent>')
361
+ .description('Show the path to an agent')
362
+ .action(withAnalytics('which', async (agent) => {
363
+ try {
364
+ await handleWhichCommand(agent);
365
+ safeExit('which', 0);
366
+ }
367
+ catch (err) {
368
+ if (err instanceof ExitSignal)
369
+ throw err;
370
+ console.error(`❌ dexto which command failed: ${err}`);
371
+ safeExit('which', 1, 'error');
372
+ }
373
+ }));
374
+ // 10) `sync-agents` SUB-COMMAND
375
+ program
376
+ .command('sync-agents')
377
+ .description('Sync installed agents with bundled versions')
378
+ .option('--list', 'List agent status without updating')
379
+ .option('--force', 'Update all agents without prompting')
380
+ .action(withAnalytics('sync-agents', async (options) => {
381
+ try {
382
+ await handleSyncAgentsCommand(options);
383
+ safeExit('sync-agents', 0);
384
+ }
385
+ catch (err) {
386
+ if (err instanceof ExitSignal)
387
+ throw err;
388
+ console.error(`❌ dexto sync-agents command failed: ${err}`);
389
+ safeExit('sync-agents', 1, 'error');
390
+ }
391
+ }));
392
+ // 11) `plugin` SUB-COMMAND
393
+ const pluginCommand = program.command('plugin').description('Manage plugins');
394
+ pluginCommand
395
+ .command('list')
396
+ .description('List installed plugins')
397
+ .option('--verbose', 'Show detailed plugin information')
398
+ .action(withAnalytics('plugin list', async (options) => {
399
+ try {
400
+ await handlePluginListCommand(options);
401
+ safeExit('plugin list', 0);
402
+ }
403
+ catch (err) {
404
+ if (err instanceof ExitSignal)
405
+ throw err;
406
+ console.error(`❌ dexto plugin list command failed: ${err}`);
407
+ safeExit('plugin list', 1, 'error');
408
+ }
409
+ }));
410
+ pluginCommand
411
+ .command('install')
412
+ .description('Install a plugin from a local directory')
413
+ .requiredOption('--path <path>', 'Path to the plugin directory')
414
+ .option('--scope <scope>', 'Installation scope: user, project, or local', 'user')
415
+ .option('--force', 'Force overwrite if already installed')
416
+ .action(withAnalytics('plugin install', async (options) => {
417
+ try {
418
+ await handlePluginInstallCommand(options);
419
+ safeExit('plugin install', 0);
420
+ }
421
+ catch (err) {
422
+ if (err instanceof ExitSignal)
423
+ throw err;
424
+ console.error(`❌ dexto plugin install command failed: ${err}`);
425
+ safeExit('plugin install', 1, 'error');
426
+ }
427
+ }));
428
+ pluginCommand
429
+ .command('uninstall <name>')
430
+ .description('Uninstall a plugin by name')
431
+ .action(withAnalytics('plugin uninstall', async (name) => {
432
+ try {
433
+ await handlePluginUninstallCommand({ name });
434
+ safeExit('plugin uninstall', 0);
435
+ }
436
+ catch (err) {
437
+ if (err instanceof ExitSignal)
438
+ throw err;
439
+ console.error(`❌ dexto plugin uninstall command failed: ${err}`);
440
+ safeExit('plugin uninstall', 1, 'error');
441
+ }
442
+ }));
443
+ pluginCommand
444
+ .command('validate [path]')
445
+ .description('Validate a plugin directory structure')
446
+ .action(withAnalytics('plugin validate', async (path) => {
447
+ try {
448
+ await handlePluginValidateCommand({ path: path || '.' });
449
+ safeExit('plugin validate', 0);
450
+ }
451
+ catch (err) {
452
+ if (err instanceof ExitSignal)
453
+ throw err;
454
+ console.error(`❌ dexto plugin validate command failed: ${err}`);
455
+ safeExit('plugin validate', 1, 'error');
456
+ }
457
+ }));
458
+ // 12) `plugin marketplace` SUB-COMMANDS
459
+ const marketplaceCommand = pluginCommand
460
+ .command('marketplace')
461
+ .alias('market')
462
+ .description('Manage plugin marketplaces');
463
+ marketplaceCommand
464
+ .command('add <source>')
465
+ .description('Add a marketplace (GitHub: owner/repo, git URL, or local path)')
466
+ .option('--name <name>', 'Custom name for the marketplace')
467
+ .action(withAnalytics('plugin marketplace add', async (source, options) => {
468
+ try {
469
+ await handleMarketplaceAddCommand({ source, name: options.name });
470
+ safeExit('plugin marketplace add', 0);
471
+ }
472
+ catch (err) {
473
+ if (err instanceof ExitSignal)
474
+ throw err;
475
+ console.error(`❌ dexto plugin marketplace add command failed: ${err}`);
476
+ safeExit('plugin marketplace add', 1, 'error');
477
+ }
478
+ }));
479
+ marketplaceCommand
480
+ .command('list')
481
+ .description('List registered marketplaces')
482
+ .option('--verbose', 'Show detailed marketplace information')
483
+ .action(withAnalytics('plugin marketplace list', async (options) => {
484
+ try {
485
+ await handleMarketplaceListCommand(options);
486
+ safeExit('plugin marketplace list', 0);
487
+ }
488
+ catch (err) {
489
+ if (err instanceof ExitSignal)
490
+ throw err;
491
+ console.error(`❌ dexto plugin marketplace list command failed: ${err}`);
492
+ safeExit('plugin marketplace list', 1, 'error');
493
+ }
494
+ }));
495
+ marketplaceCommand
496
+ .command('remove <name>')
497
+ .alias('rm')
498
+ .description('Remove a registered marketplace')
499
+ .action(withAnalytics('plugin marketplace remove', async (name) => {
500
+ try {
501
+ await handleMarketplaceRemoveCommand({ name });
502
+ safeExit('plugin marketplace remove', 0);
503
+ }
504
+ catch (err) {
505
+ if (err instanceof ExitSignal)
506
+ throw err;
507
+ console.error(`❌ dexto plugin marketplace remove command failed: ${err}`);
508
+ safeExit('plugin marketplace remove', 1, 'error');
509
+ }
510
+ }));
511
+ marketplaceCommand
512
+ .command('update [name]')
513
+ .description('Update marketplace(s) from remote (git pull)')
514
+ .action(withAnalytics('plugin marketplace update', async (name) => {
515
+ try {
516
+ await handleMarketplaceUpdateCommand({ name });
517
+ safeExit('plugin marketplace update', 0);
518
+ }
519
+ catch (err) {
520
+ if (err instanceof ExitSignal)
521
+ throw err;
522
+ console.error(`❌ dexto plugin marketplace update command failed: ${err}`);
523
+ safeExit('plugin marketplace update', 1, 'error');
524
+ }
525
+ }));
526
+ marketplaceCommand
527
+ .command('plugins [marketplace]')
528
+ .description('List plugins available in marketplaces')
529
+ .option('--verbose', 'Show plugin descriptions')
530
+ .action(withAnalytics('plugin marketplace plugins', async (marketplace, options) => {
531
+ try {
532
+ await handleMarketplacePluginsCommand({
533
+ marketplace,
534
+ verbose: options?.verbose,
535
+ });
536
+ safeExit('plugin marketplace plugins', 0);
537
+ }
538
+ catch (err) {
539
+ if (err instanceof ExitSignal)
540
+ throw err;
541
+ console.error(`❌ dexto plugin marketplace plugins command failed: ${err}`);
542
+ safeExit('plugin marketplace plugins', 1, 'error');
543
+ }
544
+ }));
545
+ marketplaceCommand
546
+ .command('install <plugin>')
547
+ .description('Install a plugin from marketplace (plugin or plugin@marketplace)')
548
+ .option('--scope <scope>', 'Installation scope: user, project, or local', 'user')
549
+ .option('--force', 'Force reinstall if already exists')
550
+ .action(withAnalytics('plugin marketplace install', async (plugin, options) => {
551
+ try {
552
+ await handleMarketplaceInstallCommand({ ...options, plugin });
553
+ safeExit('plugin marketplace install', 0);
554
+ }
555
+ catch (err) {
556
+ if (err instanceof ExitSignal)
557
+ throw err;
558
+ console.error(`❌ dexto plugin marketplace install command failed: ${err}`);
559
+ safeExit('plugin marketplace install', 1, 'error');
560
+ }
561
+ }));
562
+ // Helper to bootstrap a minimal agent for non-interactive session/search ops
563
+ async function bootstrapAgentFromGlobalOpts() {
564
+ const globalOpts = program.opts();
565
+ const resolvedPath = await resolveAgentPath(globalOpts.agent, globalOpts.autoInstall !== false);
566
+ const rawConfig = await loadAgentConfig(resolvedPath);
567
+ const mergedConfig = applyCLIOverrides(rawConfig, globalOpts);
568
+ // Load image first to apply defaults and resolve DI services
569
+ // Priority: CLI flag > Agent config > Environment variable > Default
570
+ const imageName = globalOpts.image || // --image flag
571
+ mergedConfig.image || // image field in agent config
572
+ process.env.DEXTO_IMAGE || // DEXTO_IMAGE env var
573
+ '@dexto/image-local'; // Default for convenience
574
+ let image;
575
+ try {
576
+ image = await loadImage(imageName);
577
+ }
578
+ catch (err) {
579
+ console.error(`❌ Failed to load image '${imageName}'`);
580
+ if (err instanceof Error) {
581
+ console.error(err.message);
582
+ }
583
+ console.error(`💡 Install it with: dexto image install ${imageName}`);
584
+ safeExit('bootstrap', 1, 'image-load-failed');
585
+ }
586
+ const configWithImageDefaults = applyImageDefaults(mergedConfig, image.defaults);
587
+ // Enrich config with per-agent paths BEFORE validation
588
+ const enrichedConfig = enrichAgentConfig(configWithImageDefaults, resolvedPath, {
589
+ logLevel: 'info', // CLI uses info-level logging for visibility
590
+ });
591
+ // Override approval config for read-only commands (never run conversations)
592
+ // This avoids needing to set up unused approval handlers
593
+ enrichedConfig.permissions = {
594
+ ...(enrichedConfig.permissions ?? {}),
595
+ mode: 'auto-approve',
596
+ };
597
+ enrichedConfig.elicitation = {
598
+ enabled: false,
599
+ ...(enrichedConfig.elicitation?.timeout !== undefined && {
600
+ timeout: enrichedConfig.elicitation.timeout,
601
+ }),
602
+ };
603
+ const validatedConfig = AgentConfigSchema.parse(enrichedConfig);
604
+ const services = await resolveServicesFromConfig(validatedConfig, image);
605
+ const agent = new DextoAgent(toDextoAgentOptions({ config: validatedConfig, services }));
606
+ await agent.start();
607
+ // Register graceful shutdown
608
+ const shutdown = async () => {
609
+ try {
610
+ await agent.stop();
611
+ }
612
+ catch (_err) {
613
+ // Ignore shutdown errors
614
+ }
615
+ };
616
+ process.on('SIGINT', shutdown);
617
+ process.on('SIGTERM', shutdown);
618
+ return agent;
619
+ }
620
+ // Helper to find the most recent session
621
+ async function getMostRecentSessionId(agent) {
622
+ const sessionIds = await agent.listSessions();
623
+ if (sessionIds.length === 0) {
624
+ return null;
625
+ }
626
+ // Get metadata for all sessions to find most recent
627
+ let mostRecentId = null;
628
+ let mostRecentActivity = 0;
629
+ for (const sessionId of sessionIds) {
630
+ const metadata = await agent.getSessionMetadata(sessionId);
631
+ if (metadata && metadata.lastActivity > mostRecentActivity) {
632
+ mostRecentActivity = metadata.lastActivity;
633
+ mostRecentId = sessionId;
634
+ }
635
+ }
636
+ return mostRecentId;
637
+ }
638
+ // 11) `session` SUB-COMMAND
639
+ const sessionCommand = program.command('session').description('Manage chat sessions');
640
+ sessionCommand
641
+ .command('list')
642
+ .description('List all sessions')
643
+ .action(withAnalytics('session list', async () => {
644
+ try {
645
+ const agent = await bootstrapAgentFromGlobalOpts();
646
+ await handleSessionListCommand(agent);
647
+ await agent.stop();
648
+ safeExit('session list', 0);
649
+ }
650
+ catch (err) {
651
+ if (err instanceof ExitSignal)
652
+ throw err;
653
+ console.error(`❌ dexto session list command failed: ${err}`);
654
+ safeExit('session list', 1, 'error');
655
+ }
656
+ }));
657
+ sessionCommand
658
+ .command('history')
659
+ .description('Show session history')
660
+ .argument('[sessionId]', 'Session ID (defaults to current session)')
661
+ .action(withAnalytics('session history', async (sessionId) => {
662
+ try {
663
+ const agent = await bootstrapAgentFromGlobalOpts();
664
+ await handleSessionHistoryCommand(agent, sessionId);
665
+ await agent.stop();
666
+ safeExit('session history', 0);
667
+ }
668
+ catch (err) {
669
+ if (err instanceof ExitSignal)
670
+ throw err;
671
+ console.error(`❌ dexto session history command failed: ${err}`);
672
+ safeExit('session history', 1, 'error');
673
+ }
674
+ }));
675
+ sessionCommand
676
+ .command('delete')
677
+ .description('Delete a session')
678
+ .argument('<sessionId>', 'Session ID to delete')
679
+ .action(withAnalytics('session delete', async (sessionId) => {
680
+ try {
681
+ const agent = await bootstrapAgentFromGlobalOpts();
682
+ await handleSessionDeleteCommand(agent, sessionId);
683
+ await agent.stop();
684
+ safeExit('session delete', 0);
685
+ }
686
+ catch (err) {
687
+ if (err instanceof ExitSignal)
688
+ throw err;
689
+ console.error(`❌ dexto session delete command failed: ${err}`);
690
+ safeExit('session delete', 1, 'error');
691
+ }
692
+ }));
693
+ // 12) `search` SUB-COMMAND
694
+ program
695
+ .command('search')
696
+ .description('Search session history')
697
+ .argument('<query>', 'Search query')
698
+ .option('--session <sessionId>', 'Search in specific session')
699
+ .option('--role <role>', 'Filter by role (user, assistant, system, tool)')
700
+ .option('--limit <number>', 'Limit number of results', '10')
701
+ .action(withAnalytics('search', async (query, options) => {
702
+ try {
703
+ const agent = await bootstrapAgentFromGlobalOpts();
704
+ const searchOptions = {};
705
+ if (options.session) {
706
+ searchOptions.sessionId = options.session;
707
+ }
708
+ if (options.role) {
709
+ const allowed = new Set(['user', 'assistant', 'system', 'tool']);
710
+ if (!allowed.has(options.role)) {
711
+ console.error(`❌ Invalid role: ${options.role}. Use one of: user, assistant, system, tool`);
712
+ safeExit('search', 1, 'invalid-role');
713
+ }
714
+ searchOptions.role = options.role;
715
+ }
716
+ if (options.limit) {
717
+ const parsed = parseInt(options.limit, 10);
718
+ if (Number.isNaN(parsed) || parsed <= 0) {
719
+ console.error(`❌ Invalid --limit: ${options.limit}. Use a positive integer (e.g., 10).`);
720
+ safeExit('search', 1, 'invalid-limit');
721
+ }
722
+ searchOptions.limit = parsed;
723
+ }
724
+ await handleSessionSearchCommand(agent, query, searchOptions);
725
+ await agent.stop();
726
+ safeExit('search', 0);
727
+ }
728
+ catch (err) {
729
+ if (err instanceof ExitSignal)
730
+ throw err;
731
+ console.error(`❌ dexto search command failed: ${err}`);
732
+ safeExit('search', 1, 'error');
733
+ }
734
+ }));
735
+ // 13) `auth` SUB-COMMAND GROUP
736
+ const authCommand = program.command('auth').description('Manage authentication');
737
+ authCommand
738
+ .command('login')
739
+ .description('Login to Dexto')
740
+ .option('--api-key <key>', 'Use Dexto API key instead of browser login')
741
+ .option('--no-interactive', 'Disable interactive prompts')
742
+ .action(withAnalytics('auth login', async (options) => {
743
+ try {
744
+ await handleLoginCommand(options);
745
+ safeExit('auth login', 0);
746
+ }
747
+ catch (err) {
748
+ if (err instanceof ExitSignal)
749
+ throw err;
750
+ console.error(`❌ dexto auth login command failed: ${err}`);
751
+ safeExit('auth login', 1, 'error');
752
+ }
753
+ }));
754
+ authCommand
755
+ .command('logout')
756
+ .description('Logout from Dexto')
757
+ .option('--force', 'Skip confirmation prompt')
758
+ .option('--no-interactive', 'Disable interactive prompts')
759
+ .action(withAnalytics('auth logout', async (options) => {
760
+ try {
761
+ await handleLogoutCommand(options);
762
+ safeExit('auth logout', 0);
763
+ }
764
+ catch (err) {
765
+ if (err instanceof ExitSignal)
766
+ throw err;
767
+ console.error(`❌ dexto auth logout command failed: ${err}`);
768
+ safeExit('auth logout', 1, 'error');
769
+ }
770
+ }));
771
+ authCommand
772
+ .command('status')
773
+ .description('Show authentication status')
774
+ .action(withAnalytics('auth status', async () => {
775
+ try {
776
+ await handleStatusCommand();
777
+ safeExit('auth status', 0);
778
+ }
779
+ catch (err) {
780
+ if (err instanceof ExitSignal)
781
+ throw err;
782
+ console.error(`❌ dexto auth status command failed: ${err}`);
783
+ safeExit('auth status', 1, 'error');
784
+ }
785
+ }));
786
+ // Also add convenience aliases at root level
787
+ program
788
+ .command('login')
789
+ .description('Login to Dexto (alias for `dexto auth login`)')
790
+ .option('--api-key <key>', 'Use Dexto API key instead of browser login')
791
+ .option('--no-interactive', 'Disable interactive prompts')
792
+ .action(withAnalytics('login', async (options) => {
793
+ try {
794
+ await handleLoginCommand(options);
795
+ safeExit('login', 0);
796
+ }
797
+ catch (err) {
798
+ if (err instanceof ExitSignal)
799
+ throw err;
800
+ console.error(`❌ dexto login command failed: ${err}`);
801
+ safeExit('login', 1, 'error');
802
+ }
803
+ }));
804
+ program
805
+ .command('logout')
806
+ .description('Logout from Dexto (alias for `dexto auth logout`)')
807
+ .option('--force', 'Skip confirmation prompt')
808
+ .option('--no-interactive', 'Disable interactive prompts')
809
+ .action(withAnalytics('logout', async (options) => {
810
+ try {
811
+ await handleLogoutCommand(options);
812
+ safeExit('logout', 0);
813
+ }
814
+ catch (err) {
815
+ if (err instanceof ExitSignal)
816
+ throw err;
817
+ console.error(`❌ dexto logout command failed: ${err}`);
818
+ safeExit('logout', 1, 'error');
819
+ }
820
+ }));
821
+ // 14) `billing` COMMAND
822
+ program
823
+ .command('billing')
824
+ .description('Show billing status and credit balance')
825
+ .option('--buy', 'Open Dexto Nova credits purchase page')
826
+ .action(withAnalytics('billing', async (options) => {
827
+ try {
828
+ await handleBillingStatusCommand(options);
829
+ safeExit('billing', 0);
830
+ }
831
+ catch (err) {
832
+ if (err instanceof ExitSignal)
833
+ throw err;
834
+ console.error(`❌ dexto billing command failed: ${err}`);
835
+ safeExit('billing', 1, 'error');
836
+ }
837
+ }));
838
+ // 15) `mcp` SUB-COMMAND
839
+ // For now, this mode simply aggregates and re-expose tools from configured MCP servers (no agent)
840
+ // dexto --mode mcp will be moved to this sub-command in the future
841
+ program
842
+ .command('mcp')
843
+ .description('Start Dexto as an MCP server. Use --group-servers to aggregate and re-expose tools from configured MCP servers. \
844
+ In the future, this command will expose the agent as an MCP server by default.')
845
+ .option('-s, --strict', 'Require all MCP server connections to succeed')
846
+ .option('--group-servers', 'Aggregate and re-expose tools from configured MCP servers (required for now)')
847
+ .option('--name <n>', 'Name for the MCP server', 'dexto-tools')
848
+ .option('--version <version>', 'Version for the MCP server', '1.0.0')
849
+ .action(withAnalytics('mcp', async (options) => {
850
+ try {
851
+ // Validate that --group-servers flag is provided (mandatory for now)
852
+ if (!options.groupServers) {
853
+ console.error('❌ The --group-servers flag is required. This command currently only supports aggregating and re-exposing tools from configured MCP servers.');
854
+ console.error('Usage: dexto mcp --group-servers');
855
+ safeExit('mcp', 1, 'missing-group-servers');
856
+ }
857
+ // Load and resolve config
858
+ // Get the global agent option from the main program
859
+ const globalOpts = program.opts();
860
+ const nameOrPath = globalOpts.agent;
861
+ const configPath = await resolveAgentPath(nameOrPath, globalOpts.autoInstall !== false);
862
+ console.log(`📄 Loading Dexto config from: ${configPath}`);
863
+ const config = await loadAgentConfig(configPath);
864
+ logger.info(`Validating MCP servers...`);
865
+ // Validate that MCP servers are configured
866
+ if (!config.mcpServers || Object.keys(config.mcpServers).length === 0) {
867
+ console.error('❌ No MCP servers configured. Please configure mcpServers in your config file.');
868
+ safeExit('mcp', 1, 'no-mcp-servers');
869
+ }
870
+ const { ServersConfigSchema } = await import('@dexto/core');
871
+ const validatedServers = ServersConfigSchema.parse(config.mcpServers);
872
+ logger.info(`Validated MCP servers. Configured servers: ${Object.keys(validatedServers).join(', ')}`);
873
+ // Logs are already redirected to file by default to prevent interference with stdio transport
874
+ const currentLogPath = logger.getLogFilePath();
875
+ logger.info(`MCP mode using log file: ${currentLogPath || 'default .dexto location'}`);
876
+ logger.info(`Starting MCP tool aggregation server: ${options.name} v${options.version}`);
877
+ // Create stdio transport for MCP tool aggregation
878
+ const mcpTransport = await createMcpTransport('stdio');
879
+ // Initialize tool aggregation server
880
+ await initializeMcpToolAggregationServer(validatedServers, mcpTransport, options.name, options.version, options.strict);
881
+ logger.info('MCP tool aggregation server started successfully');
882
+ }
883
+ catch (err) {
884
+ if (err instanceof ExitSignal)
885
+ throw err;
886
+ // Write to stderr to avoid interfering with MCP protocol
887
+ process.stderr.write(`MCP tool aggregation server startup failed: ${err}\n`);
888
+ safeExit('mcp', 1, 'mcp-agg-failed');
889
+ }
890
+ }, { timeoutMs: 0 }));
891
+ // 16) Main dexto CLI - Interactive/One shot (CLI/HEADLESS) or run in other modes (--mode web/server/mcp)
892
+ program
893
+ // Main customer facing description
894
+ .description('Dexto CLI - AI-powered assistant with session management.\n\n' +
895
+ 'Basic Usage:\n' +
896
+ ' dexto Start web UI (default)\n' +
897
+ ' dexto --mode cli Start interactive CLI\n' +
898
+ ' dexto --prompt "query" Start interactive CLI and run the prompt\n\n' +
899
+ 'Session Management Commands:\n' +
900
+ ' dexto session list List all sessions\n' +
901
+ ' dexto session history [id] Show session history\n' +
902
+ ' dexto session delete <id> Delete a session\n' +
903
+ ' dexto search <query> Search across sessions\n' +
904
+ ' Options: --session <id>, --role <user|assistant>, --limit <n>\n\n' +
905
+ 'Agent Selection:\n' +
906
+ ' dexto --agent coding-agent Use installed agent by name\n' +
907
+ ' dexto --agent ./my-agent.yml Use agent from file path\n' +
908
+ ' dexto -a agents/custom.yml Short form with relative path\n\n' +
909
+ 'Tool Confirmation:\n' +
910
+ ' dexto --auto-approve Auto-approve all tool executions\n\n' +
911
+ 'Advanced Modes:\n' +
912
+ ' dexto --mode server Run as API server\n' +
913
+ ' dexto --mode mcp Run as MCP server\n\n' +
914
+ 'Docs: https://docs.dexto.ai')
915
+ .action(withAnalytics('main', async () => {
916
+ // ——— ENV CHECK (optional) ———
917
+ if (!existsSync('.env')) {
918
+ logger.debug('WARNING: .env file not found; copy .env.example and set your API keys.');
919
+ }
920
+ const opts = program.opts();
921
+ // Set dev mode early to use local repo agents instead of ~/.dexto
922
+ if (opts.dev) {
923
+ process.env.DEXTO_DEV_MODE = 'true';
924
+ }
925
+ // ——— LOAD DEFAULT MODE FROM PREFERENCES ———
926
+ // If --mode was not explicitly provided on CLI, use defaultMode from preferences
927
+ const modeSource = program.getOptionValueSource('mode');
928
+ const explicitModeProvided = modeSource === 'cli';
929
+ if (!explicitModeProvided) {
930
+ try {
931
+ if (globalPreferencesExist()) {
932
+ const preferences = await loadGlobalPreferences();
933
+ if (preferences.defaults?.defaultMode) {
934
+ opts.mode = preferences.defaults.defaultMode;
935
+ logger.debug(`Using default mode from preferences: ${opts.mode}`);
936
+ }
937
+ }
938
+ }
939
+ catch (error) {
940
+ // Silently fall back to hardcoded default if preferences loading fails
941
+ logger.debug(`Failed to load default mode from preferences: ${error instanceof Error ? error.message : String(error)}`);
942
+ }
943
+ }
944
+ const initialPrompt = opts.prompt !== undefined ? String(opts.prompt) : undefined;
945
+ if (initialPrompt !== undefined && initialPrompt.trim() === '') {
946
+ console.error('❌ Prompt cannot be empty. Provide a non-empty prompt with -p/--prompt.');
947
+ safeExit('main', 1, 'empty-prompt');
948
+ }
949
+ // Note: Agent selection must be passed via -a/--agent. We no longer interpret
950
+ // the first positional argument as an agent name to avoid ambiguity with prompts.
951
+ // ——— FORCE CLI MODE FOR PROMPT/SESSION FLAGS ———
952
+ // If a prompt or session flag was provided, force CLI mode.
953
+ if ((initialPrompt || opts.continue || opts.resume) && opts.mode !== 'cli') {
954
+ console.error(`ℹ️ Forcing CLI mode due to --prompt/--continue/--resume.`);
955
+ console.error(` Original mode: ${opts.mode} → Overridden to: cli`);
956
+ opts.mode = 'cli';
957
+ }
958
+ // The interactive CLI requires a TTY (Ink uses stdin for keypress input).
959
+ if (opts.mode === 'cli' && !process.stdin.isTTY) {
960
+ console.error('❌ Interactive CLI requires a TTY.');
961
+ console.error('💡 Headless one-shot mode has been removed. Run in an interactive terminal, or use --mode server for automation.');
962
+ safeExit('main', 1, 'no-tty');
963
+ }
964
+ // ——— Infer provider & API key from model ———
965
+ if (opts.model) {
966
+ if (opts.model.includes('/')) {
967
+ console.error(`❌ Model '${opts.model}' looks like an OpenRouter-format ID (provider/model).`);
968
+ console.error(` This is ambiguous for --model inference. Please also pass --provider (e.g. --provider dexto-nova or --provider openrouter).`);
969
+ safeExit('main', 1, 'ambiguous-model');
970
+ }
971
+ let provider;
972
+ try {
973
+ provider = getProviderFromModel(opts.model);
974
+ }
975
+ catch (err) {
976
+ console.error(`❌ ${err.message}`);
977
+ console.error(`Supported models: ${getAllSupportedModels().join(', ')}`);
978
+ safeExit('main', 1, 'invalid-model');
979
+ }
980
+ const apiKey = resolveApiKeyForProvider(provider);
981
+ if (!apiKey) {
982
+ const envVar = getPrimaryApiKeyEnvVar(provider);
983
+ console.error(`❌ Missing API key for provider '${provider}' - please set $${envVar}`);
984
+ safeExit('main', 1, 'missing-api-key');
985
+ }
986
+ opts.provider = provider;
987
+ opts.apiKey = apiKey;
988
+ }
989
+ try {
990
+ validateCliOptions(opts);
991
+ }
992
+ catch (err) {
993
+ handleCliOptionsError(err);
994
+ }
995
+ // ——— ENHANCED PREFERENCE-AWARE CONFIG LOADING ———
996
+ let validatedConfig;
997
+ let resolvedPath;
998
+ let image;
999
+ let imageName;
1000
+ // Determine validation mode early - used throughout config loading and agent creation
1001
+ // Use relaxed validation for interactive modes (web/cli) where users can configure later
1002
+ // Use strict validation for non-interactive modes (server/mcp) that need full config upfront
1003
+ const isInteractiveMode = opts.mode === 'web' || opts.mode === 'cli';
1004
+ try {
1005
+ // Case 1: File path - skip all validation and setup
1006
+ if (opts.agent && isPath(opts.agent)) {
1007
+ resolvedPath = await resolveAgentPath(opts.agent, opts.autoInstall !== false);
1008
+ }
1009
+ // Cases 2 & 3: Default agent or registry agent
1010
+ else {
1011
+ // Early registry validation for named agents
1012
+ if (opts.agent) {
1013
+ // Load bundled registry to check if agent exists
1014
+ try {
1015
+ const bundledRegistryPath = resolveBundledScript('agents/agent-registry.json');
1016
+ const registryContent = readFileSync(bundledRegistryPath, 'utf-8');
1017
+ const bundledRegistry = JSON.parse(registryContent);
1018
+ // Check if agent exists in bundled registry
1019
+ if (!(opts.agent in bundledRegistry.agents)) {
1020
+ console.error(`❌ Agent '${opts.agent}' not found in registry`);
1021
+ // Show available agents
1022
+ const available = Object.keys(bundledRegistry.agents);
1023
+ if (available.length > 0) {
1024
+ console.log(`📋 Available agents: ${available.join(', ')}`);
1025
+ }
1026
+ else {
1027
+ console.log('📋 No agents available in registry');
1028
+ }
1029
+ safeExit('main', 1, 'agent-not-in-registry');
1030
+ return;
1031
+ }
1032
+ }
1033
+ catch (error) {
1034
+ logger.warn(`Could not validate agent against registry: ${error instanceof Error ? error.message : String(error)}`);
1035
+ // Continue anyway - resolver will handle it
1036
+ }
1037
+ }
1038
+ // Check setup state and auto-trigger if needed
1039
+ // Skip if --skip-setup flag is set (for MCP mode, automation, etc.)
1040
+ if (!opts.skipSetup && (await requiresSetup())) {
1041
+ if (opts.interactive === false) {
1042
+ console.error('❌ Setup required but --no-interactive flag is set.');
1043
+ console.error('💡 Run `dexto setup` first, or use --skip-setup to bypass global setup.');
1044
+ safeExit('main', 1, 'setup-required-non-interactive');
1045
+ }
1046
+ await handleSetupCommand({ interactive: true });
1047
+ // Reload preferences after setup to get the newly selected default mode
1048
+ // (setup may have just saved a different mode than the default 'web')
1049
+ try {
1050
+ const newPreferences = await loadGlobalPreferences();
1051
+ if (newPreferences.defaults?.defaultMode) {
1052
+ opts.mode = newPreferences.defaults.defaultMode;
1053
+ logger.debug(`Updated mode from setup preferences: ${opts.mode}`);
1054
+ }
1055
+ }
1056
+ catch {
1057
+ // Ignore errors - will use default mode
1058
+ }
1059
+ }
1060
+ // Now resolve agent (will auto-install since setup is complete)
1061
+ resolvedPath = await resolveAgentPath(opts.agent, opts.autoInstall !== false);
1062
+ }
1063
+ // Load raw config and apply CLI overrides
1064
+ const rawConfig = await loadAgentConfig(resolvedPath);
1065
+ let mergedConfig = applyCLIOverrides(rawConfig, opts);
1066
+ // ——— PREFERENCE-AWARE CONFIG HANDLING ———
1067
+ // User's LLM preferences from preferences.yml apply to ALL agents
1068
+ // See feature-plans/auto-update.md section 8.11 - Three-Layer LLM Resolution
1069
+ const agentId = opts.agent ?? 'coding-agent';
1070
+ let preferences = null;
1071
+ if (globalPreferencesExist()) {
1072
+ try {
1073
+ preferences = await loadGlobalPreferences();
1074
+ }
1075
+ catch {
1076
+ // Preferences exist but couldn't load - continue without them
1077
+ logger.debug('Could not load preferences, continuing without them');
1078
+ }
1079
+ }
1080
+ // Check if user is configured for Dexto credits but not authenticated
1081
+ // This can happen if user logged out after setting up with Dexto
1082
+ // Now that preferences apply to ALL agents, we check for any agent
1083
+ // Only run this check when Dexto auth feature is enabled
1084
+ if (isDextoAuthEnabled()) {
1085
+ const { checkDextoAuthState } = await import('./cli/utils/dexto-auth-check.js');
1086
+ const authCheck = await checkDextoAuthState(opts.interactive !== false, agentId);
1087
+ if (!authCheck.shouldContinue) {
1088
+ if (authCheck.action === 'login') {
1089
+ // User wants to log in - run login flow then restart
1090
+ const { handleLoginCommand } = await import('./cli/commands/auth/login.js');
1091
+ await handleLoginCommand({ interactive: true });
1092
+ // Verify key was actually provisioned (provisionKeys silently catches errors)
1093
+ const { canUseDextoProvider } = await import('./cli/utils/dexto-setup.js');
1094
+ if (!(await canUseDextoProvider())) {
1095
+ console.error('\n❌ API key provisioning failed. Please try again or run `dexto setup` to use a different provider.\n');
1096
+ safeExit('main', 1, 'dexto-key-provisioning-failed');
1097
+ }
1098
+ // After login, continue with startup (preferences unchanged, now authenticated)
1099
+ }
1100
+ else if (authCheck.action === 'setup') {
1101
+ // User wants to configure different provider - run setup
1102
+ const { handleSetupCommand } = await import('./cli/commands/setup.js');
1103
+ await handleSetupCommand({ interactive: true, force: true });
1104
+ // Reload preferences after setup
1105
+ preferences = await loadGlobalPreferences();
1106
+ }
1107
+ else {
1108
+ // User cancelled
1109
+ safeExit('main', 0, 'dexto-auth-check-cancelled');
1110
+ }
1111
+ }
1112
+ }
1113
+ // Check for pending API key setup (user skipped during initial setup)
1114
+ // Since preferences now apply to ALL agents, this check runs for any agent
1115
+ if (preferences?.setup?.apiKeyPending && opts.interactive !== false) {
1116
+ // Check if API key is still missing (user may have set it manually)
1117
+ const configuredApiKey = resolveApiKeyForProvider(preferences.llm.provider);
1118
+ if (!configuredApiKey) {
1119
+ const { promptForPendingApiKey } = await import('./cli/utils/api-key-setup.js');
1120
+ const { updateGlobalPreferences } = await import('@dexto/agent-management');
1121
+ const result = await promptForPendingApiKey(preferences.llm.provider, preferences.llm.model);
1122
+ if (result.action === 'cancel') {
1123
+ safeExit('main', 0, 'pending-api-key-cancelled');
1124
+ }
1125
+ if (result.action === 'setup' && result.apiKey) {
1126
+ // API key was configured - update preferences to clear pending flag
1127
+ await updateGlobalPreferences({
1128
+ setup: { apiKeyPending: false },
1129
+ });
1130
+ // Update the merged config with the new API key
1131
+ mergedConfig.llm.apiKey = result.apiKey;
1132
+ logger.debug('API key configured, pending flag cleared');
1133
+ }
1134
+ // If 'skip', continue without API key (user chose to proceed)
1135
+ }
1136
+ else {
1137
+ // API key exists (user set it manually) - clear the pending flag
1138
+ const { updateGlobalPreferences } = await import('@dexto/agent-management');
1139
+ await updateGlobalPreferences({
1140
+ setup: { apiKeyPending: false },
1141
+ });
1142
+ logger.debug('API key found in environment, cleared pending flag');
1143
+ }
1144
+ }
1145
+ // Apply user's LLM preferences to ALL agents (not just the default)
1146
+ // See feature-plans/auto-update.md section 8.11 - Three-Layer LLM Resolution:
1147
+ // local.llm ?? preferences.llm ?? bundled.llm
1148
+ // The preferences.llm acts as a "global .local.yml" for LLM settings
1149
+ if (preferences?.llm?.provider && preferences?.llm?.model) {
1150
+ mergedConfig = applyUserPreferences(mergedConfig, preferences);
1151
+ logger.debug(`Applied user preferences to ${agentId}`, {
1152
+ provider: preferences.llm.provider,
1153
+ model: preferences.llm.model,
1154
+ });
1155
+ }
1156
+ // Clean up null values from config (can happen from YAML files with explicit nulls)
1157
+ // This prevents "Expected string, received null" errors for optional fields
1158
+ const cleanedConfig = cleanNullValues(mergedConfig);
1159
+ // Load image first to apply defaults and resolve DI services
1160
+ // Priority: CLI flag > Agent config > Environment variable > Default
1161
+ imageName =
1162
+ opts.image || // --image flag
1163
+ cleanedConfig.image || // image field in agent config
1164
+ process.env.DEXTO_IMAGE || // DEXTO_IMAGE env var
1165
+ '@dexto/image-local'; // Default for convenience
1166
+ try {
1167
+ image = await loadImage(imageName);
1168
+ logger.debug(`Loaded image: ${imageName}`);
1169
+ }
1170
+ catch (err) {
1171
+ console.error(`❌ Failed to load image '${imageName}'`);
1172
+ if (err instanceof Error) {
1173
+ console.error(err.message);
1174
+ logger.debug(`Image load error: ${err.message}`);
1175
+ }
1176
+ console.error(`💡 Install it with: dexto image install ${imageName}`);
1177
+ safeExit('main', 1, 'image-load-failed');
1178
+ }
1179
+ const configWithImageDefaults = applyImageDefaults(cleanedConfig, image.defaults);
1180
+ // Enrich config with per-agent paths BEFORE validation
1181
+ // Enrichment adds filesystem paths to storage (schema has in-memory defaults)
1182
+ // Interactive CLI mode: only log to file (console would interfere with chat UI)
1183
+ const isInteractiveCli = opts.mode === 'cli';
1184
+ const enrichedConfig = enrichAgentConfig(configWithImageDefaults, resolvedPath, {
1185
+ isInteractiveCli,
1186
+ logLevel: 'info', // CLI uses info-level logging for visibility
1187
+ });
1188
+ // Validate enriched config + preflight credentials.
1189
+ // - Interactive modes (web/cli): warn and continue when credentials are missing
1190
+ // - Headless modes (server/mcp): treat missing credentials as errors
1191
+ const validationResult = await validateAgentConfig(enrichedConfig, opts.interactive !== false, {
1192
+ credentialPolicy: isInteractiveMode ? 'warn' : 'error',
1193
+ agentPath: resolvedPath,
1194
+ });
1195
+ if (validationResult.success && validationResult.config) {
1196
+ validatedConfig = validationResult.config;
1197
+ }
1198
+ else if (validationResult.skipped) {
1199
+ // User chose to continue despite validation errors
1200
+ // SAFETY: This cast is intentionally unsafe - it's an escape hatch for users
1201
+ // when validation is overly strict or incorrect. Runtime errors will surface
1202
+ // if the config truly doesn't work. Future: explicit `allowUnvalidated` mode.
1203
+ logger.warn('Starting with validation warnings - some features may not work');
1204
+ validatedConfig = enrichedConfig;
1205
+ }
1206
+ else {
1207
+ // Validation failed and user didn't skip - show next steps and exit
1208
+ safeExit('main', 1, 'config-validation-failed');
1209
+ }
1210
+ // Validate that if config specifies an image, it matches what was loaded
1211
+ // Skip this check if user explicitly provided --image flag (intentional override)
1212
+ // Note: Image was already loaded earlier before enrichment
1213
+ if (!opts.image &&
1214
+ validatedConfig.image &&
1215
+ validatedConfig.image !== imageName) {
1216
+ console.error(`❌ Config specifies image '${validatedConfig.image}' but '${imageName}' was loaded instead`);
1217
+ console.error(`💡 Either remove 'image' from config or ensure it matches the loaded image`);
1218
+ safeExit('main', 1, 'image-mismatch');
1219
+ }
1220
+ }
1221
+ catch (err) {
1222
+ if (err instanceof ExitSignal)
1223
+ throw err;
1224
+ // Config loading failed completely
1225
+ console.error(`❌ Failed to load configuration: ${err}`);
1226
+ safeExit('main', 1, 'config-load-failed');
1227
+ }
1228
+ // ——— VALIDATE APPROVAL MODE COMPATIBILITY ———
1229
+ // Check if approval handler is needed (manual mode OR elicitation enabled)
1230
+ const needsHandler = validatedConfig.permissions.mode === 'manual' ||
1231
+ validatedConfig.elicitation.enabled;
1232
+ if (needsHandler) {
1233
+ // Only web, server, and interactive CLI support approval handlers
1234
+ // TODO: Add approval support for other modes:
1235
+ // - Discord: Could use Discord message buttons/reactions for approval UI
1236
+ // - Telegram: Could use Telegram inline keyboards for approval prompts
1237
+ // - MCP: Could implement callback-based approval mechanism in MCP protocol
1238
+ const supportedModes = ['web', 'server', 'cli'];
1239
+ if (!supportedModes.includes(opts.mode)) {
1240
+ console.error(`❌ Manual approval and elicitation are not supported in "${opts.mode}" mode.`);
1241
+ console.error(`💡 These features require interactive UI and are only supported in: ${supportedModes.join(', ')}`);
1242
+ console.error('💡 Run `dexto --auto-approve` or configure your agent to skip approvals when running non-interactively.');
1243
+ console.error(' permissions.mode: auto-approve (or auto-deny if you want to deny certain tools)');
1244
+ console.error(' elicitation.enabled: false');
1245
+ safeExit('main', 1, 'approval-unsupported-mode');
1246
+ }
1247
+ }
1248
+ // ——— CREATE AGENT ———
1249
+ let agent;
1250
+ let derivedAgentId;
1251
+ try {
1252
+ // Set run mode for tool confirmation provider
1253
+ process.env.DEXTO_RUN_MODE = opts.mode;
1254
+ // Apply --strict flag to all server configs
1255
+ if (opts.strict && validatedConfig.mcpServers) {
1256
+ for (const [_serverName, serverConfig] of Object.entries(validatedConfig.mcpServers)) {
1257
+ // All server config types have connectionMode field
1258
+ serverConfig.connectionMode = 'strict';
1259
+ }
1260
+ }
1261
+ // Config is already enriched and validated - ready for agent creation
1262
+ // DextoAgent will parse/validate again (parse-twice pattern)
1263
+ // isInteractiveMode is already defined above for validateAgentConfig
1264
+ const sessionLoggerFactory = createFileSessionLoggerFactory();
1265
+ const mcpAuthProviderFactory = opts.mode === 'cli'
1266
+ ? (await import('./cli/mcp/oauth-factory.js')).createMcpAuthProviderFactory({
1267
+ logger,
1268
+ })
1269
+ : null;
1270
+ const services = await resolveServicesFromConfig(validatedConfig, image);
1271
+ agent = new DextoAgent(toDextoAgentOptions({
1272
+ config: validatedConfig,
1273
+ services,
1274
+ overrides: { sessionLoggerFactory, mcpAuthProviderFactory },
1275
+ }));
1276
+ // Start the agent (initialize async services)
1277
+ // - web/server modes: initializeHonoApi will set approval handler and start the agent
1278
+ // - cli mode: handles its own approval setup in the case block
1279
+ // - other modes: start immediately (no approval support)
1280
+ if (opts.mode !== 'web' && opts.mode !== 'server' && opts.mode !== 'cli') {
1281
+ await agent.start();
1282
+ }
1283
+ // Derive a concise agent ID for display purposes (used by API/UI)
1284
+ // Prefer agentCard.name, otherwise extract from filename
1285
+ derivedAgentId =
1286
+ validatedConfig.agentCard?.name ||
1287
+ path.basename(resolvedPath, path.extname(resolvedPath));
1288
+ }
1289
+ catch (err) {
1290
+ if (err instanceof ExitSignal)
1291
+ throw err;
1292
+ // Ensure config errors are shown to user, not hidden in logs
1293
+ console.error(`❌ Configuration Error: ${err.message}`);
1294
+ safeExit('main', 1, 'config-error');
1295
+ }
1296
+ // ——— Dispatch based on --mode ———
1297
+ // TODO: Refactor mode-specific logic into separate handler files
1298
+ // This switch statement has grown large with nested if-else chains for each mode.
1299
+ // Consider breaking down into mode-specific handlers (e.g., cli/modes/cli.ts, cli/modes/web.ts)
1300
+ // to improve maintainability and reduce complexity in this entry point file.
1301
+ // See PR 450 comment: https://github.com/truffle-ai/dexto/pull/450#discussion_r2546242983
1302
+ switch (opts.mode) {
1303
+ case 'cli': {
1304
+ // Set up approval handler for interactive CLI if manual mode OR elicitation enabled
1305
+ const needsHandler = validatedConfig.permissions.mode === 'manual' ||
1306
+ validatedConfig.elicitation.enabled;
1307
+ if (needsHandler) {
1308
+ // CLI uses its own approval handler that works directly with AgentEventBus
1309
+ // This avoids the indirection of ApprovalCoordinator (designed for HTTP flows)
1310
+ const { createCLIApprovalHandler } = await import('./cli/approval/index.js');
1311
+ const handler = createCLIApprovalHandler(agent);
1312
+ agent.setApprovalHandler(handler);
1313
+ logger.debug('CLI approval handler configured for Ink CLI');
1314
+ }
1315
+ // Start the agent now that approval handler is configured
1316
+ await agent.start();
1317
+ // Session management - CLI uses explicit sessionId like WebUI
1318
+ // NOTE: Migrated from defaultSession pattern which will be deprecated in core
1319
+ // We now pass sessionId explicitly to all agent methods (agent.run, agent.switchLLM, etc.)
1320
+ // Check if API key is configured before trying to create session
1321
+ // Session creation triggers LLM service init which requires API key
1322
+ const llmConfig = agent.getCurrentLLMConfig();
1323
+ const { requiresApiKey } = await import('@dexto/core');
1324
+ if (requiresApiKey(llmConfig.provider) && !llmConfig.apiKey?.trim()) {
1325
+ // Offer interactive API key setup instead of just exiting
1326
+ const { interactiveApiKeySetup } = await import('./cli/utils/api-key-setup.js');
1327
+ console.log(chalk.yellow(`\n⚠️ API key required for provider '${llmConfig.provider}'\n`));
1328
+ const setupResult = await interactiveApiKeySetup(llmConfig.provider, {
1329
+ exitOnCancel: false,
1330
+ model: llmConfig.model,
1331
+ });
1332
+ if (setupResult.cancelled) {
1333
+ await agent.stop().catch(() => { });
1334
+ safeExit('main', 0, 'api-key-setup-cancelled');
1335
+ }
1336
+ if (setupResult.skipped) {
1337
+ // User chose to skip - exit with instructions
1338
+ await agent.stop().catch(() => { });
1339
+ safeExit('main', 0, 'api-key-pending');
1340
+ }
1341
+ if (setupResult.success && setupResult.apiKey) {
1342
+ // API key was entered and saved - reload config and continue
1343
+ // Update the agent's LLM config with the new API key
1344
+ await agent.switchLLM({
1345
+ provider: llmConfig.provider,
1346
+ model: llmConfig.model,
1347
+ apiKey: setupResult.apiKey,
1348
+ });
1349
+ logger.info('API key configured successfully, continuing...');
1350
+ }
1351
+ }
1352
+ // Resolve the initial session
1353
+ let cliSessionId;
1354
+ if (opts.resume) {
1355
+ const existing = await agent.getSession(opts.resume);
1356
+ if (!existing) {
1357
+ console.error(`❌ Session '${opts.resume}' not found`);
1358
+ console.error('💡 Use `dexto session list` to see available sessions');
1359
+ safeExit('main', 1, 'resume-failed');
1360
+ }
1361
+ cliSessionId = opts.resume;
1362
+ }
1363
+ else if (opts.continue) {
1364
+ const mostRecentSessionId = await getMostRecentSessionId(agent);
1365
+ if (mostRecentSessionId) {
1366
+ cliSessionId = mostRecentSessionId;
1367
+ }
1368
+ else {
1369
+ const session = await agent.createSession();
1370
+ cliSessionId = session.id;
1371
+ }
1372
+ }
1373
+ else {
1374
+ const session = await agent.createSession();
1375
+ cliSessionId = session.id;
1376
+ }
1377
+ // Check for updates (will be shown in Ink header)
1378
+ const cliUpdateInfo = await versionCheckPromise;
1379
+ // Check if installed agents differ from bundled and prompt to sync
1380
+ const needsSync = await shouldPromptForSync();
1381
+ if (needsSync) {
1382
+ const shouldSync = await p.confirm({
1383
+ message: 'Agent config updates available. Sync now?',
1384
+ initialValue: true,
1385
+ });
1386
+ if (!p.isCancel(shouldSync) && shouldSync) {
1387
+ await handleSyncAgentsCommand({ force: true, quiet: true });
1388
+ }
1389
+ }
1390
+ // Interactive mode - use Ink CLI with session support
1391
+ // Suppress console output before starting Ink UI
1392
+ const originalConsole = {
1393
+ log: console.log,
1394
+ error: console.error,
1395
+ warn: console.warn,
1396
+ info: console.info,
1397
+ };
1398
+ const noOp = () => { };
1399
+ console.log = noOp;
1400
+ console.error = noOp;
1401
+ console.warn = noOp;
1402
+ console.info = noOp;
1403
+ let inkError = undefined;
1404
+ try {
1405
+ const { startInkCliRefactored } = await import('./cli/ink-cli/InkCLIRefactored.js');
1406
+ await startInkCliRefactored(agent, cliSessionId, {
1407
+ updateInfo: cliUpdateInfo ?? undefined,
1408
+ configFilePath: resolvedPath,
1409
+ ...(initialPrompt && { initialPrompt }),
1410
+ });
1411
+ }
1412
+ catch (error) {
1413
+ inkError = error;
1414
+ }
1415
+ finally {
1416
+ // Restore console methods so any errors are visible
1417
+ console.log = originalConsole.log;
1418
+ console.error = originalConsole.error;
1419
+ console.warn = originalConsole.warn;
1420
+ console.info = originalConsole.info;
1421
+ }
1422
+ // Stop the agent after Ink CLI exits
1423
+ try {
1424
+ await agent.stop();
1425
+ }
1426
+ catch {
1427
+ // Ignore shutdown errors
1428
+ }
1429
+ // Handle any errors from Ink CLI
1430
+ if (inkError) {
1431
+ if (inkError instanceof ExitSignal)
1432
+ throw inkError;
1433
+ const errorMessage = inkError instanceof Error ? inkError.message : String(inkError);
1434
+ console.error(`❌ Ink CLI failed: ${errorMessage}`);
1435
+ if (inkError instanceof Error && inkError.stack) {
1436
+ console.error(inkError.stack);
1437
+ }
1438
+ safeExit('main', 1, 'ink-cli-error');
1439
+ }
1440
+ safeExit('main', 0);
1441
+ }
1442
+ // falls through - safeExit returns never, but eslint doesn't know that
1443
+ case 'web': {
1444
+ // Default to 3000 for web mode
1445
+ const defaultPort = opts.port ? parseInt(opts.port, 10) : 3000;
1446
+ const port = getPort(process.env.PORT, defaultPort, 'PORT');
1447
+ const serverUrl = process.env.DEXTO_URL ?? `http://localhost:${port}`;
1448
+ // Resolve webRoot path (embedded WebUI dist folder)
1449
+ const webRoot = resolveWebRoot();
1450
+ if (!webRoot) {
1451
+ console.warn(chalk.yellow('⚠️ WebUI not found in this build.'));
1452
+ console.info('For production: Run "pnpm build:all" to embed the WebUI');
1453
+ console.info('For development: Run "pnpm dev" for hot reload');
1454
+ }
1455
+ // Build WebUI runtime config (analytics, etc.) for injection into index.html
1456
+ const webUIConfig = webRoot
1457
+ ? { analytics: await getWebUIAnalyticsConfig() }
1458
+ : undefined;
1459
+ // Start single Hono server serving both API and WebUI
1460
+ await startHonoApiServer(agent, port, agent.config.agentCard || {}, derivedAgentId, resolvedPath, webRoot, webUIConfig);
1461
+ console.log(chalk.green(`✅ Server running at ${serverUrl}`));
1462
+ // Show update notification if available
1463
+ const webUpdateInfo = await versionCheckPromise;
1464
+ if (webUpdateInfo) {
1465
+ displayUpdateNotification(webUpdateInfo);
1466
+ }
1467
+ // Open WebUI in browser if webRoot is available
1468
+ if (webRoot) {
1469
+ try {
1470
+ const { default: open } = await import('open');
1471
+ await open(serverUrl, { wait: false });
1472
+ console.log(chalk.green(`🌐 Opened WebUI in browser: ${serverUrl}`));
1473
+ }
1474
+ catch (_error) {
1475
+ console.log(chalk.yellow(`💡 WebUI is available at: ${serverUrl}`));
1476
+ }
1477
+ }
1478
+ break;
1479
+ }
1480
+ // Start server with REST APIs and SSE on port 3001
1481
+ // This also enables dexto to be used as a remote mcp server at localhost:3001/mcp
1482
+ case 'server': {
1483
+ // Start server with REST APIs and SSE only
1484
+ const agentCard = agent.config.agentCard ?? {};
1485
+ // Default to 3001 for server mode
1486
+ const defaultPort = opts.port ? parseInt(opts.port, 10) : 3001;
1487
+ const apiPort = getPort(process.env.PORT, defaultPort, 'PORT');
1488
+ const apiUrl = process.env.DEXTO_URL ?? `http://localhost:${apiPort}`;
1489
+ console.log('🌐 Starting server (REST APIs + SSE)...');
1490
+ await startHonoApiServer(agent, apiPort, agentCard, derivedAgentId, resolvedPath);
1491
+ console.log(`✅ Server running at ${apiUrl}`);
1492
+ console.log('Available endpoints:');
1493
+ console.log(' POST /api/message - Send async message');
1494
+ console.log(' POST /api/message-sync - Send sync message');
1495
+ console.log(' POST /api/reset - Reset conversation');
1496
+ console.log(' GET /api/mcp/servers - List MCP servers');
1497
+ console.log(' SSE support available for real-time events');
1498
+ // Show update notification if available
1499
+ const serverUpdateInfo = await versionCheckPromise;
1500
+ if (serverUpdateInfo) {
1501
+ displayUpdateNotification(serverUpdateInfo);
1502
+ }
1503
+ break;
1504
+ }
1505
+ // TODO: Remove if server mode is stable and supports mcp
1506
+ // Starts dexto as a local mcp server
1507
+ // Use `dexto --mode mcp` to start dexto as a local mcp server
1508
+ // Use `dexto --mode server` to start dexto as a remote server
1509
+ case 'mcp': {
1510
+ // Start stdio mcp server only
1511
+ const agentCardConfig = agent.config.agentCard || {
1512
+ name: 'dexto',
1513
+ version: '1.0.0',
1514
+ };
1515
+ try {
1516
+ // Logs are already redirected to file by default to prevent interference with stdio transport
1517
+ const agentCardData = createAgentCard({
1518
+ defaultName: agentCardConfig.name ?? 'dexto',
1519
+ defaultVersion: agentCardConfig.version ?? '1.0.0',
1520
+ defaultBaseUrl: 'stdio://local-dexto',
1521
+ }, agentCardConfig // preserve overrides from agent file
1522
+ );
1523
+ // Use stdio transport in mcp mode
1524
+ const mcpTransport = await createMcpTransport('stdio');
1525
+ await initializeMcpServer(agent, agentCardData, mcpTransport);
1526
+ }
1527
+ catch (err) {
1528
+ // Write to stderr instead of stdout to avoid interfering with MCP protocol
1529
+ process.stderr.write(`MCP server startup failed: ${err}\n`);
1530
+ safeExit('main', 1, 'mcp-startup-failed');
1531
+ }
1532
+ break;
1533
+ }
1534
+ default:
1535
+ if (opts.mode === 'discord' || opts.mode === 'telegram') {
1536
+ console.error(`❌ Error: '${opts.mode}' mode has been moved to examples`);
1537
+ console.error('');
1538
+ console.error(`The ${opts.mode} bot is now a standalone example that you can customize.`);
1539
+ console.error('');
1540
+ console.error(`📖 See: examples/${opts.mode}-bot/README.md`);
1541
+ console.error('');
1542
+ console.error(`To run it:`);
1543
+ console.error(` cd examples/${opts.mode}-bot`);
1544
+ console.error(` pnpm install`);
1545
+ console.error(` pnpm start`);
1546
+ }
1547
+ else {
1548
+ console.error(`❌ Unknown mode '${opts.mode}'. Use web, cli, server, or mcp.`);
1549
+ }
1550
+ safeExit('main', 1, 'unknown-mode');
1551
+ }
1552
+ }, { timeoutMs: 0 }));
1553
+ // 17) PARSE & EXECUTE
1554
+ program.parseAsync(process.argv);