dexto 1.6.7 → 1.6.9

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 (104) hide show
  1. package/README.md +4 -4
  2. package/dist/analytics/wrapper.d.ts.map +1 -1
  3. package/dist/analytics/wrapper.js +43 -9
  4. package/dist/cli/auth/api-client.d.ts +50 -0
  5. package/dist/cli/auth/api-client.d.ts.map +1 -1
  6. package/dist/cli/auth/api-client.js +379 -15
  7. package/dist/cli/auth/browser-launch.d.ts +6 -0
  8. package/dist/cli/auth/browser-launch.d.ts.map +1 -0
  9. package/dist/cli/auth/browser-launch.js +24 -0
  10. package/dist/cli/auth/device.d.ts +14 -0
  11. package/dist/cli/auth/device.d.ts.map +1 -0
  12. package/dist/cli/auth/device.js +93 -0
  13. package/dist/cli/auth/index.d.ts +3 -1
  14. package/dist/cli/auth/index.d.ts.map +1 -1
  15. package/dist/cli/auth/index.js +2 -1
  16. package/dist/cli/auth/login-persistence.d.ts +13 -0
  17. package/dist/cli/auth/login-persistence.d.ts.map +1 -0
  18. package/dist/cli/auth/login-persistence.js +22 -0
  19. package/dist/cli/auth/oauth.d.ts +4 -1
  20. package/dist/cli/auth/oauth.d.ts.map +1 -1
  21. package/dist/cli/auth/oauth.js +6 -2
  22. package/dist/cli/auth/types.d.ts +12 -0
  23. package/dist/cli/auth/types.d.ts.map +1 -0
  24. package/dist/cli/auth/types.js +1 -0
  25. package/dist/cli/commands/agents/register.d.ts +6 -0
  26. package/dist/cli/commands/agents/register.d.ts.map +1 -0
  27. package/dist/cli/commands/agents/register.js +85 -0
  28. package/dist/cli/commands/auth/index.d.ts +1 -1
  29. package/dist/cli/commands/auth/index.d.ts.map +1 -1
  30. package/dist/cli/commands/auth/index.js +1 -1
  31. package/dist/cli/commands/auth/login.d.ts +6 -6
  32. package/dist/cli/commands/auth/login.d.ts.map +1 -1
  33. package/dist/cli/commands/auth/login.js +85 -115
  34. package/dist/cli/commands/auth/logout.d.ts.map +1 -1
  35. package/dist/cli/commands/auth/logout.js +32 -2
  36. package/dist/cli/commands/auth/register.d.ts +3 -0
  37. package/dist/cli/commands/auth/register.d.ts.map +1 -0
  38. package/dist/cli/commands/auth/register.js +94 -0
  39. package/dist/cli/commands/billing/register.d.ts +3 -0
  40. package/dist/cli/commands/billing/register.d.ts.map +1 -0
  41. package/dist/cli/commands/billing/register.js +20 -0
  42. package/dist/cli/commands/helpers/formatters.d.ts.map +1 -1
  43. package/dist/cli/commands/helpers/formatters.js +9 -0
  44. package/dist/cli/commands/image/register.d.ts +6 -0
  45. package/dist/cli/commands/image/register.d.ts.map +1 -0
  46. package/dist/cli/commands/image/register.js +144 -0
  47. package/dist/cli/commands/install.d.ts +2 -2
  48. package/dist/cli/commands/list-agents.d.ts.map +1 -1
  49. package/dist/cli/commands/list-agents.js +3 -3
  50. package/dist/cli/commands/mcp/register.d.ts +6 -0
  51. package/dist/cli/commands/mcp/register.d.ts.map +1 -0
  52. package/dist/cli/commands/mcp/register.js +64 -0
  53. package/dist/cli/commands/plugin/register.d.ts +6 -0
  54. package/dist/cli/commands/plugin/register.d.ts.map +1 -0
  55. package/dist/cli/commands/plugin/register.js +183 -0
  56. package/dist/cli/commands/plugin.d.ts +4 -4
  57. package/dist/cli/commands/register-context.d.ts +12 -0
  58. package/dist/cli/commands/register-context.d.ts.map +1 -0
  59. package/dist/cli/commands/register-context.js +1 -0
  60. package/dist/cli/commands/run/headless.d.ts +20 -0
  61. package/dist/cli/commands/run/headless.d.ts.map +1 -0
  62. package/dist/cli/commands/run/headless.js +275 -0
  63. package/dist/cli/commands/run/register.d.ts +3 -0
  64. package/dist/cli/commands/run/register.d.ts.map +1 -0
  65. package/dist/cli/commands/run/register.js +78 -0
  66. package/dist/cli/commands/search/register.d.ts +3 -0
  67. package/dist/cli/commands/search/register.d.ts.map +1 -0
  68. package/dist/cli/commands/search/register.js +55 -0
  69. package/dist/cli/commands/session/register.d.ts +3 -0
  70. package/dist/cli/commands/session/register.d.ts.map +1 -0
  71. package/dist/cli/commands/session/register.js +75 -0
  72. package/dist/cli/commands/setup.js +4 -4
  73. package/dist/cli/commands/sync-agents.d.ts +3 -3
  74. package/dist/cli/commands/sync-agents.js +4 -4
  75. package/dist/cli/commands/uninstall.d.ts +2 -2
  76. package/dist/cli/modes/cli.d.ts +3 -0
  77. package/dist/cli/modes/cli.d.ts.map +1 -0
  78. package/dist/cli/modes/cli.js +170 -0
  79. package/dist/cli/modes/context.d.ts +20 -0
  80. package/dist/cli/modes/context.d.ts.map +1 -0
  81. package/dist/cli/modes/context.js +1 -0
  82. package/dist/cli/modes/dispatch.d.ts +3 -0
  83. package/dist/cli/modes/dispatch.d.ts.map +1 -0
  84. package/dist/cli/modes/dispatch.js +52 -0
  85. package/dist/cli/modes/mcp.d.ts +3 -0
  86. package/dist/cli/modes/mcp.d.ts.map +1 -0
  87. package/dist/cli/modes/mcp.js +23 -0
  88. package/dist/cli/modes/server.d.ts +3 -0
  89. package/dist/cli/modes/server.d.ts.map +1 -0
  90. package/dist/cli/modes/server.js +36 -0
  91. package/dist/cli/modes/web.d.ts +3 -0
  92. package/dist/cli/modes/web.d.ts.map +1 -0
  93. package/dist/cli/modes/web.js +50 -0
  94. package/dist/cli/utils/setup-utils.js +1 -1
  95. package/dist/index-main.js +150 -991
  96. package/dist/utils/port-utils.d.ts +1 -1
  97. package/dist/utils/port-utils.d.ts.map +1 -1
  98. package/dist/utils/port-utils.js +7 -3
  99. package/dist/webui/assets/index-Bn9YuTdA.css +1 -0
  100. package/dist/webui/assets/index-CNiOYnOb.js +2059 -0
  101. package/dist/webui/index.html +2 -2
  102. package/package.json +12 -12
  103. package/dist/webui/assets/index-d6c-yJNn.js +0 -2059
  104. package/dist/webui/assets/index-yKdFLN1k.css +0 -1
@@ -4,9 +4,7 @@ import { fileURLToPath } from 'url';
4
4
  import { Command } from 'commander';
5
5
  import * as p from '@clack/prompts';
6
6
  import chalk from 'chalk';
7
- import { initAnalytics, capture, getWebUIAnalyticsConfig } from './analytics/index.js';
8
7
  import { withAnalytics, safeExit, ExitSignal } from './analytics/wrapper.js';
9
- import { createFileSessionLoggerFactory } from './utils/session-logger-factory.js';
10
8
  function readVersionFromPackageJson(packageJsonPath) {
11
9
  if (!existsSync(packageJsonPath)) {
12
10
  return undefined;
@@ -45,51 +43,63 @@ function resolveCliVersion() {
45
43
  const cliVersion = resolveCliVersion();
46
44
  // Set CLI version for Dexto Gateway usage tracking
47
45
  process.env.DEXTO_CLI_VERSION = cliVersion;
48
- // Populate DEXTO_API_KEY for Dexto gateway routing
49
- // Resolution order in getDextoApiKey():
50
- // 1. Explicit env var (CI, testing, account override)
51
- // 2. auth.json from `dexto login`
52
- import { isDextoAuthEnabled } from '@dexto/agent-management';
53
- if (isDextoAuthEnabled()) {
54
- const { getDextoApiKey } = await import('./cli/auth/index.js');
55
- const dextoApiKey = await getDextoApiKey();
56
- if (dextoApiKey) {
57
- process.env.DEXTO_API_KEY = dextoApiKey;
58
- }
59
- }
60
46
  import { logger, getProviderFromModel, getAllSupportedModels, startLlmRegistryAutoUpdate, DextoAgent, isPath, resolveApiKeyForProvider, getPrimaryApiKeyEnvVar, } from '@dexto/core';
61
47
  import { applyImageDefaults, cleanNullValues, AgentConfigSchema, loadImage, resolveServicesFromConfig, setImageImporter, toDextoAgentOptions, } from '@dexto/agent-config';
62
- import { getDextoPackageRoot, resolveAgentPath, loadAgentConfig, globalPreferencesExist, loadGlobalPreferences, resolveBundledScript, } from '@dexto/agent-management';
63
- import { startHonoApiServer } from './api/server-hono.js';
48
+ import { getDextoPackageRoot, resolveAgentPath, loadAgentConfig, globalPreferencesExist, loadGlobalPreferences, resolveBundledScript, enrichAgentConfig, isDextoAuthEnabled, } from '@dexto/agent-management';
64
49
  import { validateCliOptions, handleCliOptionsError } from './cli/utils/options.js';
65
50
  import { validateAgentConfig } from './cli/utils/config-validation.js';
66
51
  import { applyCLIOverrides, applyUserPreferences } from './config/cli-overrides.js';
67
- import { enrichAgentConfig } from '@dexto/agent-management';
68
- import { getPort } from './utils/port-utils.js';
69
- import { createDextoProject, createImage, getUserInputToInitDextoApp, initDexto, postInitDexto, } from './cli/commands/index.js';
70
- import { handleSetupCommand, handleInstallCommand, handleUninstallCommand, handleImageDoctorCommand, handleImageInstallCommand, handleImageListCommand, handleImageRemoveCommand, handleImageUseCommand, handleListAgentsCommand, handleWhichCommand, handleSyncAgentsCommand, shouldPromptForSync, handleLoginCommand, handleLogoutCommand, handleStatusCommand, handleBillingStatusCommand, handlePluginListCommand, handlePluginInstallCommand, handlePluginUninstallCommand, handlePluginValidateCommand,
71
- // Marketplace handlers
72
- handleMarketplaceAddCommand, handleMarketplaceRemoveCommand, handleMarketplaceUpdateCommand, handleMarketplaceListCommand, handleMarketplacePluginsCommand, handleMarketplaceInstallCommand, } from './cli/commands/index.js';
73
- import { handleSessionListCommand, handleSessionHistoryCommand, handleSessionDeleteCommand, handleSessionSearchCommand, } from './cli/commands/session-commands.js';
74
- import { requiresSetup } from './cli/utils/setup-utils.js';
75
- import { checkForFileInCurrentDirectory, FileNotFoundError } from './cli/utils/package-mgmt.js';
76
- import { checkForUpdates, displayUpdateNotification } from './cli/utils/version-check.js';
77
- import { resolveWebRoot } from './web.js';
78
- import { initializeMcpServer, createMcpTransport } from '@dexto/server';
79
- import { createAgentCard } from '@dexto/core';
80
- import { initializeMcpToolAggregationServer } from './api/mcp/tool-aggregation-handler.js';
81
- import { importImageModule } from './cli/utils/image-store.js';
52
+ import { registerRunCommand } from './cli/commands/run/register.js';
53
+ import { registerSessionCommand } from './cli/commands/session/register.js';
54
+ import { registerSearchCommand } from './cli/commands/search/register.js';
55
+ import { registerAuthCommand } from './cli/commands/auth/register.js';
56
+ import { registerBillingCommand } from './cli/commands/billing/register.js';
57
+ import { registerMcpCommand } from './cli/commands/mcp/register.js';
58
+ import { registerImageCommand } from './cli/commands/image/register.js';
59
+ import { registerPluginCommand } from './cli/commands/plugin/register.js';
60
+ import { registerAgentsCommand } from './cli/commands/agents/register.js';
82
61
  const program = new Command();
83
- // Resolve images via the Dexto image store when installed; fall back to host imports (pnpm-safe).
84
- setImageImporter((specifier) => importImageModule(specifier));
85
- // Initialize analytics early (no-op if disabled)
86
- await initAnalytics({ appVersion: cliVersion });
87
- // Start version check early (non-blocking)
88
- // We'll check the result later and display notification for interactive modes
89
- const versionCheckPromise = checkForUpdates(cliVersion);
90
- // Start self-updating LLM registry refresh (models.dev + OpenRouter mapping).
91
- // Uses a cached snapshot on disk and refreshes in the background.
92
- startLlmRegistryAutoUpdate();
62
+ let imageImporterConfigured = false;
63
+ let dextoApiKeyBootstrapped = false;
64
+ let versionCheckPromise = null;
65
+ let llmRegistryAutoUpdateStarted = false;
66
+ async function ensureImageImporterConfigured() {
67
+ if (imageImporterConfigured) {
68
+ return;
69
+ }
70
+ const { importImageModule } = await import('./cli/utils/image-store.js');
71
+ setImageImporter((specifier) => importImageModule(specifier));
72
+ imageImporterConfigured = true;
73
+ }
74
+ async function ensureDextoApiKeyBootstrap() {
75
+ if (dextoApiKeyBootstrapped) {
76
+ return;
77
+ }
78
+ if (!isDextoAuthEnabled()) {
79
+ dextoApiKeyBootstrapped = true;
80
+ return;
81
+ }
82
+ const { getDextoApiKey } = await import('./cli/auth/index.js');
83
+ const dextoApiKey = await getDextoApiKey();
84
+ if (dextoApiKey) {
85
+ process.env.DEXTO_API_KEY = dextoApiKey;
86
+ }
87
+ dextoApiKeyBootstrapped = true;
88
+ }
89
+ async function getVersionCheckResult() {
90
+ if (!versionCheckPromise) {
91
+ const { checkForUpdates } = await import('./cli/utils/version-check.js');
92
+ versionCheckPromise = checkForUpdates(cliVersion);
93
+ }
94
+ return versionCheckPromise;
95
+ }
96
+ function ensureLlmRegistryAutoUpdateStarted() {
97
+ if (llmRegistryAutoUpdateStarted) {
98
+ return;
99
+ }
100
+ startLlmRegistryAutoUpdate();
101
+ llmRegistryAutoUpdateStarted = true;
102
+ }
93
103
  // 1) GLOBAL OPTIONS
94
104
  program
95
105
  .name('dexto')
@@ -123,6 +133,7 @@ program
123
133
  try {
124
134
  p.intro(chalk.inverse('Create Dexto App'));
125
135
  // Create the app project structure (fully self-contained)
136
+ const { createDextoProject } = await import('./cli/commands/create-app.js');
126
137
  await createDextoProject(name, options);
127
138
  p.outro(chalk.greenBright('Dexto app created successfully!'));
128
139
  safeExit('create-app', 0);
@@ -134,151 +145,23 @@ program
134
145
  safeExit('create-app', 1, 'error');
135
146
  }
136
147
  }));
137
- // 3) `create-image` SUB-COMMAND (hidden alias for `dexto image create`)
138
- program
139
- .command('create-image [name]', { hidden: true })
140
- .description('Alias for `dexto image create`')
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
- // 3b) `image` SUB-COMMAND
157
- const imageCommand = program.command('image').description('Manage images');
158
- imageCommand.addHelpText('after', `
159
- Examples:
160
- $ dexto image create my-image
161
- $ dexto image install @dexto/image-local
162
- $ dexto image install @myorg/my-image@1.2.3
163
- $ dexto image list
164
- $ dexto image use @myorg/my-image@1.2.3
165
- $ dexto image remove @myorg/my-image@1.2.3
166
- $ dexto image doctor
167
- `);
168
- imageCommand
169
- .command('create [name]')
170
- .description('Create a Dexto image project (scaffold)')
171
- .action(withAnalytics('image create', async (name) => {
172
- try {
173
- p.intro(chalk.inverse('Create Dexto Image'));
174
- // Create the image project structure
175
- const projectPath = await createImage(name);
176
- p.outro(chalk.greenBright(`Dexto image created successfully at ${projectPath}!`));
177
- safeExit('image create', 0);
178
- }
179
- catch (err) {
180
- if (err instanceof ExitSignal)
181
- throw err;
182
- console.error(`❌ dexto image create command failed: ${err}`);
183
- safeExit('image create', 1, 'error');
184
- }
185
- }));
186
- imageCommand
187
- .command('install <image>')
188
- .description('Install an image into the local Dexto image store')
189
- .option('--force', 'Force reinstall if already installed')
190
- .option('--no-activate', 'Do not set as the active version')
191
- .addHelpText('after', `
192
- Examples:
193
- $ dexto image install @dexto/image-local
194
- $ dexto image install @myorg/my-image@1.2.3
195
- $ dexto image install ./my-image-1.0.0.tgz
196
- `)
197
- .action(withAnalytics('image install', async (image, options) => {
198
- try {
199
- await handleImageInstallCommand({ ...options, image });
200
- safeExit('image install', 0);
201
- }
202
- catch (err) {
203
- if (err instanceof ExitSignal)
204
- throw err;
205
- console.error(`❌ dexto image install command failed: ${err}`);
206
- safeExit('image install', 1, 'error');
207
- }
208
- }));
209
- imageCommand
210
- .command('list')
211
- .description('List installed images')
212
- .action(withAnalytics('image list', async () => {
213
- try {
214
- await handleImageListCommand();
215
- safeExit('image list', 0);
216
- }
217
- catch (err) {
218
- if (err instanceof ExitSignal)
219
- throw err;
220
- console.error(`❌ dexto image list command failed: ${err}`);
221
- safeExit('image list', 1, 'error');
222
- }
223
- }));
224
- imageCommand
225
- .command('use <image>')
226
- .description('Set the active version for an installed image (image@version)')
227
- .action(withAnalytics('image use', async (image) => {
228
- try {
229
- await handleImageUseCommand({ image });
230
- safeExit('image use', 0);
231
- }
232
- catch (err) {
233
- if (err instanceof ExitSignal)
234
- throw err;
235
- console.error(`❌ dexto image use command failed: ${err}`);
236
- safeExit('image use', 1, 'error');
237
- }
238
- }));
239
- imageCommand
240
- .command('remove <image>')
241
- .description('Remove an image from the store (image or image@version)')
242
- .action(withAnalytics('image remove', async (image) => {
243
- try {
244
- await handleImageRemoveCommand({ image });
245
- safeExit('image remove', 0);
246
- }
247
- catch (err) {
248
- if (err instanceof ExitSignal)
249
- throw err;
250
- console.error(`❌ dexto image remove command failed: ${err}`);
251
- safeExit('image remove', 1, 'error');
252
- }
253
- }));
254
- imageCommand
255
- .command('doctor')
256
- .description('Print image store diagnostics')
257
- .action(withAnalytics('image doctor', async () => {
258
- try {
259
- await handleImageDoctorCommand();
260
- safeExit('image doctor', 0);
261
- }
262
- catch (err) {
263
- if (err instanceof ExitSignal)
264
- throw err;
265
- console.error(`❌ dexto image doctor command failed: ${err}`);
266
- safeExit('image doctor', 1, 'error');
267
- }
268
- }));
148
+ registerImageCommand({ program });
269
149
  // 4) `init-app` SUB-COMMAND
270
150
  program
271
151
  .command('init-app')
272
152
  .description('Initialize an existing Typescript app with Dexto')
273
153
  .action(withAnalytics('init-app', async () => {
154
+ const { checkForFileInCurrentDirectory, FileNotFoundError } = await import('./cli/utils/package-mgmt.js');
274
155
  try {
275
156
  // pre-condition: check that package.json and tsconfig.json exist in current directory to know that project is valid
276
157
  await checkForFileInCurrentDirectory('package.json');
277
158
  await checkForFileInCurrentDirectory('tsconfig.json');
278
159
  // start intro
279
160
  p.intro(chalk.inverse('Dexto Init App'));
161
+ const { getUserInputToInitDextoApp, initDexto, postInitDexto } = await import('./cli/commands/init-app.js');
280
162
  const userInput = await getUserInputToInitDextoApp();
281
163
  try {
164
+ const { capture } = await import('./analytics/index.js');
282
165
  capture('dexto_init', {
283
166
  provider: userInput.llmProvider,
284
167
  providedKey: Boolean(userInput.llmApiKey),
@@ -316,6 +199,7 @@ program
316
199
  .option('--force', 'Overwrite existing setup without confirmation')
317
200
  .action(withAnalytics('setup', async (options) => {
318
201
  try {
202
+ const { handleSetupCommand } = await import('./cli/commands/setup.js');
319
203
  await handleSetupCommand(options);
320
204
  safeExit('setup', 0);
321
205
  }
@@ -326,75 +210,14 @@ program
326
210
  safeExit('setup', 1, 'error');
327
211
  }
328
212
  }));
329
- // 6) `install` SUB-COMMAND
330
- program
331
- .command('install [agents...]')
332
- .description('Install agents from registry or custom YAML files/directories')
333
- .option('--all', 'Install all available agents from registry')
334
- .option('--no-inject-preferences', 'Skip injecting global preferences into installed agents')
335
- .option('--force', 'Force reinstall even if agent is already installed')
336
- .addHelpText('after', `
337
- Examples:
338
- $ dexto install coding-agent Install agent from registry
339
- $ dexto install agent1 agent2 Install multiple registry agents
340
- $ dexto install --all Install all available registry agents
341
- $ dexto install ./my-agent.yml Install custom agent from YAML file
342
- $ dexto install ./my-agent-dir/ Install custom agent from directory (interactive)`)
343
- .action(withAnalytics('install', async (agents = [], options) => {
344
- try {
345
- await handleInstallCommand(agents, options);
346
- safeExit('install', 0);
347
- }
348
- catch (err) {
349
- if (err instanceof ExitSignal)
350
- throw err;
351
- console.error(`❌ dexto install command failed: ${err}`);
352
- safeExit('install', 1, 'error');
353
- }
354
- }));
355
- // 7) `uninstall` SUB-COMMAND
356
- program
357
- .command('uninstall [agents...]')
358
- .description('Uninstall agents from the local installation')
359
- .option('--all', 'Uninstall all installed agents')
360
- .option('--force', 'Force uninstall even if agent is protected (e.g., coding-agent)')
361
- .action(withAnalytics('uninstall', async (agents, options) => {
362
- try {
363
- await handleUninstallCommand(agents, options);
364
- safeExit('uninstall', 0);
365
- }
366
- catch (err) {
367
- if (err instanceof ExitSignal)
368
- throw err;
369
- console.error(`❌ dexto uninstall command failed: ${err}`);
370
- safeExit('uninstall', 1, 'error');
371
- }
372
- }));
373
- // 8) `list-agents` SUB-COMMAND
374
- program
375
- .command('list-agents')
376
- .description('List available and installed agents')
377
- .option('--verbose', 'Show detailed agent information')
378
- .option('--installed', 'Show only installed agents')
379
- .option('--available', 'Show only available agents')
380
- .action(withAnalytics('list-agents', async (options) => {
381
- try {
382
- await handleListAgentsCommand(options);
383
- safeExit('list-agents', 0);
384
- }
385
- catch (err) {
386
- if (err instanceof ExitSignal)
387
- throw err;
388
- console.error(`❌ dexto list-agents command failed: ${err}`);
389
- safeExit('list-agents', 1, 'error');
390
- }
391
- }));
392
- // 9) `which` SUB-COMMAND
213
+ registerAgentsCommand({ program });
214
+ // 7) `which` SUB-COMMAND
393
215
  program
394
216
  .command('which <agent>')
395
217
  .description('Show the path to an agent')
396
218
  .action(withAnalytics('which', async (agent) => {
397
219
  try {
220
+ const { handleWhichCommand } = await import('./cli/commands/which.js');
398
221
  await handleWhichCommand(agent);
399
222
  safeExit('which', 0);
400
223
  }
@@ -405,200 +228,44 @@ program
405
228
  safeExit('which', 1, 'error');
406
229
  }
407
230
  }));
408
- // 10) `sync-agents` SUB-COMMAND
409
- program
410
- .command('sync-agents')
411
- .description('Sync installed agents with bundled versions')
412
- .option('--list', 'List agent status without updating')
413
- .option('--force', 'Update all agents without prompting')
414
- .action(withAnalytics('sync-agents', async (options) => {
415
- try {
416
- await handleSyncAgentsCommand(options);
417
- safeExit('sync-agents', 0);
418
- }
419
- catch (err) {
420
- if (err instanceof ExitSignal)
421
- throw err;
422
- console.error(`❌ dexto sync-agents command failed: ${err}`);
423
- safeExit('sync-agents', 1, 'error');
424
- }
425
- }));
426
- // 11) `plugin` SUB-COMMAND
427
- const pluginCommand = program.command('plugin').description('Manage plugins');
428
- pluginCommand
429
- .command('list')
430
- .description('List installed plugins')
431
- .option('--verbose', 'Show detailed plugin information')
432
- .action(withAnalytics('plugin list', async (options) => {
433
- try {
434
- await handlePluginListCommand(options);
435
- safeExit('plugin list', 0);
436
- }
437
- catch (err) {
438
- if (err instanceof ExitSignal)
439
- throw err;
440
- console.error(`❌ dexto plugin list command failed: ${err}`);
441
- safeExit('plugin list', 1, 'error');
442
- }
443
- }));
444
- pluginCommand
445
- .command('install')
446
- .description('Install a plugin from a local directory')
447
- .requiredOption('--path <path>', 'Path to the plugin directory')
448
- .option('--scope <scope>', 'Installation scope: user, project, or local', 'user')
449
- .option('--force', 'Force overwrite if already installed')
450
- .action(withAnalytics('plugin install', async (options) => {
451
- try {
452
- await handlePluginInstallCommand(options);
453
- safeExit('plugin install', 0);
454
- }
455
- catch (err) {
456
- if (err instanceof ExitSignal)
457
- throw err;
458
- console.error(`❌ dexto plugin install command failed: ${err}`);
459
- safeExit('plugin install', 1, 'error');
460
- }
461
- }));
462
- pluginCommand
463
- .command('uninstall <name>')
464
- .description('Uninstall a plugin by name')
465
- .action(withAnalytics('plugin uninstall', async (name) => {
466
- try {
467
- await handlePluginUninstallCommand({ name });
468
- safeExit('plugin uninstall', 0);
469
- }
470
- catch (err) {
471
- if (err instanceof ExitSignal)
472
- throw err;
473
- console.error(`❌ dexto plugin uninstall command failed: ${err}`);
474
- safeExit('plugin uninstall', 1, 'error');
475
- }
476
- }));
477
- pluginCommand
478
- .command('validate [path]')
479
- .description('Validate a plugin directory structure')
480
- .action(withAnalytics('plugin validate', async (path) => {
481
- try {
482
- await handlePluginValidateCommand({ path: path || '.' });
483
- safeExit('plugin validate', 0);
484
- }
485
- catch (err) {
486
- if (err instanceof ExitSignal)
487
- throw err;
488
- console.error(`❌ dexto plugin validate command failed: ${err}`);
489
- safeExit('plugin validate', 1, 'error');
490
- }
491
- }));
492
- // 12) `plugin marketplace` SUB-COMMANDS
493
- const marketplaceCommand = pluginCommand
494
- .command('marketplace')
495
- .alias('market')
496
- .description('Manage plugin marketplaces');
497
- marketplaceCommand
498
- .command('add <source>')
499
- .description('Add a marketplace (GitHub: owner/repo, git URL, or local path)')
500
- .option('--name <name>', 'Custom name for the marketplace')
501
- .action(withAnalytics('plugin marketplace add', async (source, options) => {
502
- try {
503
- await handleMarketplaceAddCommand({ source, name: options.name });
504
- safeExit('plugin marketplace add', 0);
505
- }
506
- catch (err) {
507
- if (err instanceof ExitSignal)
508
- throw err;
509
- console.error(`❌ dexto plugin marketplace add command failed: ${err}`);
510
- safeExit('plugin marketplace add', 1, 'error');
511
- }
512
- }));
513
- marketplaceCommand
514
- .command('list')
515
- .description('List registered marketplaces')
516
- .option('--verbose', 'Show detailed marketplace information')
517
- .action(withAnalytics('plugin marketplace list', async (options) => {
518
- try {
519
- await handleMarketplaceListCommand(options);
520
- safeExit('plugin marketplace list', 0);
521
- }
522
- catch (err) {
523
- if (err instanceof ExitSignal)
524
- throw err;
525
- console.error(`❌ dexto plugin marketplace list command failed: ${err}`);
526
- safeExit('plugin marketplace list', 1, 'error');
527
- }
528
- }));
529
- marketplaceCommand
530
- .command('remove <name>')
531
- .alias('rm')
532
- .description('Remove a registered marketplace')
533
- .action(withAnalytics('plugin marketplace remove', async (name) => {
534
- try {
535
- await handleMarketplaceRemoveCommand({ name });
536
- safeExit('plugin marketplace remove', 0);
537
- }
538
- catch (err) {
539
- if (err instanceof ExitSignal)
540
- throw err;
541
- console.error(`❌ dexto plugin marketplace remove command failed: ${err}`);
542
- safeExit('plugin marketplace remove', 1, 'error');
543
- }
544
- }));
545
- marketplaceCommand
546
- .command('update [name]')
547
- .description('Update marketplace(s) from remote (git pull)')
548
- .action(withAnalytics('plugin marketplace update', async (name) => {
549
- try {
550
- await handleMarketplaceUpdateCommand({ name });
551
- safeExit('plugin marketplace update', 0);
552
- }
553
- catch (err) {
554
- if (err instanceof ExitSignal)
555
- throw err;
556
- console.error(`❌ dexto plugin marketplace update command failed: ${err}`);
557
- safeExit('plugin marketplace update', 1, 'error');
558
- }
559
- }));
560
- marketplaceCommand
561
- .command('plugins [marketplace]')
562
- .description('List plugins available in marketplaces')
563
- .option('--verbose', 'Show plugin descriptions')
564
- .action(withAnalytics('plugin marketplace plugins', async (marketplace, options) => {
565
- try {
566
- await handleMarketplacePluginsCommand({
567
- marketplace,
568
- verbose: options?.verbose,
569
- });
570
- safeExit('plugin marketplace plugins', 0);
571
- }
572
- catch (err) {
573
- if (err instanceof ExitSignal)
574
- throw err;
575
- console.error(`❌ dexto plugin marketplace plugins command failed: ${err}`);
576
- safeExit('plugin marketplace plugins', 1, 'error');
577
- }
578
- }));
579
- marketplaceCommand
580
- .command('install <plugin>')
581
- .description('Install a plugin from marketplace (plugin or plugin@marketplace)')
582
- .option('--scope <scope>', 'Installation scope: user, project, or local', 'user')
583
- .option('--force', 'Force reinstall if already exists')
584
- .action(withAnalytics('plugin marketplace install', async (plugin, options) => {
585
- try {
586
- await handleMarketplaceInstallCommand({ ...options, plugin });
587
- safeExit('plugin marketplace install', 0);
588
- }
589
- catch (err) {
590
- if (err instanceof ExitSignal)
591
- throw err;
592
- console.error(`❌ dexto plugin marketplace install command failed: ${err}`);
593
- safeExit('plugin marketplace install', 1, 'error');
594
- }
595
- }));
596
- // Helper to bootstrap a minimal agent for non-interactive session/search ops
597
- async function bootstrapAgentFromGlobalOpts() {
231
+ registerPluginCommand({ program });
232
+ // Helper to bootstrap a minimal agent for non-interactive commands
233
+ async function bootstrapAgentFromGlobalOpts(options) {
234
+ const { mode, modelOverride } = options;
235
+ const isHeadlessRun = mode === 'headless-run';
236
+ await ensureDextoApiKeyBootstrap();
237
+ await ensureImageImporterConfigured();
598
238
  const globalOpts = program.opts();
239
+ const effectiveModel = modelOverride ?? globalOpts.model;
240
+ let inferredProvider;
241
+ let inferredApiKey;
242
+ // Non-interactive subcommands bypass the main mode action, so replicate
243
+ // model -> provider/apiKey inference here.
244
+ if (effectiveModel) {
245
+ if (effectiveModel.includes('/')) {
246
+ throw new Error(`Model '${effectiveModel}' looks like an OpenRouter-format ID (provider/model). Please set provider/model explicitly in agent config for this command.`);
247
+ }
248
+ inferredProvider = getProviderFromModel(effectiveModel);
249
+ const apiKey = resolveApiKeyForProvider(inferredProvider);
250
+ if (!apiKey) {
251
+ const envVar = getPrimaryApiKeyEnvVar(inferredProvider);
252
+ throw new Error(`Missing API key for provider '${inferredProvider}' - please set $${envVar}`);
253
+ }
254
+ inferredApiKey = apiKey;
255
+ }
599
256
  const resolvedPath = await resolveAgentPath(globalOpts.agent, globalOpts.autoInstall !== false);
600
257
  const rawConfig = await loadAgentConfig(resolvedPath);
601
- const mergedConfig = applyCLIOverrides(rawConfig, globalOpts);
258
+ const mergedConfig = applyCLIOverrides(rawConfig, {
259
+ ...globalOpts,
260
+ ...(modelOverride ? { model: modelOverride } : {}),
261
+ });
262
+ if (effectiveModel) {
263
+ mergedConfig.llm.model = effectiveModel;
264
+ }
265
+ if (inferredProvider && inferredApiKey) {
266
+ mergedConfig.llm.provider = inferredProvider;
267
+ mergedConfig.llm.apiKey = inferredApiKey;
268
+ }
602
269
  // Load image first to apply defaults and resolve DI services
603
270
  // Priority: CLI flag > Agent config > Environment variable > Default
604
271
  const imageName = globalOpts.image || // --image flag
@@ -620,10 +287,21 @@ async function bootstrapAgentFromGlobalOpts() {
620
287
  const configWithImageDefaults = applyImageDefaults(mergedConfig, image.defaults);
621
288
  // Enrich config with per-agent paths BEFORE validation
622
289
  const enrichedConfig = enrichAgentConfig(configWithImageDefaults, resolvedPath, {
623
- logLevel: 'info', // CLI uses info-level logging for visibility
290
+ // Headless run keeps output deterministic and noise-free.
291
+ // Other non-interactive commands keep visible logs.
292
+ logLevel: isHeadlessRun ? 'error' : 'info',
624
293
  });
625
- // Override approval config for read-only commands (never run conversations)
626
- // This avoids needing to set up unused approval handlers
294
+ if (isHeadlessRun) {
295
+ // Force silent transport in headless mode even when agent config defines logger settings.
296
+ // `dexto run` owns stderr formatting and should be the only writer.
297
+ enrichedConfig.logger = {
298
+ level: 'error',
299
+ transports: [{ type: 'silent' }],
300
+ };
301
+ }
302
+ // Override approval config for non-interactive commands.
303
+ // Headless operations default to auto-approve and disable elicitation to
304
+ // avoid waiting for interactive approval handlers.
627
305
  enrichedConfig.permissions = {
628
306
  ...(enrichedConfig.permissions ?? {}),
629
307
  mode: 'auto-approve',
@@ -651,285 +329,27 @@ async function bootstrapAgentFromGlobalOpts() {
651
329
  process.on('SIGTERM', shutdown);
652
330
  return agent;
653
331
  }
654
- // Helper to find the most recent session
655
- async function getMostRecentSessionId(agent) {
656
- const sessionIds = await agent.listSessions();
657
- if (sessionIds.length === 0) {
658
- return null;
659
- }
660
- // Get metadata for all sessions to find most recent
661
- let mostRecentId = null;
662
- let mostRecentActivity = 0;
663
- for (const sessionId of sessionIds) {
664
- const metadata = await agent.getSessionMetadata(sessionId);
665
- if (metadata && metadata.lastActivity > mostRecentActivity) {
666
- mostRecentActivity = metadata.lastActivity;
667
- mostRecentId = sessionId;
668
- }
669
- }
670
- return mostRecentId;
671
- }
672
- // 11) `session` SUB-COMMAND
673
- const sessionCommand = program.command('session').description('Manage chat sessions');
674
- sessionCommand
675
- .command('list')
676
- .description('List all sessions')
677
- .action(withAnalytics('session list', async () => {
678
- try {
679
- const agent = await bootstrapAgentFromGlobalOpts();
680
- await handleSessionListCommand(agent);
681
- await agent.stop();
682
- safeExit('session list', 0);
683
- }
684
- catch (err) {
685
- if (err instanceof ExitSignal)
686
- throw err;
687
- console.error(`❌ dexto session list command failed: ${err}`);
688
- safeExit('session list', 1, 'error');
689
- }
690
- }));
691
- sessionCommand
692
- .command('history')
693
- .description('Show session history')
694
- .argument('[sessionId]', 'Session ID (defaults to current session)')
695
- .action(withAnalytics('session history', async (sessionId) => {
696
- try {
697
- const agent = await bootstrapAgentFromGlobalOpts();
698
- await handleSessionHistoryCommand(agent, sessionId);
699
- await agent.stop();
700
- safeExit('session history', 0);
701
- }
702
- catch (err) {
703
- if (err instanceof ExitSignal)
704
- throw err;
705
- console.error(`❌ dexto session history command failed: ${err}`);
706
- safeExit('session history', 1, 'error');
707
- }
708
- }));
709
- sessionCommand
710
- .command('delete')
711
- .description('Delete a session')
712
- .argument('<sessionId>', 'Session ID to delete')
713
- .action(withAnalytics('session delete', async (sessionId) => {
714
- try {
715
- const agent = await bootstrapAgentFromGlobalOpts();
716
- await handleSessionDeleteCommand(agent, sessionId);
717
- await agent.stop();
718
- safeExit('session delete', 0);
719
- }
720
- catch (err) {
721
- if (err instanceof ExitSignal)
722
- throw err;
723
- console.error(`❌ dexto session delete command failed: ${err}`);
724
- safeExit('session delete', 1, 'error');
725
- }
726
- }));
727
- // 12) `search` SUB-COMMAND
728
- program
729
- .command('search')
730
- .description('Search session history')
731
- .argument('<query>', 'Search query')
732
- .option('--session <sessionId>', 'Search in specific session')
733
- .option('--role <role>', 'Filter by role (user, assistant, system, tool)')
734
- .option('--limit <number>', 'Limit number of results', '10')
735
- .action(withAnalytics('search', async (query, options) => {
736
- try {
737
- const agent = await bootstrapAgentFromGlobalOpts();
738
- const searchOptions = {};
739
- if (options.session) {
740
- searchOptions.sessionId = options.session;
741
- }
742
- if (options.role) {
743
- const allowed = new Set(['user', 'assistant', 'system', 'tool']);
744
- if (!allowed.has(options.role)) {
745
- console.error(`❌ Invalid role: ${options.role}. Use one of: user, assistant, system, tool`);
746
- safeExit('search', 1, 'invalid-role');
747
- }
748
- searchOptions.role = options.role;
749
- }
750
- if (options.limit) {
751
- const parsed = parseInt(options.limit, 10);
752
- if (Number.isNaN(parsed) || parsed <= 0) {
753
- console.error(`❌ Invalid --limit: ${options.limit}. Use a positive integer (e.g., 10).`);
754
- safeExit('search', 1, 'invalid-limit');
755
- }
756
- searchOptions.limit = parsed;
757
- }
758
- await handleSessionSearchCommand(agent, query, searchOptions);
759
- await agent.stop();
760
- safeExit('search', 0);
761
- }
762
- catch (err) {
763
- if (err instanceof ExitSignal)
764
- throw err;
765
- console.error(`❌ dexto search command failed: ${err}`);
766
- safeExit('search', 1, 'error');
767
- }
768
- }));
769
- // 13) `auth` SUB-COMMAND GROUP
770
- const authCommand = program.command('auth').description('Manage authentication');
771
- authCommand
772
- .command('login')
773
- .description('Login to Dexto')
774
- .option('--api-key <key>', 'Use Dexto API key instead of browser login')
775
- .option('--no-interactive', 'Disable interactive prompts')
776
- .action(withAnalytics('auth login', async (options) => {
777
- try {
778
- await handleLoginCommand(options);
779
- safeExit('auth login', 0);
780
- }
781
- catch (err) {
782
- if (err instanceof ExitSignal)
783
- throw err;
784
- console.error(`❌ dexto auth login command failed: ${err}`);
785
- safeExit('auth login', 1, 'error');
786
- }
787
- }));
788
- authCommand
789
- .command('logout')
790
- .description('Logout from Dexto')
791
- .option('--force', 'Skip confirmation prompt')
792
- .option('--no-interactive', 'Disable interactive prompts')
793
- .action(withAnalytics('auth logout', async (options) => {
794
- try {
795
- await handleLogoutCommand(options);
796
- safeExit('auth logout', 0);
797
- }
798
- catch (err) {
799
- if (err instanceof ExitSignal)
800
- throw err;
801
- console.error(`❌ dexto auth logout command failed: ${err}`);
802
- safeExit('auth logout', 1, 'error');
803
- }
804
- }));
805
- authCommand
806
- .command('status')
807
- .description('Show authentication status')
808
- .action(withAnalytics('auth status', async () => {
809
- try {
810
- await handleStatusCommand();
811
- safeExit('auth status', 0);
812
- }
813
- catch (err) {
814
- if (err instanceof ExitSignal)
815
- throw err;
816
- console.error(`❌ dexto auth status command failed: ${err}`);
817
- safeExit('auth status', 1, 'error');
818
- }
819
- }));
820
- // Also add convenience aliases at root level
821
- program
822
- .command('login')
823
- .description('Login to Dexto (alias for `dexto auth login`)')
824
- .option('--api-key <key>', 'Use Dexto API key instead of browser login')
825
- .option('--no-interactive', 'Disable interactive prompts')
826
- .action(withAnalytics('login', async (options) => {
827
- try {
828
- await handleLoginCommand(options);
829
- safeExit('login', 0);
830
- }
831
- catch (err) {
832
- if (err instanceof ExitSignal)
833
- throw err;
834
- console.error(`❌ dexto login command failed: ${err}`);
835
- safeExit('login', 1, 'error');
836
- }
837
- }));
838
- program
839
- .command('logout')
840
- .description('Logout from Dexto (alias for `dexto auth logout`)')
841
- .option('--force', 'Skip confirmation prompt')
842
- .option('--no-interactive', 'Disable interactive prompts')
843
- .action(withAnalytics('logout', async (options) => {
844
- try {
845
- await handleLogoutCommand(options);
846
- safeExit('logout', 0);
847
- }
848
- catch (err) {
849
- if (err instanceof ExitSignal)
850
- throw err;
851
- console.error(`❌ dexto logout command failed: ${err}`);
852
- safeExit('logout', 1, 'error');
853
- }
854
- }));
855
- // 14) `billing` COMMAND
856
- program
857
- .command('billing')
858
- .description('Show billing status and credit balance')
859
- .option('--buy', 'Open Dexto Nova credits purchase page')
860
- .action(withAnalytics('billing', async (options) => {
861
- try {
862
- await handleBillingStatusCommand(options);
863
- safeExit('billing', 0);
864
- }
865
- catch (err) {
866
- if (err instanceof ExitSignal)
867
- throw err;
868
- console.error(`❌ dexto billing command failed: ${err}`);
869
- safeExit('billing', 1, 'error');
870
- }
871
- }));
872
- // 15) `mcp` SUB-COMMAND
873
- // For now, this mode simply aggregates and re-expose tools from configured MCP servers (no agent)
874
- // dexto --mode mcp will be moved to this sub-command in the future
875
- program
876
- .command('mcp')
877
- .description('Start Dexto as an MCP server. Use --group-servers to aggregate and re-expose tools from configured MCP servers. \
878
- In the future, this command will expose the agent as an MCP server by default.')
879
- .option('-s, --strict', 'Require all MCP server connections to succeed')
880
- .option('--group-servers', 'Aggregate and re-expose tools from configured MCP servers (required for now)')
881
- .option('--name <n>', 'Name for the MCP server', 'dexto-tools')
882
- .option('--version <version>', 'Version for the MCP server', '1.0.0')
883
- .action(withAnalytics('mcp', async (options) => {
884
- try {
885
- // Validate that --group-servers flag is provided (mandatory for now)
886
- if (!options.groupServers) {
887
- console.error('❌ The --group-servers flag is required. This command currently only supports aggregating and re-exposing tools from configured MCP servers.');
888
- console.error('Usage: dexto mcp --group-servers');
889
- safeExit('mcp', 1, 'missing-group-servers');
890
- }
891
- // Load and resolve config
892
- // Get the global agent option from the main program
893
- const globalOpts = program.opts();
894
- const nameOrPath = globalOpts.agent;
895
- const configPath = await resolveAgentPath(nameOrPath, globalOpts.autoInstall !== false);
896
- console.log(`📄 Loading Dexto config from: ${configPath}`);
897
- const config = await loadAgentConfig(configPath);
898
- logger.info(`Validating MCP servers...`);
899
- // Validate that MCP servers are configured
900
- if (!config.mcpServers || Object.keys(config.mcpServers).length === 0) {
901
- console.error('❌ No MCP servers configured. Please configure mcpServers in your config file.');
902
- safeExit('mcp', 1, 'no-mcp-servers');
903
- }
904
- const { ServersConfigSchema } = await import('@dexto/core');
905
- const validatedServers = ServersConfigSchema.parse(config.mcpServers);
906
- logger.info(`Validated MCP servers. Configured servers: ${Object.keys(validatedServers).join(', ')}`);
907
- // Logs are already redirected to file by default to prevent interference with stdio transport
908
- const currentLogPath = logger.getLogFilePath();
909
- logger.info(`MCP mode using log file: ${currentLogPath || 'default .dexto location'}`);
910
- logger.info(`Starting MCP tool aggregation server: ${options.name} v${options.version}`);
911
- // Create stdio transport for MCP tool aggregation
912
- const mcpTransport = await createMcpTransport('stdio');
913
- // Initialize tool aggregation server
914
- await initializeMcpToolAggregationServer(validatedServers, mcpTransport, options.name, options.version, options.strict);
915
- logger.info('MCP tool aggregation server started successfully');
916
- }
917
- catch (err) {
918
- if (err instanceof ExitSignal)
919
- throw err;
920
- // Write to stderr to avoid interfering with MCP protocol
921
- process.stderr.write(`MCP tool aggregation server startup failed: ${err}\n`);
922
- safeExit('mcp', 1, 'mcp-agg-failed');
923
- }
924
- }, { timeoutMs: 0 }));
925
- // 16) Main dexto CLI - Interactive/One shot (CLI/HEADLESS) or run in other modes (--mode web/server/mcp)
332
+ // 11) Runtime commands (`run`, `session`, `search`, `auth`, `billing`)
333
+ const runtimeCommandContext = {
334
+ program,
335
+ cliVersion,
336
+ bootstrapAgentFromGlobalOpts,
337
+ };
338
+ registerRunCommand(runtimeCommandContext);
339
+ registerSessionCommand(runtimeCommandContext);
340
+ registerSearchCommand(runtimeCommandContext);
341
+ registerAuthCommand(runtimeCommandContext);
342
+ registerBillingCommand(runtimeCommandContext);
343
+ registerMcpCommand({ program });
344
+ // 13) Main dexto CLI - Interactive (CLI) or run in other modes (--mode web/server/mcp)
926
345
  program
927
346
  // Main customer facing description
928
347
  .description('Dexto CLI - AI-powered assistant with session management.\n\n' +
929
348
  'Basic Usage:\n' +
930
349
  ' dexto Start web UI (default)\n' +
931
350
  ' dexto --mode cli Start interactive CLI\n' +
932
- ' dexto --prompt "query" Start interactive CLI and run the prompt\n\n' +
351
+ ' dexto --prompt "query" Start interactive CLI and run the prompt\n' +
352
+ ' dexto run "query" Run one-off headless task\n\n' +
933
353
  'Session Management Commands:\n' +
934
354
  ' dexto session list List all sessions\n' +
935
355
  ' dexto session history [id] Show session history\n' +
@@ -949,6 +369,9 @@ program
949
369
  ' dexto --mode mcp Run as MCP server\n\n' +
950
370
  'Docs: https://docs.dexto.ai')
951
371
  .action(withAnalytics('main', async () => {
372
+ await ensureDextoApiKeyBootstrap();
373
+ await ensureImageImporterConfigured();
374
+ ensureLlmRegistryAutoUpdateStarted();
952
375
  // ——— ENV CHECK (optional) ———
953
376
  if (!existsSync('.env')) {
954
377
  logger.debug('WARNING: .env file not found; copy .env.example and set your API keys.');
@@ -994,7 +417,7 @@ program
994
417
  // The interactive CLI requires a TTY (Ink uses stdin for keypress input).
995
418
  if (opts.mode === 'cli' && !process.stdin.isTTY) {
996
419
  console.error('❌ Interactive CLI requires a TTY.');
997
- console.error('💡 Headless one-shot mode has been removed. Run in an interactive terminal, or use --mode server for automation.');
420
+ console.error('💡 For non-interactive runs, use `dexto run "<prompt>"`, or use --mode server for automation.');
998
421
  safeExit('main', 1, 'no-tty');
999
422
  }
1000
423
  // ——— Infer provider & API key from model ———
@@ -1073,12 +496,14 @@ program
1073
496
  }
1074
497
  // Check setup state and auto-trigger if needed
1075
498
  // Skip if --skip-setup flag is set (for MCP mode, automation, etc.)
499
+ const { requiresSetup } = await import('./cli/utils/setup-utils.js');
1076
500
  if (!opts.skipSetup && (await requiresSetup())) {
1077
501
  if (opts.interactive === false) {
1078
502
  console.error('❌ Setup required but --no-interactive flag is set.');
1079
503
  console.error('💡 Run `dexto setup` first, or use --skip-setup to bypass global setup.');
1080
504
  safeExit('main', 1, 'setup-required-non-interactive');
1081
505
  }
506
+ const { handleSetupCommand } = await import('./cli/commands/setup.js');
1082
507
  await handleSetupCommand({ interactive: true });
1083
508
  // Reload preferences after setup to get the newly selected default mode
1084
509
  // (setup may have just saved a different mode than the default 'web')
@@ -1124,7 +549,7 @@ program
1124
549
  if (authCheck.action === 'login') {
1125
550
  // User wants to log in - run login flow then restart
1126
551
  const { handleLoginCommand } = await import('./cli/commands/auth/login.js');
1127
- await handleLoginCommand({ interactive: true });
552
+ await handleLoginCommand();
1128
553
  // Verify key was actually provisioned (provisionKeys silently catches errors)
1129
554
  const { canUseDextoProvider } = await import('./cli/utils/dexto-setup.js');
1130
555
  if (!(await canUseDextoProvider())) {
@@ -1297,6 +722,7 @@ program
1297
722
  // Config is already enriched and validated - ready for agent creation
1298
723
  // DextoAgent will parse/validate again (parse-twice pattern)
1299
724
  // isInteractiveMode is already defined above for validateAgentConfig
725
+ const { createFileSessionLoggerFactory } = await import('./utils/session-logger-factory.js');
1300
726
  const sessionLoggerFactory = createFileSessionLoggerFactory();
1301
727
  const mcpAuthProviderFactory = opts.mode === 'cli'
1302
728
  ? (await import('./cli/mcp/oauth-factory.js')).createMcpAuthProviderFactory({
@@ -1332,290 +758,23 @@ program
1332
758
  console.error(`❌ Configuration Error: ${err.message}`);
1333
759
  safeExit('main', 1, 'config-error');
1334
760
  }
1335
- // ——— Dispatch based on --mode ———
1336
- // TODO: Refactor mode-specific logic into separate handler files
1337
- // This switch statement has grown large with nested if-else chains for each mode.
1338
- // Consider breaking down into mode-specific handlers (e.g., cli/modes/cli.ts, cli/modes/web.ts)
1339
- // to improve maintainability and reduce complexity in this entry point file.
1340
- // See PR 450 comment: https://github.com/truffle-ai/dexto/pull/450#discussion_r2546242983
1341
- switch (opts.mode) {
1342
- case 'cli': {
1343
- // Set up approval handler for interactive CLI if manual mode OR elicitation enabled
1344
- const needsHandler = validatedConfig.permissions.mode === 'manual' ||
1345
- validatedConfig.elicitation.enabled;
1346
- if (needsHandler) {
1347
- // CLI uses its own approval handler that works directly with AgentEventBus
1348
- // This avoids the indirection of ApprovalCoordinator (designed for HTTP flows)
1349
- const { createCLIApprovalHandler } = await import('./cli/approval/index.js');
1350
- const handler = createCLIApprovalHandler(agent);
1351
- agent.setApprovalHandler(handler);
1352
- logger.debug('CLI approval handler configured for Ink CLI');
1353
- }
1354
- // Start the agent now that approval handler is configured
1355
- await agent.start();
1356
- // Session management - CLI uses explicit sessionId like WebUI
1357
- // NOTE: Migrated from defaultSession pattern which will be deprecated in core
1358
- // We now pass sessionId explicitly to all agent methods (agent.run, agent.switchLLM, etc.)
1359
- // Check if API key is configured before trying to create session
1360
- // Session creation triggers LLM service init which requires API key
1361
- const llmConfig = agent.getCurrentLLMConfig();
1362
- const { requiresApiKey } = await import('@dexto/core');
1363
- if (requiresApiKey(llmConfig.provider) && !llmConfig.apiKey?.trim()) {
1364
- // Offer interactive API key setup instead of just exiting
1365
- const { interactiveApiKeySetup } = await import('./cli/utils/api-key-setup.js');
1366
- console.log(chalk.yellow(`\n⚠️ API key required for provider '${llmConfig.provider}'\n`));
1367
- const setupResult = await interactiveApiKeySetup(llmConfig.provider, {
1368
- exitOnCancel: false,
1369
- model: llmConfig.model,
1370
- });
1371
- if (setupResult.cancelled) {
1372
- await agent.stop().catch(() => { });
1373
- safeExit('main', 0, 'api-key-setup-cancelled');
1374
- }
1375
- if (setupResult.skipped) {
1376
- // User chose to skip - exit with instructions
1377
- await agent.stop().catch(() => { });
1378
- safeExit('main', 0, 'api-key-pending');
1379
- }
1380
- if (setupResult.success && setupResult.apiKey) {
1381
- // API key was entered and saved - reload config and continue
1382
- // Update the agent's LLM config with the new API key
1383
- await agent.switchLLM({
1384
- provider: llmConfig.provider,
1385
- model: llmConfig.model,
1386
- apiKey: setupResult.apiKey,
1387
- });
1388
- logger.info('API key configured successfully, continuing...');
1389
- }
1390
- }
1391
- // Resolve the initial session
1392
- let cliSessionId;
1393
- if (opts.resume) {
1394
- const existing = await agent.getSession(opts.resume);
1395
- if (!existing) {
1396
- console.error(`❌ Session '${opts.resume}' not found`);
1397
- console.error('💡 Use `dexto session list` to see available sessions');
1398
- safeExit('main', 1, 'resume-failed');
1399
- }
1400
- cliSessionId = opts.resume;
1401
- }
1402
- else if (opts.continue) {
1403
- const mostRecentSessionId = await getMostRecentSessionId(agent);
1404
- if (mostRecentSessionId) {
1405
- cliSessionId = mostRecentSessionId;
1406
- }
1407
- else {
1408
- const session = await agent.createSession();
1409
- cliSessionId = session.id;
1410
- }
1411
- }
1412
- else {
1413
- const session = await agent.createSession();
1414
- cliSessionId = session.id;
1415
- }
1416
- // Check for updates (will be shown in Ink header)
1417
- const cliUpdateInfo = await versionCheckPromise;
1418
- // Check if installed agents differ from bundled and prompt to sync
1419
- const needsSync = await shouldPromptForSync();
1420
- if (needsSync) {
1421
- const shouldSync = await p.confirm({
1422
- message: 'Agent config updates available. Sync now?',
1423
- initialValue: true,
1424
- });
1425
- if (!p.isCancel(shouldSync) && shouldSync) {
1426
- await handleSyncAgentsCommand({ force: true, quiet: true });
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, setTuiRuntimeServices }, { registerGracefulShutdown }, { applyLayeredEnvironmentLoading }, { getProviderDisplayName, isValidApiKeyFormat, getProviderInstructions, }, { beginOAuthLogin, DEFAULT_OAUTH_CONFIG, ensureDextoApiKeyForAuthToken, loadAuth, storeAuth, removeAuth, removeDextoApiKeyFromEnv, }, { isUsingDextoCredits }, { canUseDextoProvider },] = await Promise.all([
1445
- import('@dexto/tui'),
1446
- import('./utils/graceful-shutdown.js'),
1447
- import('./utils/env.js'),
1448
- import('./cli/utils/provider-setup.js'),
1449
- import('./cli/auth/index.js'),
1450
- import('./config/effective-llm.js'),
1451
- import('./cli/utils/dexto-setup.js'),
1452
- ]);
1453
- setTuiRuntimeServices({
1454
- registerGracefulShutdown,
1455
- capture: (event, properties) => {
1456
- capture(event, properties);
1457
- },
1458
- applyLayeredEnvironmentLoading,
1459
- getProviderDisplayName,
1460
- isValidApiKeyFormat,
1461
- getProviderInstructions,
1462
- beginOAuthLogin,
1463
- defaultOAuthConfig: DEFAULT_OAUTH_CONFIG,
1464
- ensureDextoApiKeyForAuthToken,
1465
- loadAuth,
1466
- storeAuth,
1467
- removeAuth,
1468
- removeDextoApiKeyFromEnv,
1469
- isUsingDextoCredits,
1470
- canUseDextoProvider,
1471
- });
1472
- await startInkCliRefactored(agent, cliSessionId, {
1473
- updateInfo: cliUpdateInfo ?? undefined,
1474
- configFilePath: resolvedPath,
1475
- ...(initialPrompt && { initialPrompt }),
1476
- bypassPermissions: opts.bypassPermissions,
1477
- });
1478
- }
1479
- catch (error) {
1480
- inkError = error;
1481
- }
1482
- finally {
1483
- // Restore console methods so any errors are visible
1484
- console.log = originalConsole.log;
1485
- console.error = originalConsole.error;
1486
- console.warn = originalConsole.warn;
1487
- console.info = originalConsole.info;
1488
- }
1489
- // Stop the agent after Ink CLI exits
1490
- try {
1491
- await agent.stop();
1492
- }
1493
- catch {
1494
- // Ignore shutdown errors
1495
- }
1496
- // Handle any errors from Ink CLI
1497
- if (inkError) {
1498
- if (inkError instanceof ExitSignal)
1499
- throw inkError;
1500
- const errorMessage = inkError instanceof Error ? inkError.message : String(inkError);
1501
- console.error(`❌ Ink CLI failed: ${errorMessage}`);
1502
- if (inkError instanceof Error && inkError.stack) {
1503
- console.error(inkError.stack);
1504
- }
1505
- safeExit('main', 1, 'ink-cli-error');
1506
- }
1507
- safeExit('main', 0);
1508
- }
1509
- // falls through - safeExit returns never, but eslint doesn't know that
1510
- case 'web': {
1511
- // Default to 3000 for web mode
1512
- const defaultPort = opts.port ? parseInt(opts.port, 10) : 3000;
1513
- const port = getPort(process.env.PORT, defaultPort, 'PORT');
1514
- const serverUrl = process.env.DEXTO_URL ?? `http://localhost:${port}`;
1515
- // Resolve webRoot path (embedded WebUI dist folder)
1516
- const webRoot = resolveWebRoot();
1517
- if (!webRoot) {
1518
- console.warn(chalk.yellow('⚠️ WebUI not found in this build.'));
1519
- console.info('For production: Run "pnpm build:all" to embed the WebUI');
1520
- console.info('For development: Run "pnpm dev" for hot reload');
1521
- }
1522
- // Build WebUI runtime config (analytics, etc.) for injection into index.html
1523
- const webUIConfig = webRoot
1524
- ? { analytics: await getWebUIAnalyticsConfig() }
1525
- : undefined;
1526
- // Start single Hono server serving both API and WebUI
1527
- await startHonoApiServer(agent, port, agent.config.agentCard || {}, derivedAgentId, resolvedPath, webRoot, webUIConfig);
1528
- console.log(chalk.green(`✅ Server running at ${serverUrl}`));
1529
- // Show update notification if available
1530
- const webUpdateInfo = await versionCheckPromise;
1531
- if (webUpdateInfo) {
1532
- displayUpdateNotification(webUpdateInfo);
1533
- }
1534
- // Open WebUI in browser if webRoot is available
1535
- if (webRoot) {
1536
- try {
1537
- const { default: open } = await import('open');
1538
- await open(serverUrl, { wait: false });
1539
- console.log(chalk.green(`🌐 Opened WebUI in browser: ${serverUrl}`));
1540
- }
1541
- catch (_error) {
1542
- console.log(chalk.yellow(`💡 WebUI is available at: ${serverUrl}`));
1543
- }
1544
- }
1545
- break;
1546
- }
1547
- // Start server with REST APIs and SSE on port 3001
1548
- // This also enables dexto to be used as a remote mcp server at localhost:3001/mcp
1549
- case 'server': {
1550
- // Start server with REST APIs and SSE only
1551
- const agentCard = agent.config.agentCard ?? {};
1552
- // Default to 3001 for server mode
1553
- const defaultPort = opts.port ? parseInt(opts.port, 10) : 3001;
1554
- const apiPort = getPort(process.env.PORT, defaultPort, 'PORT');
1555
- const apiUrl = process.env.DEXTO_URL ?? `http://localhost:${apiPort}`;
1556
- console.log('🌐 Starting server (REST APIs + SSE)...');
1557
- await startHonoApiServer(agent, apiPort, agentCard, derivedAgentId, resolvedPath);
1558
- console.log(`✅ Server running at ${apiUrl}`);
1559
- console.log('Available endpoints:');
1560
- console.log(' POST /api/message - Send async message');
1561
- console.log(' POST /api/message-sync - Send sync message');
1562
- console.log(' POST /api/reset - Reset conversation');
1563
- console.log(' GET /api/mcp/servers - List MCP servers');
1564
- console.log(' SSE support available for real-time events');
1565
- // Show update notification if available
1566
- const serverUpdateInfo = await versionCheckPromise;
1567
- if (serverUpdateInfo) {
1568
- displayUpdateNotification(serverUpdateInfo);
1569
- }
1570
- break;
1571
- }
1572
- // TODO: Remove if server mode is stable and supports mcp
1573
- // Starts dexto as a local mcp server
1574
- // Use `dexto --mode mcp` to start dexto as a local mcp server
1575
- // Use `dexto --mode server` to start dexto as a remote server
1576
- case 'mcp': {
1577
- // Start stdio mcp server only
1578
- const agentCardConfig = agent.config.agentCard || {
1579
- name: 'dexto',
1580
- version: '1.0.0',
1581
- };
1582
- try {
1583
- // Logs are already redirected to file by default to prevent interference with stdio transport
1584
- const agentCardData = createAgentCard({
1585
- defaultName: agentCardConfig.name ?? 'dexto',
1586
- defaultVersion: agentCardConfig.version ?? '1.0.0',
1587
- defaultBaseUrl: 'stdio://local-dexto',
1588
- }, agentCardConfig // preserve overrides from agent file
1589
- );
1590
- // Use stdio transport in mcp mode
1591
- const mcpTransport = await createMcpTransport('stdio');
1592
- await initializeMcpServer(agent, agentCardData, mcpTransport);
1593
- }
1594
- catch (err) {
1595
- // Write to stderr instead of stdout to avoid interfering with MCP protocol
1596
- process.stderr.write(`MCP server startup failed: ${err}\n`);
1597
- safeExit('main', 1, 'mcp-startup-failed');
1598
- }
1599
- break;
1600
- }
1601
- default:
1602
- if (opts.mode === 'discord' || opts.mode === 'telegram') {
1603
- console.error(`❌ Error: '${opts.mode}' mode has been moved to examples`);
1604
- console.error('');
1605
- console.error(`The ${opts.mode} bot is now a standalone example that you can customize.`);
1606
- console.error('');
1607
- console.error(`📖 See: examples/${opts.mode}-bot/README.md`);
1608
- console.error('');
1609
- console.error(`To run it:`);
1610
- console.error(` cd examples/${opts.mode}-bot`);
1611
- console.error(` pnpm install`);
1612
- console.error(` pnpm start`);
1613
- }
1614
- else {
1615
- console.error(`❌ Unknown mode '${opts.mode}'. Use web, cli, server, or mcp.`);
1616
- }
1617
- safeExit('main', 1, 'unknown-mode');
1618
- }
761
+ const { dispatchMainMode } = await import('./cli/modes/dispatch.js');
762
+ const mainModeOpts = {
763
+ mode: opts.mode,
764
+ port: opts.port,
765
+ resume: opts.resume,
766
+ continue: opts.continue,
767
+ bypassPermissions: opts.bypassPermissions,
768
+ };
769
+ await dispatchMainMode({
770
+ agent,
771
+ opts: mainModeOpts,
772
+ validatedConfig,
773
+ resolvedPath,
774
+ derivedAgentId,
775
+ initialPrompt,
776
+ getVersionCheckResult,
777
+ });
1619
778
  }, { timeoutMs: 0 }));
1620
- // 17) PARSE & EXECUTE
779
+ // 14) PARSE & EXECUTE
1621
780
  program.parseAsync(process.argv);