import type { Command } from "commander"; import { connectToDaemon, getDaemonHost } from "../../utils/client.js"; import type { CommandOptions, SingleResult, OutputSchema, CommandError, } from "../../output/index.js"; import { readFile } from "node:fs/promises"; import { extname, resolve } from "node:path"; /** Result type for agent send command */ export interface AgentSendResult { agentId: string; status: "sent" | "completed" | "timeout" | "permission" | "error"; message: string; } /** Schema for agent send output */ export const agentSendSchema: OutputSchema = { idField: "agentId", columns: [ { header: "AGENT ID", field: "agentId", width: 12 }, { header: "STATUS", field: "status", width: 12 }, { header: "MESSAGE", field: "message", width: 40 }, ], }; export interface AgentSendOptions extends CommandOptions { noWait?: boolean; image?: string[]; prompt?: string; promptFile?: string; } /** * Read image files and convert them to base64 data URIs */ async function readImageFiles( imagePaths: string[], ): Promise> { const images: Array<{ data: string; mimeType: string }> = []; for (const path of imagePaths) { try { const buffer = await readFile(path); const ext = extname(path).toLowerCase(); // Determine media type from extension let mimeType = "image/jpeg"; switch (ext) { case ".png": mimeType = "image/png"; break; case ".jpg": case ".jpeg": mimeType = "image/jpeg"; break; case ".gif": mimeType = "image/gif"; break; case ".webp": mimeType = "image/webp"; break; default: // Default to jpeg for unknown types mimeType = "image/jpeg"; } const data = buffer.toString("base64"); images.push({ data, mimeType, }); } catch (err) { const message = err instanceof Error ? err.message : String(err); const error: CommandError = { code: "IMAGE_READ_ERROR", message: `Failed to read image file: ${path}`, details: message, }; throw error; } } return images; } async function resolvePromptInput(options: { promptArgument: string | undefined; promptOption: string | undefined; promptFile: string | undefined; }): Promise { const promptText = options.promptArgument?.trim(); const promptOptionText = options.promptOption?.trim(); const promptFilePath = options.promptFile?.trim(); const providedSourceCount = [promptText, promptOptionText, promptFilePath].filter(Boolean).length; if (providedSourceCount > 1) { const error: CommandError = { code: "CONFLICTING_PROMPT_INPUT", message: "Provide exactly one of prompt argument, --prompt, or --prompt-file", }; throw error; } if (promptText) { return options.promptArgument as string; } if (promptOptionText) { return options.promptOption as string; } if (!promptFilePath) { const error: CommandError = { code: "MISSING_PROMPT", message: "A prompt is required", details: "Usage: paseo agent send [options] [prompt] | --prompt | --prompt-file ", }; throw error; } try { return await readFile(resolve(promptFilePath), "utf8"); } catch (err) { const message = err instanceof Error ? err.message : String(err); const error: CommandError = { code: "PROMPT_FILE_READ_ERROR", message: `Failed to read prompt file: ${promptFilePath}`, details: message, }; throw error; } } export async function runSendCommand( agentIdArg: string, prompt: string | undefined, options: AgentSendOptions, _command: Command, ): Promise> { const host = getDaemonHost({ host: options.host as string | undefined }); // Validate arguments if (!agentIdArg || agentIdArg.trim().length === 0) { const error: CommandError = { code: "MISSING_AGENT_ID", message: "Agent ID is required", details: "Usage: paseo agent send [options] [prompt]", }; throw error; } const promptInput = await resolvePromptInput({ promptArgument: prompt, promptOption: options.prompt, promptFile: options.promptFile, }); let client; try { client = await connectToDaemon({ host: options.host as string | undefined }); } catch (err) { const message = err instanceof Error ? err.message : String(err); const error: CommandError = { code: "DAEMON_NOT_RUNNING", message: `Cannot connect to daemon at ${host}: ${message}`, details: "Start the daemon with: paseo daemon start", }; throw error; } try { // Read image files if provided const images = options.image && options.image.length > 0 ? await readImageFiles(options.image) : undefined; // Send the message await client.sendAgentMessage(agentIdArg, promptInput, { images }); // If --no-wait, return immediately if (options.noWait) { await client.close(); return { type: "single", data: { agentId: agentIdArg, status: "sent", message: "Message sent, not waiting for completion", }, schema: agentSendSchema, }; } // Wait for agent to finish const state = await client.waitForFinish(agentIdArg, 600000); // 10 minute timeout await client.close(); if (state.status === "timeout") { return { type: "single", data: { agentId: state.final?.id ?? agentIdArg, status: "timeout", message: "Timed out waiting for agent to finish", }, schema: agentSendSchema, }; } if (state.status === "permission") { return { type: "single", data: { agentId: state.final?.id ?? agentIdArg, status: "permission", message: "Agent is waiting for permission", }, schema: agentSendSchema, }; } if (state.status === "error") { return { type: "single", data: { agentId: state.final?.id ?? agentIdArg, status: "error", message: state.error ?? "Agent finished with error", }, schema: agentSendSchema, }; } return { type: "single", data: { agentId: state.final?.id ?? agentIdArg, status: "completed", message: "Agent completed processing the message", }, schema: agentSendSchema, }; } catch (err) { await client.close().catch(() => {}); // Re-throw CommandError as-is if (err && typeof err === "object" && "code" in err) { throw err; } const message = err instanceof Error ? err.message : String(err); const error: CommandError = { code: "SEND_FAILED", message: `Failed to send message: ${message}`, }; throw error; } }