From d4b470ca582e3c69c40438895b90748d44fc4653 Mon Sep 17 00:00:00 2001 From: Adam Malczewski Date: Thu, 25 Jun 2026 10:30:41 +0900 Subject: feat(cli): add --file flag to 'dispatch send' subcommand Add the same --file support that the summon (chat) command has to the 'dispatch send' subcommand. When --file is given, the file's contents are read and attached to the message (composed via composeMessage, identical to chat). - args.ts: add 'file' to the send ParsedCommand, make 'text' optional, parse --file, and require at least one of --text or --file. - main.ts: read the file and compose the message in the send case, using the composed message in both the --queue and streaming branches; update USAGE. - args.test.ts: cover --file parsing (alone, with --text, missing value) and update the existing send expectations + the both-missing error message. --- packages/cli/src/args.test.ts | 33 +++++++++++++++++++++++++++++++-- packages/cli/src/args.ts | 16 +++++++++++++--- packages/cli/src/main.ts | 16 +++++++++++++--- 3 files changed, 57 insertions(+), 8 deletions(-) diff --git a/packages/cli/src/args.test.ts b/packages/cli/src/args.test.ts index 3d07c96..7a45c02 100644 --- a/packages/cli/src/args.test.ts +++ b/packages/cli/src/args.test.ts @@ -320,11 +320,31 @@ describe("parseArgs", () => { server: "http://localhost:24203", conversationId: "deadbeef", text: "hi", + file: undefined, + queue: false, + open: false, + }); + }); + + it("parses 'send' with --file", () => { + expect(parseArgs(["send", "deadbeef", "--file", "foo.txt"], { defaultServer })).toEqual({ + kind: "send", + server: "http://localhost:24203", + conversationId: "deadbeef", + text: undefined, + file: "foo.txt", queue: false, open: false, }); }); + it("parses 'send' with both --text and --file", () => { + const result = parseArgs(["send", "deadbeef", "--text", "hi", "--file", "f.txt"], { + defaultServer, + }); + expect(result).toMatchObject({ kind: "send", text: "hi", file: "f.txt" }); + }); + it("parses 'send' with --queue", () => { const result = parseArgs(["send", "deadbeef", "--text", "hi", "--queue"], { defaultServer, @@ -334,6 +354,7 @@ describe("parseArgs", () => { server: "http://localhost:24203", conversationId: "deadbeef", text: "hi", + file: undefined, queue: true, open: false, }); @@ -348,6 +369,7 @@ describe("parseArgs", () => { server: "http://localhost:24203", conversationId: "deadbeef", text: "hi", + file: undefined, queue: false, open: true, }); @@ -363,6 +385,7 @@ describe("parseArgs", () => { server: "http://localhost:24203", conversationId: "deadbeef", text: "hi", + file: undefined, queue: false, open: false, cwd: "/tmp", @@ -370,10 +393,10 @@ describe("parseArgs", () => { }); }); - it("requires --text", () => { + it("errors when --text and --file are both missing", () => { const result = parseArgs(["send", "deadbeef"], { defaultServer }); expect(result.kind).toBe("error"); - if (result.kind === "error") expect(result.message).toContain("--text"); + if (result.kind === "error") expect(result.message).toContain("--text or --file"); }); it("requires a conversation id", () => { @@ -386,6 +409,12 @@ describe("parseArgs", () => { const result = parseArgs(["send", "deadbeef", "--text"], { defaultServer }); expect(result.kind).toBe("error"); }); + + it("errors when --file has no value", () => { + const result = parseArgs(["send", "deadbeef", "--file"], { defaultServer }); + expect(result.kind).toBe("error"); + if (result.kind === "error") expect(result.message).toContain("--file requires a value"); + }); }); describe("open", () => { diff --git a/packages/cli/src/args.ts b/packages/cli/src/args.ts index 8a63777..aaad2de 100644 --- a/packages/cli/src/args.ts +++ b/packages/cli/src/args.ts @@ -42,7 +42,8 @@ export type ParsedCommand = readonly kind: "send"; readonly server: string; readonly conversationId: string; - readonly text: string; + readonly text?: string | undefined; + readonly file?: string | undefined; readonly queue: boolean; readonly open: boolean; readonly cwd?: string; @@ -204,6 +205,7 @@ export function parseArgs(argv: readonly string[], opts: ParseOpts): ParsedComma let server = opts.defaultServer; let conversationId: string | undefined; let text: string | undefined; + let file: string | undefined; let queue = false; let open = false; let cwd: string | undefined; @@ -221,6 +223,10 @@ export function parseArgs(argv: readonly string[], opts: ParseOpts): ParsedComma if (i + 1 >= argv.length) return { kind: "error", message: "--text requires a value" }; text = argv[++i]; break; + case "--file": + if (i + 1 >= argv.length) return { kind: "error", message: "--file requires a value" }; + file = argv[++i]; + break; case "--queue": queue = true; break; @@ -263,8 +269,11 @@ export function parseArgs(argv: readonly string[], opts: ParseOpts): ParsedComma if (conversationId === undefined) { return { kind: "error", message: "'send' requires a conversation id" }; } - if (text === undefined) { - return { kind: "error", message: "'send' requires --text" }; + if (!text && !file) { + return { + kind: "error", + message: "At least one of --text or --file is required for 'send'", + }; } return { @@ -272,6 +281,7 @@ export function parseArgs(argv: readonly string[], opts: ParseOpts): ParsedComma server, conversationId, text, + file, queue, open, ...(cwd !== undefined && { cwd }), diff --git a/packages/cli/src/main.ts b/packages/cli/src/main.ts index 9dfc317..b61be07 100644 --- a/packages/cli/src/main.ts +++ b/packages/cli/src/main.ts @@ -29,7 +29,7 @@ const USAGE = `Usage: dispatch compact [--server ] dispatch read [--server ] dispatch open [--server ] - dispatch send --text "..." [--queue] [--open] [--cwd ] [--effort ] [--workspace ] [--server ] + dispatch send --text "..." [--file ] [--queue] [--open] [--cwd ] [--effort ] [--workspace ] [--server ] dispatch --text "..." [--file ] [--cwd ] [--conversation ] [--effort ] [--workspace ] [--server ] [--show-reasoning] [--open] dispatch --help @@ -156,10 +156,20 @@ async function main(): Promise { process.stdout.write(`Signaled frontend to open ${conversationId}\n`); } + let fileContent: string | undefined; + if (parsed.file) { + fileContent = await readFile(parsed.file, "utf-8"); + } + const message = composeMessage({ + ...(parsed.text !== undefined && { text: parsed.text }), + ...(parsed.file !== undefined && { file: parsed.file }), + ...(fileContent !== undefined && { fileContent }), + }); + if (parsed.queue) { const queued = await enqueueMessage( { fetchImpl: globalThis.fetch }, - { server: parsed.server, conversationId, text: parsed.text }, + { server: parsed.server, conversationId, text: message }, ); const line = queued.startedTurn ? `Started turn for ${conversationId}` @@ -168,7 +178,7 @@ async function main(): Promise { } else { const request = { conversationId, - message: parsed.text, + message, ...(parsed.cwd !== undefined && { cwd: parsed.cwd }), ...(parsed.reasoningEffort !== undefined && { reasoningEffort: parsed.reasoningEffort }), ...(parsed.workspaceId !== undefined && { workspaceId: parsed.workspaceId }), -- cgit v1.2.3