dexto 1.5.7 → 1.6.0

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