170 lines
5.4 KiB
TypeScript
170 lines
5.4 KiB
TypeScript
#!/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 <data-dir>/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<void> {
|
|
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<typeof buildTurnParams>[0],
|
|
text: string,
|
|
events: import("@zesdex/domain").TurnEvent[],
|
|
abort: AbortController,
|
|
) {
|
|
return buildTurnParams(runtime, text, events, abort);
|
|
}
|
|
|
|
/** Main entry. */
|
|
async function main(): Promise<void> {
|
|
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);
|
|
});
|