Files
didactyl/chat-didactyl-cli.js

151 lines
3.6 KiB
JavaScript
Executable File

#!/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);
});