From c5e9fd6cd6565b55fab1bf2b9d8dacf8ba72a9f4 Mon Sep 17 00:00:00 2001 From: Adam Malczewski Date: Sun, 21 Jun 2026 19:20:10 +0900 Subject: feat(cli): list, read, send commands (Wave 3) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CLI gains three new sub-commands: - dispatch list [--server] — list conversations (short ID + title + activity) - dispatch read [--server] — block until turn settles, print last AI message - dispatch send --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. --- packages/cli/src/args.ts | 125 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 125 insertions(+) (limited to 'packages/cli/src/args.ts') diff --git a/packages/cli/src/args.ts b/packages/cli/src/args.ts index b3fa1e5..b4478b0 100644 --- a/packages/cli/src/args.ts +++ b/packages/cli/src/args.ts @@ -26,6 +26,18 @@ export type ParsedCommand = readonly reasoningEffort?: ReasoningEffort | undefined; readonly showReasoning: boolean; } + | { readonly kind: "list"; readonly server: string; readonly query?: string } + | { readonly kind: "read"; readonly server: string; readonly conversationId: string } + | { + readonly kind: "send"; + readonly server: string; + readonly conversationId: string; + readonly text: string; + readonly queue: boolean; + readonly open: boolean; + readonly cwd?: string; + readonly reasoningEffort?: ReasoningEffort; + } | { readonly kind: "help" } | { readonly kind: "error"; readonly message: string }; @@ -56,6 +68,119 @@ export function parseArgs(argv: readonly string[], opts: ParseOpts): ParsedComma return { kind: "models", server }; } + if (first === "list") { + let server = opts.defaultServer; + let query: string | undefined; + for (let i = 1; i < argv.length; i++) { + const arg = argv[i] as string; + if (arg === "--server") { + if (i + 1 >= argv.length) return { kind: "error", message: "--server requires a value" }; + server = argv[++i] as string; + } else if (arg.startsWith("--")) { + return { kind: "error", message: `Unknown flag: ${arg}` }; + } else if (query !== undefined) { + return { kind: "error", message: `Unexpected argument for 'list': ${arg}` }; + } else { + query = arg; + } + } + return { kind: "list", server, ...(query !== undefined && { query }) }; + } + + if (first === "read") { + let server = opts.defaultServer; + let conversationId: string | undefined; + for (let i = 1; i < argv.length; i++) { + const arg = argv[i] as string; + if (arg === "--server") { + if (i + 1 >= argv.length) return { kind: "error", message: "--server requires a value" }; + server = argv[++i] as string; + } else if (arg.startsWith("--")) { + return { kind: "error", message: `Unknown flag: ${arg}` }; + } else if (conversationId !== undefined) { + return { kind: "error", message: `Unexpected argument for 'read': ${arg}` }; + } else { + conversationId = arg; + } + } + if (conversationId === undefined) { + return { kind: "error", message: "'read' requires a conversation id" }; + } + return { kind: "read", server, conversationId }; + } + + if (first === "send") { + let server = opts.defaultServer; + let conversationId: string | undefined; + let text: string | undefined; + let queue = false; + let open = false; + let cwd: string | undefined; + let reasoningEffort: ReasoningEffort | undefined; + + for (let i = 1; i < argv.length; i++) { + const arg = argv[i] as string; + switch (arg) { + case "--server": + if (i + 1 >= argv.length) return { kind: "error", message: "--server requires a value" }; + server = argv[++i] as string; + break; + case "--text": + if (i + 1 >= argv.length) return { kind: "error", message: "--text requires a value" }; + text = argv[++i]; + break; + case "--queue": + queue = true; + break; + case "--open": + open = true; + break; + case "--cwd": + if (i + 1 >= argv.length) return { kind: "error", message: "--cwd requires a value" }; + cwd = argv[++i]; + break; + case "--effort": { + if (i + 1 >= argv.length) + return { + kind: "error", + message: `--effort requires a value (one of: ${VALID_EFFORTS.join(", ")})`, + }; + const val = argv[++i] as string; + if (!isValidEffort(val)) + return { + kind: "error", + message: `Invalid effort level "${val}". Must be one of: ${VALID_EFFORTS.join(", ")}`, + }; + reasoningEffort = val; + break; + } + default: + if (arg.startsWith("--")) return { kind: "error", message: `Unknown flag: ${arg}` }; + if (conversationId !== undefined) + return { kind: "error", message: `Unexpected argument for 'send': ${arg}` }; + conversationId = arg; + } + } + + if (conversationId === undefined) { + return { kind: "error", message: "'send' requires a conversation id" }; + } + if (text === undefined) { + return { kind: "error", message: "'send' requires --text" }; + } + + return { + kind: "send", + server, + conversationId, + text, + queue, + open, + ...(cwd !== undefined && { cwd }), + ...(reasoningEffort !== undefined && { reasoningEffort }), + }; + } + // Chat mode: first arg is the model name const modelName = first; let text: string | undefined; -- cgit v1.2.3