dexto 1.13.5 → 1.14.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 (62) hide show
  1. package/dist/api/mcp/http-gateway.d.ts +21 -0
  2. package/dist/api/mcp/http-gateway.d.ts.map +1 -0
  3. package/dist/api/mcp/http-gateway.js +110 -0
  4. package/dist/api/mcp/tool-aggregation-handler.d.ts +9 -6
  5. package/dist/api/mcp/tool-aggregation-handler.d.ts.map +1 -1
  6. package/dist/api/mcp/tool-aggregation-handler.js +203 -70
  7. package/dist/api/server-hono.d.ts +2 -0
  8. package/dist/api/server-hono.d.ts.map +1 -1
  9. package/dist/api/server-hono.js +377 -276
  10. package/dist/cli/approval/headless-approval-handler.d.ts +4 -0
  11. package/dist/cli/approval/headless-approval-handler.d.ts.map +1 -0
  12. package/dist/cli/approval/headless-approval-handler.js +19 -0
  13. package/dist/cli/auth/api-client.js +1 -1
  14. package/dist/cli/auth/constants.d.ts +1 -0
  15. package/dist/cli/auth/constants.d.ts.map +1 -1
  16. package/dist/cli/auth/constants.js +2 -1
  17. package/dist/cli/auth/dexto-api-key.d.ts.map +1 -1
  18. package/dist/cli/auth/dexto-api-key.js +3 -0
  19. package/dist/cli/auth/login-persistence.d.ts +1 -1
  20. package/dist/cli/auth/login-persistence.d.ts.map +1 -1
  21. package/dist/cli/auth/login-persistence.js +9 -1
  22. package/dist/cli/auth/service.d.ts +2 -0
  23. package/dist/cli/auth/service.d.ts.map +1 -1
  24. package/dist/cli/auth/service.js +1 -0
  25. package/dist/cli/commands/agents/register.d.ts.map +1 -1
  26. package/dist/cli/commands/agents/register.js +0 -1
  27. package/dist/cli/commands/auth/login.d.ts.map +1 -1
  28. package/dist/cli/commands/auth/login.js +9 -4
  29. package/dist/cli/commands/cloud/discovery.d.ts +17 -0
  30. package/dist/cli/commands/cloud/discovery.d.ts.map +1 -0
  31. package/dist/cli/commands/cloud/discovery.js +88 -0
  32. package/dist/cli/commands/cloud/register.d.ts +3 -0
  33. package/dist/cli/commands/cloud/register.d.ts.map +1 -0
  34. package/dist/cli/commands/cloud/register.js +27 -0
  35. package/dist/cli/commands/mcp/register.d.ts +1 -1
  36. package/dist/cli/commands/mcp/register.d.ts.map +1 -1
  37. package/dist/cli/commands/mcp/register.js +259 -26
  38. package/dist/cli/commands/mcp/standalone.d.ts +98 -0
  39. package/dist/cli/commands/mcp/standalone.d.ts.map +1 -0
  40. package/dist/cli/commands/mcp/standalone.js +489 -0
  41. package/dist/cli/commands/run/headless.d.ts +3 -1
  42. package/dist/cli/commands/run/headless.d.ts.map +1 -1
  43. package/dist/cli/commands/run/headless.js +13 -1
  44. package/dist/cli/commands/run/output.d.ts +21 -0
  45. package/dist/cli/commands/run/output.d.ts.map +1 -0
  46. package/dist/cli/commands/run/output.js +95 -0
  47. package/dist/cli/commands/run/register.d.ts.map +1 -1
  48. package/dist/cli/commands/run/register.js +27 -3
  49. package/dist/cli/commands/setup.d.ts +2 -2
  50. package/dist/cli/commands/trace/client.d.ts +2 -2
  51. package/dist/cli/utils/options.js +3 -1
  52. package/dist/config/cli-overrides.d.ts +3 -1
  53. package/dist/config/cli-overrides.d.ts.map +1 -1
  54. package/dist/config/cli-overrides.js +9 -3
  55. package/dist/index-main.js +11 -10
  56. package/dist/utils/env.d.ts +2 -0
  57. package/dist/utils/env.d.ts.map +1 -1
  58. package/dist/utils/env.js +6 -0
  59. package/dist/utils/graceful-shutdown.d.ts +1 -1
  60. package/dist/utils/graceful-shutdown.d.ts.map +1 -1
  61. package/dist/utils/graceful-shutdown.js +28 -9
  62. package/package.json +14 -14
@@ -0,0 +1,489 @@
1
+ import { readFile, mkdir, open, rename, rm, lstat, writeFile, unlink, } from 'node:fs/promises';
2
+ import { randomUUID } from 'node:crypto';
3
+ import { resolve, dirname } from 'node:path';
4
+ import { parseDocument } from 'yaml';
5
+ import { z } from 'zod';
6
+ import { StdioServerConfigSchema, HttpServerConfigSchema, SseServerConfigSchema, McpServerConfigSchema, MCPManager, MCPError, } from '@dexto/core/mcp';
7
+ import { DextoLogger, DextoLogComponent } from '@dexto/core/logger';
8
+ const configWriteFailureMessage = 'Cannot write MCP configuration. Use a regular file in a writable directory.';
9
+ // Configuration edits keep templates literal; Core expands them at connection time.
10
+ const literalString = z.string();
11
+ const literalMap = z.record(z.string(), literalString);
12
+ const storedServerSchema = z.discriminatedUnion('type', [
13
+ StdioServerConfigSchema.extend({
14
+ command: literalString.min(1),
15
+ args: z.array(literalString).default([]),
16
+ env: literalMap.default({}),
17
+ }),
18
+ HttpServerConfigSchema.extend({ url: literalString.min(1), headers: literalMap.default({}) }),
19
+ SseServerConfigSchema.extend({ url: literalString.min(1), headers: literalMap.default({}) }),
20
+ ]);
21
+ const documentSchema = z
22
+ .object({ mcpServers: z.record(z.string(), storedServerSchema).default({}) })
23
+ .passthrough();
24
+ export async function runStandaloneMcp(command, options) {
25
+ if ('server' in command && command.server.trim().length === 0) {
26
+ return {
27
+ exitCode: 2,
28
+ output: {
29
+ error: { code: 'invalid_server_name', message: 'Server name must not be blank.' },
30
+ },
31
+ };
32
+ }
33
+ const configPath = resolve(options.config ?? '.dexto/mcp.yml');
34
+ try {
35
+ if (command.command === 'add' || command.command === 'remove')
36
+ return await editMcpConfiguration(command, configPath);
37
+ const { servers } = await loadStandaloneMcpConfiguration(configPath, false);
38
+ const config = { mcpServers: servers };
39
+ if (command.command !== 'list') {
40
+ const server = config.mcpServers[command.server];
41
+ if (!server || !Object.hasOwn(config.mcpServers, command.server))
42
+ return {
43
+ exitCode: 2,
44
+ output: {
45
+ error: { code: 'server_not_found', message: 'Server is not configured.' },
46
+ },
47
+ };
48
+ if (!server.enabled)
49
+ return {
50
+ exitCode: 2,
51
+ output: { error: { code: 'server_disabled', message: 'Server is disabled.' } },
52
+ };
53
+ if (command.command === 'get-prompt') {
54
+ let args;
55
+ try {
56
+ args = z
57
+ .record(z.string(), z.string())
58
+ .parse(JSON.parse(command.argumentsJson));
59
+ }
60
+ catch {
61
+ return {
62
+ exitCode: 2,
63
+ output: {
64
+ error: {
65
+ code: 'invalid_arguments',
66
+ message: 'Prompt arguments must be a valid JSON object of strings.',
67
+ },
68
+ },
69
+ };
70
+ }
71
+ return await runMcpOperation({
72
+ command: 'get-prompt',
73
+ server: command.server,
74
+ prompt: command.prompt,
75
+ arguments: args,
76
+ }, McpServerConfigSchema.parse(server), {});
77
+ }
78
+ let args = {};
79
+ if (command.command === 'call') {
80
+ try {
81
+ args = z
82
+ .record(z.string(), z.unknown())
83
+ .parse(JSON.parse(command.argumentsJson));
84
+ }
85
+ catch {
86
+ return {
87
+ exitCode: 2,
88
+ output: {
89
+ error: {
90
+ code: 'invalid_arguments',
91
+ message: 'Tool arguments must be a valid JSON object.',
92
+ },
93
+ },
94
+ };
95
+ }
96
+ }
97
+ return await runMcpOperation(command, McpServerConfigSchema.parse(server), args);
98
+ }
99
+ return {
100
+ exitCode: 0,
101
+ output: {
102
+ servers: Object.entries(config.mcpServers).map(([name, server]) => ({
103
+ name,
104
+ type: server.type,
105
+ enabled: server.enabled,
106
+ status: server.enabled ? 'configured' : 'disabled',
107
+ })),
108
+ },
109
+ };
110
+ }
111
+ catch (error) {
112
+ if (error instanceof McpConfigurationError)
113
+ return { exitCode: 2, output: { error: { code: error.code, message: error.message } } };
114
+ if (typeof error === 'object' && error !== null && 'code' in error)
115
+ return {
116
+ exitCode: 2,
117
+ output: {
118
+ error: {
119
+ code: 'config_write_failed',
120
+ message: configWriteFailureMessage,
121
+ },
122
+ },
123
+ };
124
+ return {
125
+ exitCode: 2,
126
+ output: {
127
+ error: { code: 'invalid_config', message: 'Cannot validate MCP configuration.' },
128
+ },
129
+ };
130
+ }
131
+ }
132
+ async function rejectMcpConfigurationSymlink(path) {
133
+ try {
134
+ if ((await lstat(path)).isSymbolicLink())
135
+ throw new McpConfigurationError('config_write_failed', configWriteFailureMessage);
136
+ }
137
+ catch (error) {
138
+ if (typeof error === 'object' &&
139
+ error !== null &&
140
+ 'code' in error &&
141
+ error.code === 'ENOENT')
142
+ return;
143
+ throw error;
144
+ }
145
+ }
146
+ /** Replace POSIX files privately; retain Windows in-place permissions. */
147
+ async function writeMcpConfiguration(path, content) {
148
+ if (process.platform === 'win32') {
149
+ await rejectMcpConfigurationSymlink(path);
150
+ await writeFile(path, content, { mode: 0o600 });
151
+ return;
152
+ }
153
+ const temporaryPath = `${path}.${randomUUID()}.tmp`;
154
+ const file = await open(temporaryPath, 'wx', 0o600);
155
+ try {
156
+ await file.chmod(0o600);
157
+ await file.writeFile(content, 'utf8');
158
+ await file.close();
159
+ await rejectMcpConfigurationSymlink(path);
160
+ await rename(temporaryPath, path);
161
+ }
162
+ catch (error) {
163
+ await file.close().catch(() => undefined);
164
+ await rm(temporaryPath, { force: true }).catch(() => undefined);
165
+ throw error;
166
+ }
167
+ }
168
+ async function editMcpConfiguration(command, configPath) {
169
+ const lockPath = `${configPath}.lock`;
170
+ if (command.command === 'add')
171
+ await mkdir(dirname(configPath), { recursive: true });
172
+ const editLock = await open(lockPath, 'wx', 0o600).catch((error) => {
173
+ if (typeof error === 'object' && error !== null && 'code' in error) {
174
+ if (error.code === 'EEXIST')
175
+ throw new McpConfigurationError('config_busy', 'MCP configuration is being edited. Retry after the current edit completes.');
176
+ if (command.command === 'remove' && error.code === 'ENOENT')
177
+ throw new McpConfigurationError('config_read_failed', 'Cannot read MCP configuration. Check --config and file permissions.');
178
+ }
179
+ throw error;
180
+ });
181
+ let committed = false;
182
+ try {
183
+ await rejectMcpConfigurationSymlink(configPath);
184
+ const { document, servers } = await loadStandaloneMcpConfiguration(configPath, command.command === 'add');
185
+ if (command.command === 'remove') {
186
+ if (!Object.hasOwn(servers, command.server))
187
+ return {
188
+ exitCode: 2,
189
+ output: {
190
+ error: { code: 'server_not_found', message: 'Server is not configured.' },
191
+ },
192
+ };
193
+ document.deleteIn(['mcpServers', command.server]);
194
+ await writeMcpConfiguration(configPath, document.toString());
195
+ committed = true;
196
+ return { exitCode: 0, output: { server: command.server, status: 'removed' } };
197
+ }
198
+ let raw;
199
+ try {
200
+ raw = JSON.parse(command.serverConfig);
201
+ storedServerSchema.parse(raw);
202
+ }
203
+ catch {
204
+ return {
205
+ exitCode: 2,
206
+ output: {
207
+ error: {
208
+ code: 'invalid_server_config',
209
+ message: 'Server configuration must be a valid MCP server JSON object.',
210
+ },
211
+ },
212
+ };
213
+ }
214
+ if (Object.hasOwn(servers, command.server) && !command.replace)
215
+ return {
216
+ exitCode: 2,
217
+ output: {
218
+ error: {
219
+ code: 'duplicate_server',
220
+ message: 'Server already configured. Use --replace to overwrite it.',
221
+ },
222
+ },
223
+ };
224
+ document.setIn(['mcpServers', command.server], raw);
225
+ await writeMcpConfiguration(configPath, document.toString());
226
+ committed = true;
227
+ return { exitCode: 0, output: { server: command.server, status: 'configured' } };
228
+ }
229
+ finally {
230
+ await releaseMcpConfigurationEditLock(editLock, lockPath, committed);
231
+ }
232
+ }
233
+ async function releaseMcpConfigurationEditLock(editLock, lockPath, committed) {
234
+ try {
235
+ try {
236
+ await editLock.close();
237
+ }
238
+ finally {
239
+ await unlink(lockPath);
240
+ }
241
+ }
242
+ catch {
243
+ // Failed edits retain their original error; committed edits must surface cleanup failure.
244
+ if (committed)
245
+ throw new McpConfigurationError('config_lock_cleanup_failed', 'MCP configuration edit cleanup failed. The edit may already have committed; inspect the configuration before retrying or removing its lock.');
246
+ }
247
+ }
248
+ class McpConfigurationError extends Error {
249
+ code;
250
+ constructor(code, message) {
251
+ super(message);
252
+ this.code = code;
253
+ }
254
+ }
255
+ /** Load MCP-only YAML for direct commands or the aggregation host without agent setup. */
256
+ export async function loadStandaloneMcpConfiguration(path, allowMissing) {
257
+ let text;
258
+ try {
259
+ text = await readFile(path, 'utf8');
260
+ }
261
+ catch (error) {
262
+ if (allowMissing &&
263
+ typeof error === 'object' &&
264
+ error !== null &&
265
+ 'code' in error &&
266
+ error.code === 'ENOENT')
267
+ text = 'mcpServers: {}\n';
268
+ else
269
+ throw new McpConfigurationError('config_read_failed', 'Cannot read MCP configuration. Check --config and file permissions.');
270
+ }
271
+ try {
272
+ const document = parseDocument(text);
273
+ if (document.errors.length)
274
+ throw new Error('Invalid YAML');
275
+ const config = documentSchema.parse(document.toJS());
276
+ return { document, servers: config.mcpServers };
277
+ }
278
+ catch {
279
+ throw new McpConfigurationError('invalid_config', 'Invalid MCP YAML or server configuration.');
280
+ }
281
+ }
282
+ async function runMcpOperation(command, config, args) {
283
+ const logger = new DextoLogger({
284
+ level: 'error',
285
+ component: DextoLogComponent.MCP,
286
+ agentId: 'mcp-cli',
287
+ transports: [],
288
+ });
289
+ const manager = new MCPManager(logger);
290
+ const server = command.server;
291
+ const cancellation = new AbortController();
292
+ let interruption;
293
+ const interrupt = (signal) => {
294
+ if (interruption !== undefined)
295
+ return;
296
+ interruption = signal;
297
+ cancellation.abort();
298
+ };
299
+ const onSigint = () => interrupt('SIGINT');
300
+ const onSigterm = () => interrupt('SIGTERM');
301
+ process.on('SIGINT', onSigint);
302
+ process.on('SIGTERM', onSigterm);
303
+ let connected = false;
304
+ let result = {
305
+ exitCode: 3,
306
+ output: {
307
+ server,
308
+ error: {
309
+ code: 'mcp_connection_failed',
310
+ message: 'Cannot connect to MCP server. Check its configuration, credentials and availability.',
311
+ },
312
+ },
313
+ };
314
+ try {
315
+ await manager.connectServer(server, config);
316
+ connected = true;
317
+ if (!cancellation.signal.aborted) {
318
+ result = await performMcpOperation(manager, command, args, config.timeout, cancellation.signal);
319
+ }
320
+ }
321
+ catch (error) {
322
+ if (connected) {
323
+ const notFound = typeof error === 'object' &&
324
+ error !== null &&
325
+ 'code' in error &&
326
+ error.code === 'mcp_tool_not_found';
327
+ result = {
328
+ exitCode: 4,
329
+ output: {
330
+ server,
331
+ error: {
332
+ code: notFound ? 'mcp_tool_not_found' : 'mcp_operation_failed',
333
+ message: notFound
334
+ ? 'Tool is not advertised by the selected server.'
335
+ : 'MCP operation failed. Check the selected operation and its arguments.',
336
+ },
337
+ },
338
+ };
339
+ }
340
+ }
341
+ finally {
342
+ try {
343
+ const cleanup = await Promise.allSettled([manager.disconnectAll()]);
344
+ cleanup.push(...(await Promise.allSettled([logger.destroy()])));
345
+ if (cleanup.some((item) => item.status === 'rejected') && result.exitCode === 0) {
346
+ result = {
347
+ exitCode: 4,
348
+ output: {
349
+ server,
350
+ error: {
351
+ code: 'mcp_cleanup_failed',
352
+ message: 'MCP connection cleanup failed.',
353
+ },
354
+ },
355
+ };
356
+ }
357
+ }
358
+ finally {
359
+ process.off('SIGINT', onSigint);
360
+ process.off('SIGTERM', onSigterm);
361
+ }
362
+ }
363
+ if (interruption !== undefined) {
364
+ return {
365
+ exitCode: interruption === 'SIGINT' ? 130 : 143,
366
+ output: {
367
+ server,
368
+ signal: interruption,
369
+ error: { code: 'mcp_interrupted', message: 'MCP operation interrupted.' },
370
+ },
371
+ };
372
+ }
373
+ return result;
374
+ }
375
+ async function performMcpOperation(manager, command, args, timeout, signal) {
376
+ const server = command.server;
377
+ switch (command.command) {
378
+ case 'connect':
379
+ return { exitCode: 0, output: { server, status: 'connected', connection: 'closed' } };
380
+ case 'tools':
381
+ return {
382
+ exitCode: 0,
383
+ output: { server, tools: manager.getToolDescriptors(), connection: 'closed' },
384
+ };
385
+ case 'resources':
386
+ return {
387
+ exitCode: 0,
388
+ output: {
389
+ server,
390
+ resources: await manager.listAllResources(),
391
+ connection: 'closed',
392
+ },
393
+ };
394
+ case 'prompts':
395
+ return {
396
+ exitCode: 0,
397
+ output: { server, prompts: manager.getAllPromptMetadata(), connection: 'closed' },
398
+ };
399
+ case 'get-prompt': {
400
+ const protocol = await getConnectedServerProtocol(manager, server);
401
+ const result = await protocol.getPrompt({ name: command.prompt, arguments: command.arguments }, { timeout, signal });
402
+ return {
403
+ exitCode: 0,
404
+ output: { server, prompt: command.prompt, result, connection: 'closed' },
405
+ };
406
+ }
407
+ case 'read-resource': {
408
+ const protocol = await getConnectedServerProtocol(manager, server);
409
+ const result = await protocol.readResource({ uri: command.uri }, { timeout, signal });
410
+ return {
411
+ exitCode: 0,
412
+ output: { server, uri: command.uri, result, connection: 'closed' },
413
+ };
414
+ }
415
+ case 'call': {
416
+ const descriptor = manager
417
+ .getToolDescriptors()
418
+ .find((tool) => tool.identity.connectionId === server &&
419
+ tool.identity.toolName === command.tool);
420
+ if (!descriptor)
421
+ throw MCPError.toolNotFound(command.tool);
422
+ const input = manager.validateToolInput(descriptor.name, args);
423
+ const result = await manager.callToolDirect({
424
+ identity: descriptor.identity,
425
+ arguments: input,
426
+ signal,
427
+ });
428
+ return {
429
+ exitCode: result.isError === true ? 4 : 0,
430
+ output: { server, tool: command.tool, result, connection: 'closed' },
431
+ };
432
+ }
433
+ }
434
+ }
435
+ async function getConnectedServerProtocol(manager, server) {
436
+ const client = manager.getClients().get(server);
437
+ if (!client)
438
+ throw new Error('Connected MCP client missing');
439
+ return await client.getConnectedClient();
440
+ }
441
+ /** Convert CLI setup forms to literal configuration without expanding credentials. */
442
+ export function serverConfigurationFromOptions(options) {
443
+ const { serverConfig, transport, command, arg, env, url, header } = options;
444
+ if (serverConfig !== undefined) {
445
+ if ([transport, command, arg, env, url, header].some((value) => value !== undefined))
446
+ throw new Error('Conflicting setup options');
447
+ return serverConfig;
448
+ }
449
+ if (command !== undefined &&
450
+ url === undefined &&
451
+ header === undefined &&
452
+ (transport === undefined || transport === 'stdio')) {
453
+ return JSON.stringify({
454
+ type: 'stdio',
455
+ command,
456
+ args: arg ?? [],
457
+ env: parseAssignments(env ?? []),
458
+ });
459
+ }
460
+ if (url !== undefined &&
461
+ command === undefined &&
462
+ arg === undefined &&
463
+ env === undefined &&
464
+ (transport === undefined || transport === 'http' || transport === 'sse')) {
465
+ return JSON.stringify({
466
+ type: transport ?? 'http',
467
+ url,
468
+ headers: parseAssignments(header ?? []),
469
+ });
470
+ }
471
+ throw new Error('Invalid or conflicting setup options');
472
+ }
473
+ function parseAssignments(values) {
474
+ const result = {};
475
+ for (const value of values) {
476
+ const separator = value.indexOf('=');
477
+ if (separator <= 0)
478
+ throw new Error('Expected NAME=VALUE');
479
+ const name = value.slice(0, separator);
480
+ if (Object.hasOwn(result, name))
481
+ throw new Error('Duplicate assignment');
482
+ Object.defineProperty(result, name, {
483
+ value: value.slice(separator + 1),
484
+ enumerable: true,
485
+ configurable: true,
486
+ });
487
+ }
488
+ return result;
489
+ }
@@ -1,8 +1,10 @@
1
1
  import { type DextoAgent } from '@dexto/core';
2
+ import { type HeadlessApprovalRequired, type HeadlessOutputFormat } from './output.js';
2
3
  export type HeadlessRunResult = {
3
4
  finalMessage?: string;
4
5
  totalTokens?: number;
5
6
  fatalError?: Error;
7
+ approvalRequired?: HeadlessApprovalRequired[];
6
8
  };
7
9
  export declare function resolveHeadlessPrompt(promptArg?: string): Promise<string>;
8
10
  export declare function printHeadlessMcpStartup(agent: DextoAgent): void;
@@ -13,7 +15,7 @@ export declare function printHeadlessRunSummary(params: {
13
15
  agentPath: string;
14
16
  cliVersion: string;
15
17
  }): void;
16
- export declare function executeHeadlessRun(agent: DextoAgent, sessionId: string, prompt: string): Promise<HeadlessRunResult>;
18
+ export declare function executeHeadlessRun(agent: DextoAgent, sessionId: string, prompt: string, format?: HeadlessOutputFormat): Promise<HeadlessRunResult>;
17
19
  export declare function printHeadlessAssistantResponse(message: string, totalTokens?: number): void;
18
20
  export declare function writeFinalMessageToStdout(message: string): void;
19
21
  export declare function writeHeadlessError(message: string): void;
@@ -1 +1 @@
1
- {"version":3,"file":"headless.d.ts","sourceRoot":"","sources":["../../../../src/cli/commands/run/headless.ts"],"names":[],"mappings":"AAAA,OAAO,EAGH,KAAK,UAAU,EAIlB,MAAM,aAAa,CAAC;AAWrB,MAAM,MAAM,iBAAiB,GAAG;IAC5B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,KAAK,CAAC;CACtB,CAAC;AA0IF,wBAAsB,qBAAqB,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAY/E;AAED,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,UAAU,GAAG,IAAI,CAgC/D;AAED,wBAAgB,uBAAuB,CAAC,MAAM,EAAE;IAC5C,KAAK,EAAE,UAAU,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;CACtB,GAAG,IAAI,CAgBP;AAED,wBAAsB,kBAAkB,CACpC,KAAK,EAAE,UAAU,EACjB,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,MAAM,GACf,OAAO,CAAC,iBAAiB,CAAC,CAuH5B;AAED,wBAAgB,8BAA8B,CAAC,OAAO,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAM1F;AAED,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAM/D;AAED,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAExD"}
1
+ {"version":3,"file":"headless.d.ts","sourceRoot":"","sources":["../../../../src/cli/commands/run/headless.ts"],"names":[],"mappings":"AAAA,OAAO,EAGH,KAAK,UAAU,EAIlB,MAAM,aAAa,CAAC;AAErB,OAAO,EAGH,KAAK,wBAAwB,EAC7B,KAAK,oBAAoB,EAC5B,MAAM,aAAa,CAAC;AAWrB,MAAM,MAAM,iBAAiB,GAAG;IAC5B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,KAAK,CAAC;IACnB,gBAAgB,CAAC,EAAE,wBAAwB,EAAE,CAAC;CACjD,CAAC;AA0IF,wBAAsB,qBAAqB,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAY/E;AAED,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,UAAU,GAAG,IAAI,CAgC/D;AAED,wBAAgB,uBAAuB,CAAC,MAAM,EAAE;IAC5C,KAAK,EAAE,UAAU,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;CACtB,GAAG,IAAI,CAgBP;AAED,wBAAsB,kBAAkB,CACpC,KAAK,EAAE,UAAU,EACjB,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,MAAM,EACd,MAAM,GAAE,oBAA6B,GACtC,OAAO,CAAC,iBAAiB,CAAC,CAmI5B;AAED,wBAAgB,8BAA8B,CAAC,OAAO,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAM1F;AAED,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAM/D;AAED,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAExD"}
@@ -1,4 +1,5 @@
1
1
  import { isTextPart, safeStringify, } from '@dexto/core';
2
+ import { writeHeadlessEvent, toHeadlessApprovalRequired, } from './output.js';
2
3
  const HEADLESS_TOOL_OUTPUT_MAX_LINES = 20;
3
4
  const HEADLESS_SECTION_SEPARATOR = '========================================';
4
5
  function writeHeadlessLine(line = '') {
@@ -163,15 +164,24 @@ export function printHeadlessRunSummary(params) {
163
164
  writeHeadlessSeparator();
164
165
  writeHeadlessTaggedBlock('USER', prompt);
165
166
  }
166
- export async function executeHeadlessRun(agent, sessionId, prompt) {
167
+ export async function executeHeadlessRun(agent, sessionId, prompt, format = 'text') {
167
168
  const toolCallState = new Map();
169
+ const approvalRequired = [];
168
170
  let anonymousToolCallCounter = 0;
169
171
  let finalMessage;
170
172
  let totalTokens;
171
173
  let fatalError;
172
174
  try {
173
175
  for await (const event of await agent.stream(prompt, sessionId)) {
176
+ if (format === 'jsonl')
177
+ await writeHeadlessEvent(event);
174
178
  switch (event.name) {
179
+ case 'approval:request': {
180
+ const required = toHeadlessApprovalRequired(event);
181
+ approvalRequired.push(required);
182
+ writeHeadlessTaggedLine('APPROVAL_REQUIRED', `${'toolName' in required ? required.toolName : required.approvalType}: ${required.message} (approval ${required.approvalId})`);
183
+ break;
184
+ }
175
185
  case 'llm:tool-call': {
176
186
  const callKey = event.callId ?? `anonymous-${++anonymousToolCallCounter}`;
177
187
  const call = {
@@ -264,6 +274,8 @@ export async function executeHeadlessRun(agent, sessionId, prompt) {
264
274
  if (fatalError !== undefined) {
265
275
  result.fatalError = fatalError;
266
276
  }
277
+ if (approvalRequired.length)
278
+ result.approvalRequired = approvalRequired;
267
279
  return result;
268
280
  }
269
281
  export function printHeadlessAssistantResponse(message, totalTokens) {
@@ -0,0 +1,21 @@
1
+ import { ApprovalType, type ApprovalRequest, type StreamingEvent } from '@dexto/core';
2
+ import type { HeadlessRunResult } from './headless.js';
3
+ export type HeadlessOutputFormat = 'text' | 'json' | 'jsonl';
4
+ export type HeadlessApprovalRequired = {
5
+ approvalId: string;
6
+ sessionId?: string;
7
+ message: string;
8
+ } & ({
9
+ approvalType: typeof ApprovalType.TOOL_APPROVAL;
10
+ toolName: string;
11
+ toolCallId: string;
12
+ } | {
13
+ approvalType: typeof ApprovalType.COMMAND_APPROVAL;
14
+ toolName: string;
15
+ } | {
16
+ approvalType: Exclude<ApprovalRequest['type'], typeof ApprovalType.TOOL_APPROVAL | typeof ApprovalType.COMMAND_APPROVAL>;
17
+ });
18
+ export declare function toHeadlessApprovalRequired(request: ApprovalRequest): HeadlessApprovalRequired;
19
+ export declare function writeHeadlessResult(format: HeadlessOutputFormat, result: HeadlessRunResult, sessionId: string | undefined): Promise<void>;
20
+ export declare function writeHeadlessEvent(event: StreamingEvent): Promise<void>;
21
+ //# sourceMappingURL=output.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"output.d.ts","sourceRoot":"","sources":["../../../../src/cli/commands/run/output.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,KAAK,eAAe,EAAE,KAAK,cAAc,EAAE,MAAM,aAAa,CAAC;AAEtF,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAEvD,MAAM,MAAM,oBAAoB,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC;AAE7D,MAAM,MAAM,wBAAwB,GAAG;IACnC,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;CACnB,GAAG,CACE;IAAE,YAAY,EAAE,OAAO,YAAY,CAAC,aAAa,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,GACzF;IAAE,YAAY,EAAE,OAAO,YAAY,CAAC,gBAAgB,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,GACxE;IACI,YAAY,EAAE,OAAO,CACjB,eAAe,CAAC,MAAM,CAAC,EACvB,OAAO,YAAY,CAAC,aAAa,GAAG,OAAO,YAAY,CAAC,gBAAgB,CAC3E,CAAC;CACL,CACN,CAAC;AAEF,wBAAgB,0BAA0B,CAAC,OAAO,EAAE,eAAe,GAAG,wBAAwB,CAuB7F;AAED,wBAAsB,mBAAmB,CACrC,MAAM,EAAE,oBAAoB,EAC5B,MAAM,EAAE,iBAAiB,EACzB,SAAS,EAAE,MAAM,GAAG,SAAS,GAC9B,OAAO,CAAC,IAAI,CAAC,CAaf;AAED,wBAAsB,kBAAkB,CAAC,KAAK,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,CA2C7E"}
@@ -0,0 +1,95 @@
1
+ import { ApprovalType } from '@dexto/core';
2
+ import { HEADLESS_APPROVAL_MESSAGE } from '../../approval/headless-approval-handler.js';
3
+ export function toHeadlessApprovalRequired(request) {
4
+ const correlation = {
5
+ approvalId: request.approvalId,
6
+ ...(request.sessionId !== undefined ? { sessionId: request.sessionId } : {}),
7
+ message: HEADLESS_APPROVAL_MESSAGE,
8
+ };
9
+ switch (request.type) {
10
+ case ApprovalType.TOOL_APPROVAL:
11
+ return {
12
+ ...correlation,
13
+ approvalType: request.type,
14
+ toolName: request.metadata.toolName,
15
+ toolCallId: request.metadata.toolCallId,
16
+ };
17
+ case ApprovalType.COMMAND_APPROVAL:
18
+ return {
19
+ ...correlation,
20
+ approvalType: request.type,
21
+ toolName: request.metadata.toolName,
22
+ };
23
+ default:
24
+ return { ...correlation, approvalType: request.type };
25
+ }
26
+ }
27
+ export async function writeHeadlessResult(format, result, sessionId) {
28
+ if (format === 'text')
29
+ return;
30
+ await writeJsonLineToStdout({
31
+ version: 1,
32
+ ...(format === 'json'
33
+ ? { status: result.fatalError ? 'failed' : 'completed' }
34
+ : { type: result.fatalError ? 'error' : 'complete' }),
35
+ ...(sessionId !== undefined ? { sessionId } : {}),
36
+ ...(result.finalMessage !== undefined ? { content: result.finalMessage } : {}),
37
+ ...(result.totalTokens !== undefined ? { totalTokens: result.totalTokens } : {}),
38
+ ...(result.fatalError ? { error: result.fatalError.message } : {}),
39
+ ...(result.approvalRequired?.length ? { approvalRequired: result.approvalRequired } : {}),
40
+ });
41
+ }
42
+ export async function writeHeadlessEvent(event) {
43
+ let payload;
44
+ switch (event.name) {
45
+ case 'approval:request':
46
+ payload = { type: 'approval_required', ...toHeadlessApprovalRequired(event) };
47
+ break;
48
+ case 'llm:chunk':
49
+ payload = { type: 'message_delta', content: event.content };
50
+ break;
51
+ case 'llm:tool-call':
52
+ payload = {
53
+ type: 'tool_call',
54
+ toolName: event.toolName,
55
+ args: event.args,
56
+ ...(event.callId ? { callId: event.callId } : {}),
57
+ };
58
+ break;
59
+ case 'llm:tool-result':
60
+ payload = {
61
+ type: 'tool_result',
62
+ toolName: event.toolName,
63
+ success: event.success,
64
+ ...(event.callId ? { callId: event.callId } : {}),
65
+ ...(!event.success && event.error ? { error: event.error } : {}),
66
+ };
67
+ break;
68
+ case 'llm:unsupported-input':
69
+ payload = { type: 'warning', errors: event.errors };
70
+ break;
71
+ case 'llm:error':
72
+ payload = {
73
+ type: 'run_error',
74
+ recoverable: event.recoverable,
75
+ error: event.error.message,
76
+ };
77
+ break;
78
+ case 'llm:response':
79
+ payload = { type: 'response', content: event.content };
80
+ break;
81
+ default:
82
+ return;
83
+ }
84
+ await writeJsonLineToStdout({ version: 1, ...payload });
85
+ }
86
+ function writeJsonLineToStdout(payload) {
87
+ return new Promise((resolve, reject) => {
88
+ process.stdout.write(`${JSON.stringify(payload)}\n`, (error) => {
89
+ if (error)
90
+ reject(error);
91
+ else
92
+ resolve();
93
+ });
94
+ });
95
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"register.d.ts","sourceRoot":"","sources":["../../../../src/cli/commands/run/register.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,6BAA6B,EAAE,MAAM,wBAAwB,CAAC;AAE5E,wBAAgB,kBAAkB,CAAC,EAC/B,OAAO,EACP,UAAU,EACV,4BAA4B,GAC/B,EAAE,6BAA6B,GAAG,IAAI,CA+FtC"}
1
+ {"version":3,"file":"register.d.ts","sourceRoot":"","sources":["../../../../src/cli/commands/run/register.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,6BAA6B,EAAE,MAAM,wBAAwB,CAAC;AAE5E,wBAAgB,kBAAkB,CAAC,EAC/B,OAAO,EACP,UAAU,EACV,4BAA4B,GAC/B,EAAE,6BAA6B,GAAG,IAAI,CAoItC"}