diff options
| author | Adam Malczewski <[email protected]> | 2026-06-21 19:20:10 +0900 |
|---|---|---|
| committer | Adam Malczewski <[email protected]> | 2026-06-21 19:20:10 +0900 |
| commit | c5e9fd6cd6565b55fab1bf2b9d8dacf8ba72a9f4 (patch) | |
| tree | 809bd93eaa25646f237fb4b1ddd3719e25aaca90 /packages/cli/src/main.ts | |
| parent | ea0e938eca3072649dc8707c999ec00cf87b986a (diff) | |
| download | dispatch-c5e9fd6cd6565b55fab1bf2b9d8dacf8ba72a9f4.tar.gz dispatch-c5e9fd6cd6565b55fab1bf2b9d8dacf8ba72a9f4.zip | |
feat(cli): list, read, send commands (Wave 3)
CLI gains three new sub-commands:
- dispatch list [--server] — list conversations (short ID + title + activity)
- dispatch read <id> [--server] — block until turn settles, print last AI message
- dispatch send <id> --text [--queue] [--open] [--cwd] [--effort] [--server]
- Default: blocking (consumes NDJSON stream, prints accumulated text + conv ID)
- --queue: non-blocking (POST /conversations/:id/queue, exit immediately)
- --open: signals frontend to open the conversation tab (POST /conversations/:id/open)
Short-ID resolution: 4+ char prefix → GET /conversations?q= → resolve to full ID.
32+ char input is treated as a full UUID (no resolution). Errors on 0 or >1 matches.
48 new tests (108 total in cli). Pure arg parser + HTTP client functions, zero vi.mock.
Diffstat (limited to 'packages/cli/src/main.ts')
| -rw-r--r-- | packages/cli/src/main.ts | 93 |
1 files changed, 91 insertions, 2 deletions
diff --git a/packages/cli/src/main.ts b/packages/cli/src/main.ts index bf4f603..dd8cfa8 100644 --- a/packages/cli/src/main.ts +++ b/packages/cli/src/main.ts @@ -8,12 +8,23 @@ import { readFile } from "node:fs/promises"; import { parseArgs } from "./args.js"; import { formatCatalog } from "./catalog.js"; -import { fetchModels, streamChat } from "./http.js"; +import { + enqueueMessage, + fetchConversations, + fetchLastMessage, + fetchModels, + openConversation, + resolveConversationId, + streamChat, +} from "./http.js"; import { buildChatRequest, composeMessage } from "./message.js"; -import { renderEvent } from "./render.js"; +import { extractLastText, formatConversationList, renderEvent } from "./render.js"; const USAGE = `Usage: dispatch models [--server <url>] + dispatch list [<prefix>] [--server <url>] + dispatch read <conversationId> [--server <url>] + dispatch send <conversationId> --text "..." [--queue] [--open] [--cwd <dir>] [--effort <level>] [--server <url>] dispatch <modelName> --text "..." [--file <path>] [--cwd <dir>] [--conversation <id>] [--effort <level>] [--server <url>] [--show-reasoning] dispatch --help @@ -37,6 +48,84 @@ async function main(): Promise<void> { process.stdout.write(`${formatCatalog(result)}\n`); break; } + case "list": { + const result = await fetchConversations( + { fetchImpl: globalThis.fetch }, + { server: parsed.server, ...(parsed.query !== undefined && { query: parsed.query }) }, + ); + const table = formatConversationList(result.conversations, Date.now()); + if (table.length > 0) process.stdout.write(`${table}\n`); + break; + } + case "read": { + const resolved = await resolveConversationId( + { fetchImpl: globalThis.fetch }, + { server: parsed.server, shortId: parsed.conversationId }, + ); + if (typeof resolved !== "string") { + process.stderr.write(`${resolved.error}\n`); + process.exit(1); + } + const last = await fetchLastMessage( + { fetchImpl: globalThis.fetch }, + { server: parsed.server, conversationId: resolved }, + ); + if (last.content.length > 0) process.stdout.write(`${last.content}\n`); + break; + } + case "send": { + const resolved = await resolveConversationId( + { fetchImpl: globalThis.fetch }, + { server: parsed.server, shortId: parsed.conversationId }, + ); + if (typeof resolved !== "string") { + process.stderr.write(`${resolved.error}\n`); + process.exit(1); + } + const conversationId = resolved; + + if (parsed.queue) { + const queued = await enqueueMessage( + { fetchImpl: globalThis.fetch }, + { server: parsed.server, conversationId, text: parsed.text }, + ); + const line = queued.startedTurn + ? `Started turn for ${conversationId}` + : `Queued to ${conversationId}`; + process.stdout.write(`${line}\n`); + } else { + const request = { + conversationId, + message: parsed.text, + ...(parsed.cwd !== undefined && { cwd: parsed.cwd }), + ...(parsed.reasoningEffort !== undefined && { reasoningEffort: parsed.reasoningEffort }), + }; + const { events } = await streamChat( + { fetchImpl: globalThis.fetch }, + { server: parsed.server, request }, + ); + const collected = []; + for await (const event of events) { + if (event.type === "error") { + process.stderr.write(`${event.message}\n`); + process.exit(1); + } + collected.push(event); + if (event.type === "done") break; + } + process.stdout.write(`${extractLastText(collected)}\n`); + process.stdout.write(`[conversation] ${conversationId}\n`); + } + + if (parsed.open) { + await openConversation( + { fetchImpl: globalThis.fetch }, + { server: parsed.server, conversationId }, + ); + process.stdout.write(`Signaled frontend to open ${conversationId}\n`); + } + break; + } case "chat": { let fileContent: string | undefined; if (parsed.file) { |
