import { Command } from 'commander'; import * as readline from 'readline'; import chalk from 'chalk'; import ora from 'ora'; import { IndemnClient } from '../sdk/client.js'; import { AgentsSDK } from '../sdk/agents.js'; import { ChatSession } from '../sdk/chat.js'; import { printError } from './utils/output.js'; export function registerChatCommand(program: Command): void { program .command('chat') .argument('', 'Agent ID to chat with') .description('Interactive chat with an agent (or send a single message with --message)') .option('--session ', 'Resume existing session') .option('--message ', 'Send a single message and exit (non-interactive)') .action(async (agentId: string, options) => { try { const client = new IndemnClient(); const agentsSDK = new AgentsSDK(client); const fetchSpinner = ora('Connecting to agent...').start(); const agent = await agentsSDK.getAgent(agentId); const hosts = client.getHosts(); const session = new ChatSession(agent._id, hosts.middleware, options.session); // Track whether we're currently receiving a streamed response let receiving = false; let streamedResponse = false; // Handle stream chunks — write directly to stdout session.on('stream', (data: { text?: string; isFirstChunk?: boolean; streamFinished?: boolean }) => { if (data.isFirstChunk) { receiving = true; streamedResponse = true; process.stdout.write(chalk.cyan('Agent: ')); } if (data.text) { process.stdout.write(chalk.cyan(data.text)); } if (data.streamFinished) { process.stdout.write('\n'); receiving = false; } }); // Handle non-streamed bot messages (skip if already printed via streaming) session.on('message', (text: string) => { if (streamedResponse) { streamedResponse = false; return; } if (!receiving) { console.log(chalk.cyan(`Agent: ${text}`)); } }); session.on('disconnected', (reason: string) => { console.log(chalk.dim(`\nDisconnected: ${reason}`)); process.exit(0); }); // Connect and get session ID const sessionId = await session.connect(); fetchSpinner.stop(); console.log(chalk.dim(`Connected to ${agent.name}`)); console.log(chalk.dim(`Session: ${sessionId}`)); // Non-interactive mode: send one message, wait for response, exit if (options.message) { // Wait for greeting to finish streaming await new Promise((resolve) => { const check = setInterval(() => { if (!receiving) { clearInterval(check); resolve(); } }, 100); // Timeout after 8s even if no greeting setTimeout(() => { clearInterval(check); resolve(); }, 8000); }); console.log(chalk.green(`You: ${options.message}`)); session.send(options.message); // Wait for response to finish streaming await new Promise((resolve) => { let started = false; const check = setInterval(() => { if (receiving) started = true; if (started && !receiving) { clearInterval(check); resolve(); } }, 100); // Timeout after 30s setTimeout(() => { clearInterval(check); resolve(); }, 30000); }); session.disconnect(); process.exit(0); } // Interactive mode console.log(chalk.dim('Type your message and press Enter. Ctrl+C to exit.\n')); const rl = readline.createInterface({ input: process.stdin, output: process.stdout, prompt: chalk.green('You: '), }); rl.prompt(); rl.on('line', (line) => { const message = line.trim(); if (!message) { rl.prompt(); return; } session.send(message); // Wait briefly before showing prompt again to let response start streaming setTimeout(() => { if (!receiving) { rl.prompt(); } else { // Re-prompt after stream finishes const checkDone = setInterval(() => { if (!receiving) { clearInterval(checkDone); rl.prompt(); } }, 100); } }, 200); }); rl.on('close', () => { console.log(chalk.dim('\nClosing session...')); session.disconnect(); process.exit(0); }); // Handle Ctrl+C process.on('SIGINT', () => { console.log(chalk.dim('\nClosing session...')); session.disconnect(); rl.close(); process.exit(0); }); } catch (err) { printError(err); process.exit(1); } }); }