#!/usr/bin/env node /** * Simple terminal chat client for Didactyl HTTP API. * * Usage: * node didactyl-chat-cli.js * * Optional env vars: * DIDACTYL_API_BASE_URL=http://127.0.0.1:8484 * DIDACTYL_MODEL=claude-haiku-4.5 * DIDACTYL_MAX_TURNS=4 */ const readline = require("node:readline/promises"); const { stdin, stdout } = require("node:process"); const http = require("node:http"); const https = require("node:https"); const API_BASE_URL = process.env.DIDACTYL_API_BASE_URL || "https://127.0.0.1:8484"; const MODEL = process.env.DIDACTYL_MODEL || ""; const MAX_TURNS = Number.parseInt(process.env.DIDACTYL_MAX_TURNS || "4", 10); const INSECURE_TLS = !["0", "false", "False", "FALSE"].includes( String(process.env.DIDACTYL_INSECURE_TLS || "1") ); function printMessage(role, content) { const who = role === "user" ? "You" : role === "assistant" ? "Didactyl" : role; console.log(`${who}>`); console.log(content); console.log(""); } function postJson(urlString, payload) { const url = new URL(urlString); const isHttps = url.protocol === "https:"; const data = JSON.stringify(payload); const options = { method: "POST", hostname: url.hostname, port: url.port, path: url.pathname + url.search, headers: { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(data), }, }; if (isHttps) { options.rejectUnauthorized = !INSECURE_TLS; } const client = isHttps ? https : http; return new Promise((resolve, reject) => { const req = client.request(options, (res) => { let body = ""; res.setEncoding("utf8"); res.on("data", (chunk) => { body += chunk; }); res.on("end", () => { resolve({ statusCode: res.statusCode || 0, body, }); }); }); req.on("error", reject); req.write(data); req.end(); }); } async function callDidactyl(message) { const body = { message, max_turns: Number.isFinite(MAX_TURNS) ? MAX_TURNS : 4, }; if (MODEL.trim()) { body.model = MODEL.trim(); } const { statusCode, body: responseBody } = await postJson(`${API_BASE_URL}/api/prompt/agent`, body); if (statusCode < 200 || statusCode >= 300) { throw new Error(`HTTP ${statusCode}: ${responseBody}`); } let data; try { data = JSON.parse(responseBody); } catch { throw new Error(`Invalid JSON from API: ${responseBody}`); } if (!data.success) { throw new Error(data.error || "Didactyl API returned success=false"); } return String(data.final_response || ""); } async function main() { console.log("Didactyl CLI chat"); console.log(`API: ${API_BASE_URL}`); console.log(`TLS verify: ${INSECURE_TLS ? "disabled (local dev)" : "enabled"}`); console.log("Type /exit to quit.\n"); const rl = readline.createInterface({ input: stdin, output: stdout }); try { while (true) { const input = (await rl.question("You> ")).trim(); if (!input) { console.log(""); continue; } if (input === "/exit" || input === "/quit") { console.log("Exiting."); break; } console.log(""); try { const reply = await callDidactyl(input); printMessage("assistant", reply); console.log(""); } catch (err) { const message = err instanceof Error ? err.message : String(err); console.error(`Didactyl error: ${message}`); console.error(""); } } } finally { rl.close(); } } main().catch((err) => { const message = err instanceof Error ? err.message : String(err); console.error(`Fatal error: ${message}`); process.exit(1); });