import { appendFileSync, readFileSync } from "node:fs"; import { join } from "node:path"; import { apiFetch } from "../api.mjs"; import { emit } from "../output.mjs"; import { t } from "../i18n.mjs"; import { resolveDataDir } from "../data-dir.mjs"; function resolveHistoryPath() { return join(resolveDataDir(), "cli-history.jsonl"); } export function registerChat(program) { program .command("chat [prompt]") .description(t("chat.description")) .option("--file ", t("chat.file")) .option("--stdin", t("chat.stdin")) .option("-s, --system ", t("chat.system")) .option("-m, --model ", t("chat.model"), "auto") .option("--max-tokens ", t("chat.max_tokens"), parseInt) .option("--temperature ", t("chat.temperature"), parseFloat) .option("--top-p

", t("chat.top_p"), parseFloat) .option("--reasoning-effort ", t("chat.reasoning_effort")) .option("--thinking-budget ", t("chat.thinking_budget"), parseInt) .option("--combo ", t("chat.combo")) .option("--responses-api", t("chat.responses_api")) .option("--stream", t("chat.stream")) .option("--no-history", t("chat.no_history")) .action(runChatCommand); } export async function runChatCommand(promptArg, opts, cmd) { const globalOpts = cmd.optsWithGlobals(); const prompt = await resolvePrompt(promptArg, opts); if (!prompt) { process.stderr.write(t("chat.error.empty_prompt") + "\n"); process.exit(2); } const messages = []; if (opts.system) messages.push({ role: "system", content: opts.system }); messages.push({ role: "user", content: prompt }); const body = { model: opts.model, messages, ...(opts.maxTokens && { max_tokens: opts.maxTokens }), ...(opts.temperature != null && { temperature: opts.temperature }), ...(opts.topP != null && { top_p: opts.topP }), ...(opts.reasoningEffort && { reasoning_effort: opts.reasoningEffort }), ...(opts.thinkingBudget && { thinking: { budget_tokens: opts.thinkingBudget } }), ...(opts.combo && { combo: opts.combo }), stream: !!opts.stream, }; const endpoint = opts.responsesApi ? "/v1/responses" : "/v1/chat/completions"; const startedAt = Date.now(); const response = await apiFetch(endpoint, { method: "POST", body, acceptNotOk: true, timeout: globalOpts.timeout, }); if (!response.ok) { const errText = await response.text().catch(() => ""); process.stderr.write(`\x1b[31m✖ ${response.status} ${response.statusText}\x1b[0m\n`); if (errText) process.stderr.write(errText + "\n"); process.exit(1); } const latencyMs = Date.now() - startedAt; if (opts.stream) { return streamHandle(response, opts.responsesApi); } const data = await response.json(); const text = extractText(data, opts.responsesApi); if (!opts.noHistory) { appendHistory({ prompt, model: opts.model, latencyMs, usage: data.usage, response: text }); } if (globalOpts.output === "json") { emit(data, globalOpts); } else if (globalOpts.output === "markdown") { console.log( `# Response\n\n${text}\n\n## Metadata\n- Model: ${data.model}\n- Latency: ${latencyMs}ms\n- Usage: ${JSON.stringify(data.usage)}\n` ); } else { console.log(text); if (!globalOpts.quiet) { process.stderr.write( `\n[${data.model} · ${latencyMs}ms · ${data.usage?.total_tokens ?? "?"} tok]\n` ); } } } async function resolvePrompt(arg, opts) { if (opts.file) return readFileSync(opts.file, "utf8").trim(); if (opts.stdin) return readStdin(); return arg?.trim() || ""; } function readStdin() { return new Promise((resolve) => { let buf = ""; process.stdin.setEncoding("utf8"); process.stdin.on("data", (c) => (buf += c)); process.stdin.on("end", () => resolve(buf.trim())); }); } function extractText(data, isResponses) { if (isResponses) { return data.output?.[0]?.content?.[0]?.text ?? data.output_text ?? ""; } return data.choices?.[0]?.message?.content ?? ""; } async function streamHandle(response, isResponses) { const reader = response.body.getReader(); const decoder = new TextDecoder(); let buffer = ""; while (true) { const { done, value } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); const lines = buffer.split("\n"); buffer = lines.pop() ?? ""; for (const line of lines) { if (!line.startsWith("data: ")) continue; const chunk = line.slice(6).trim(); if (chunk === "[DONE]") { process.stdout.write("\n"); return; } try { const obj = JSON.parse(chunk); const content = isResponses ? obj.delta?.content : obj.choices?.[0]?.delta?.content; if (content) process.stdout.write(content); } catch {} } } process.stdout.write("\n"); } function appendHistory(entry) { try { appendFileSync( resolveHistoryPath(), JSON.stringify({ ts: new Date().toISOString(), ...entry }) + "\n" ); } catch { // history write failures are non-fatal } }