summaryrefslogtreecommitdiffhomepage
path: root/packages/cli/src/main.ts
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-06-21 19:20:10 +0900
committerAdam Malczewski <[email protected]>2026-06-21 19:20:10 +0900
commitc5e9fd6cd6565b55fab1bf2b9d8dacf8ba72a9f4 (patch)
tree809bd93eaa25646f237fb4b1ddd3719e25aaca90 /packages/cli/src/main.ts
parentea0e938eca3072649dc8707c999ec00cf87b986a (diff)
downloaddispatch-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.ts93
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) {