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