import { randomUUID } from 'node:crypto'; import type { Command } from 'commander'; import { withAuth } from './with-auth.js'; import { attachInteractionSession, enrollInteractionSession, fetchInteractionHealth, fetchInteractionSessions, fetchInteractionStatus, fetchInteractionTree, recoverInteractionSession, sendInteractionMessage, stopInteractionSession, streamInteractionSession, } from '../tui/gateway-api.js'; interface InteractionOptions { gateway: string; agent?: string; correlationId?: string; } function configuredAgentName(options: InteractionOptions): string { const name = options.agent?.trim() || process.env['MOSAIC_AGENT_NAME']?.trim(); if (!name) throw new Error('Interaction agent name is required (--agent or MOSAIC_AGENT_NAME)'); return name; } async function authenticatedGateway(options: InteractionOptions) { return withAuth(options.gateway); } async function authRequest(options: InteractionOptions) { const auth = await authenticatedGateway(options); return { gateway: auth.gateway, cookie: auth.cookie, agentName: configuredAgentName(options), correlationId: options.correlationId?.trim() || randomUUID(), }; } function options(command: Command): Command { return command .option('-g, --gateway ', 'Gateway URL', 'http://localhost:14242') .option('--agent ', 'Configured interaction agent name') .option('--correlation-id ', 'Correlation ID for audit tracing'); } function print(value: unknown): void { console.log(JSON.stringify(value, null, 2)); } /** * Generic interaction command surface. The deployed instance name is data * (`--agent` / MOSAIC_AGENT_NAME), not a source-code command literal. */ export function registerInteractionCommand(program: Command): Command { const command = program .command('interaction') .description('Operate a configured durable interaction agent') .configureHelp({ sortSubcommands: true }); options( command .command('status') .description('Show credential-safe effective policy and provider status'), ).action(async (opts: InteractionOptions) => { const auth = await authenticatedGateway(opts); print(await fetchInteractionStatus(auth.gateway, auth.cookie)); }); options( command .command('health') .description('Show readiness without configuration or credential data'), ).action(async (opts: InteractionOptions) => { print(await fetchInteractionHealth(opts.gateway)); }); options( command .command('sessions ') .description('List provider sessions visible to this actor'), ).action(async (provider: string, opts: InteractionOptions) => { const request = await authRequest(opts); print( await fetchInteractionSessions(request.gateway, request.cookie, { ...request, providerId: provider, }), ); }); options( command.command('tree ').description('Show the provider session hierarchy'), ).action(async (provider: string, opts: InteractionOptions) => { const request = await authRequest(opts); print( await fetchInteractionTree(request.gateway, request.cookie, { ...request, providerId: provider, }), ); }); options( command .command('enroll ') .description('Bind an authorized runtime session to a durable conversation handle'), ).action( async ( sessionId: string, providerId: string, runtimeSessionId: string, opts: InteractionOptions, ) => { const request = await authRequest(opts); print( await enrollInteractionSession(request.gateway, request.cookie, { ...request, sessionId, providerId, runtimeSessionId, }), ); }, ); options( command .command('attach ') .description('Create a scoped read or write attachment and stream the session'), ) .option('--control', 'Request control mode (provider policy may deny it)') .action(async (sessionId: string, opts: InteractionOptions & { control?: boolean }) => { const request = await authRequest(opts); const attachment = await attachInteractionSession(request.gateway, request.cookie, { ...request, sessionId, mode: opts.control ? 'control' : 'read', }); print(attachment); for await (const event of streamInteractionSession(request.gateway, request.cookie, { ...request, sessionId, })) { print(event); } }); const send = options( command .command('send ') .description('Durably queue and dispatch a message'), ) .option('--idempotency-key ', 'Stable idempotency key') .action( async ( sessionId: string, message: string, opts: InteractionOptions & { idempotencyKey?: string }, ) => { const request = await authRequest(opts); print( await sendInteractionMessage(request.gateway, request.cookie, { ...request, sessionId, content: message, idempotencyKey: opts.idempotencyKey?.trim() || randomUUID(), }), ); }, ); void send; options( command .command('chat ') .description('Send a chat message through the durable interaction session'), ) .option('--idempotency-key ', 'Stable idempotency key') .action( async ( sessionId: string, message: string, opts: InteractionOptions & { idempotencyKey?: string }, ) => { const request = await authRequest(opts); print( await sendInteractionMessage(request.gateway, request.cookie, { ...request, sessionId, content: message, idempotencyKey: opts.idempotencyKey?.trim() || randomUUID(), }), ); }, ); options( command .command('stop ') .description('Terminate a session using a one-time exact-action approval'), ) .requiredOption('--approval ', 'Durable exact-action approval reference') .action(async (sessionId: string, opts: InteractionOptions & { approval: string }) => { const request = await authRequest(opts); print( await stopInteractionSession(request.gateway, request.cookie, { ...request, sessionId, approvalRef: opts.approval, }), ); }); options( command .command('recover ') .description( 'Recover durable inbox/checkpoint state without replaying ambiguous outbox effects', ), ).action(async (sessionId: string, opts: InteractionOptions) => { const request = await authRequest(opts); print( await recoverInteractionSession(request.gateway, request.cookie, { ...request, sessionId }), ); }); return command; }