#!/usr/bin/env node
import readline from 'node:readline';
import { pathToFileURL } from 'node:url';
import type { Readable, Writable } from 'node:stream';
import { loadConfig } from '../config.js';
import { validateGetJokeInput } from '../validation.js';
import type { Category, Lang } from '../validation.js';
import { createJokeAgent, JokeResolutionError } from '../agents/jokes-mcp.agent.js';
import type { JokeAgent, JokeResponse } from '../agents/jokes-mcp.agent.js';
export interface CliOptions {
input?: Readable;
output?: Writable;
env?: NodeJS.ProcessEnv;
}
interface CliRuntime {
agent: JokeAgent;
respond: (payload: unknown) => void;
defaults: { defaultCategory: Category; lang: Lang };
}
export function startCli(options: CliOptions = {}): { close: () => void; closed: Promise<void> } {
const input = options.input ?? process.stdin;
const output = options.output ?? process.stdout;
const env = options.env ?? process.env;
const config = loadConfig(env);
const agent = createJokeAgent({
provider: config.provider,
timeoutMs: config.timeoutMs,
retries: config.retries,
allowNet: config.allowNet,
verbose: config.verbose,
});
const runtime: CliRuntime = {
agent,
respond: (payload: unknown) => {
output.write(`${JSON.stringify(payload)}\n`);
},
defaults: {
defaultCategory: config.defaultCategory,
lang: config.lang,
},
};
const rl = readline.createInterface({ input, output, terminal: false });
const closed = new Promise<void>((resolve) => {
rl.on('close', () => resolve());
});
rl.on('line', (line) => {
void handleCommand(line, runtime);
});
input.on('end', () => {
rl.close();
});
const close = () => rl.close();
if (input === process.stdin) {
process.on('SIGINT', () => {
rl.close();
});
}
return { close, closed };
}
async function handleCommand(rawLine: string, runtime: CliRuntime): Promise<void> {
const trimmed = rawLine.trim();
if (!trimmed) return;
if (trimmed === 'health') {
runtime.respond({ ok: true });
return;
}
if (trimmed.startsWith('getJoke')) {
const payloadText = extractPayload(trimmed);
try {
const payload = payloadText ? JSON.parse(payloadText) : undefined;
const preferences = validateGetJokeInput(payload, runtime.defaults);
const result = await runtime.agent.resolve(preferences);
runtime.respond(toSuccessResponse(result));
} catch (error) {
runtime.respond(toResponseError(error));
}
return;
}
runtime.respond({ ok: false, error: `Unknown command: ${trimmed}` });
}
function extractPayload(commandLine: string): string {
const firstSpace = commandLine.indexOf(' ');
if (firstSpace === -1) return '';
return commandLine.slice(firstSpace + 1).trim();
}
function toSuccessResponse(result: JokeResponse): JokeResponse {
return result;
}
function toResponseError(error: unknown): { ok: false; error: string; details?: unknown } {
if (error instanceof JokeResolutionError) {
return { ok: false, error: error.message, details: error.details };
}
const message = error instanceof Error ? error.message : String(error ?? 'Unknown error');
return { ok: false, error: message };
}
if (import.meta.url === pathToFileURL(process.argv[1] ?? '').href) {
const { closed } = startCli();
closed.catch((error) => {
const message = error instanceof Error ? error.message : String(error);
process.stderr.write(`${JSON.stringify({ level: 'error', message })}\n`);
});
}