#!/usr/bin/env bun /** * Zesdex CLI — main entry point. * Mirrors `apps/gateway/src/main.rs`. * * Parses argv, sets up file logging, and dispatches to the requested mode. * Default mode runs the single-process REPL loop. */ import * as fs from "node:fs"; import * as os from "node:os"; import * as path from "node:path"; import { parseCli, modeCount } from "./args.ts"; import { runSingleProcess } from "./compose.ts"; /** Initialise file logging (append to /zesdex/zesdex.log). */ function initLogging(): void { const dataDir = os.homedir(); const logDir = path.join(dataDir, ".local", "share", "zesdex"); fs.mkdirSync(logDir, { recursive: true }); const logPath = path.join(logDir, "zesdex.log"); const stream = fs.createWriteStream(logPath, { flags: "a" }); const originalLog = console.log; const originalError = console.error; const originalWarn = console.warn; const originalInfo = console.info; const write = (level: string, args: unknown[]) => { const line = `[${new Date().toISOString()}] ${level} ${args .map((a) => (typeof a === "string" ? a : JSON.stringify(a))) .join(" ")}`; stream.write(line + "\n"); if (level === "ERROR") originalError(...args); else if (level === "WARN") originalWarn(...args); else if (level === "INFO") originalInfo(...args); else originalLog(...args); }; console.log = (...a: unknown[]) => write("LOG", a); console.info = (...a: unknown[]) => write("INFO", a); console.warn = (...a: unknown[]) => write("WARN", a); console.error = (...a: unknown[]) => write("ERROR", a); } /** Run the REPL loop (default single-process mode). */ async function runRepl(): Promise { const runtime = await runSingleProcess(); console.info("zesdex ready — single-process REPL mode. Type `exit` to quit."); // Minimal REPL over stdin. // Use node readline. const readline = (await import("node:readline")).default; const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); rl.on("line", async (line: string) => { const text = line.trim(); if (text === "exit" || text === "quit" || text === "/exit") { rl.close(); process.exit(0); } if (text === "" || text.startsWith("/")) { if (text === "/help") { console.log("Commands: type a message to talk to the agent; `exit` to quit."); } return; } const events: import("@zesdex/domain").TurnEvent[] = []; const abort = new AbortController(); const params = buildTurnParamsFrom(runtime, text, events, abort); process.stdout.write("…"); try { await runtime.turnService.runTurn(params); // Print the final assistant content from events. const lastTokens: string[] = []; for (const ev of events) { if (ev.kind === "stream_token") lastTokens.push(ev.content); } const full = lastTokens.join(""); process.stdout.write("\n"); if (full.trim() !== "") console.log(full.trim()); else console.log("(no response content)"); } catch (e) { console.error(`turn failed: ${(e as Error).message}`); } rl.prompt(); }); rl.prompt(); } // Imported lazily to avoid a circular type-only dependency. import { buildTurnParams } from "./compose.ts"; function buildTurnParamsFrom( runtime: Parameters[0], text: string, events: import("@zesdex/domain").TurnEvent[], abort: AbortController, ) { return buildTurnParams(runtime, text, events, abort); } /** Main entry. */ async function main(): Promise { initLogging(); const args = process.argv.slice(2); let parsed; try { parsed = parseCli(args); } catch (e) { console.error((e as Error).message); process.exit(2); } if ("help" in parsed) { console.log(parsed.text); process.exit(0); } const opts = parsed; if (modeCount(opts) > 1) { console.error( "Cannot specify multiple modes: --daemon, --attach, --api, --ws, --grpc, --web are mutually exclusive", ); process.exit(2); } if (opts.daemon) { console.info("daemon mode not yet wired — falling back to REPL"); await runRepl(); } else if (opts.attach !== null) { console.info(`attach mode for session '${opts.attach}' not yet wired`); process.exit(1); } else if (opts.api) { const { newApiState, startApiServer } = await import("@zesdex/api"); const baseDir = process.env.ZESDEX_STORE ?? path.join(os.homedir(), ".local", "share", "zesdex"); const state = newApiState( baseDir, process.env.ZESDEX_JWT_SECRET ?? "dev-secret-change-me", process.env.OPENAI_API_KEY ?? "", process.env.ZESDEX_MODEL ?? "claude-opus-5", process.env.OPENAI_API_BASE ?? undefined, ); startApiServer(state, opts.apiPort); await new Promise(() => {}); } else if (opts.ws) { const { startWsServer } = await import("@zesdex/ws"); startWsServer(opts.wsPort); await new Promise(() => {}); } else if (opts.grpc) { const { startGrpcServer } = await import("@zesdex/grpc"); startGrpcServer(opts.grpcPort); await new Promise(() => {}); } else if (opts.web) { const { startWebServer } = await import("@zesdex/web"); startWebServer(opts.webPort, process.env.ZESDEX_WEB_DIR ?? undefined); await new Promise(() => {}); } else { // Default: single-process REPL. await runRepl(); } } main().catch((e) => { console.error(e instanceof Error ? e.message : String(e)); process.exit(1); });