59 lines
2.6 KiB
JavaScript
Executable File
59 lines
2.6 KiB
JavaScript
Executable File
#!/usr/bin/env node
|
|
// A bundle that keeps and reads state through the runtime (novox/hq ADR 0201), written against the
|
|
// protocol with no SDK: on start it watches STATE_NAME and writes every change it is handed to
|
|
// STATE_LOG; its tools put, get, delete and list keys of whichever state they name.
|
|
import { appendFileSync } from "node:fs";
|
|
import { createInterface } from "node:readline";
|
|
|
|
const say = (m) => process.stdout.write(JSON.stringify({ jsonrpc: "2.0", ...m }) + "\n");
|
|
let next = 1;
|
|
const waiting = new Map();
|
|
const ask = (method, params) => new Promise((resolve, reject) => {
|
|
const id = `keeper-${next++}`;
|
|
waiting.set(id, { resolve, reject });
|
|
say({ id, method, params });
|
|
});
|
|
const log = (line) => appendFileSync(process.env.STATE_LOG, line + "\n");
|
|
const text = (v) => ({ content: [{ type: "text", text: JSON.stringify(v ?? null) }] });
|
|
const tool = (name) => ({ name, description: name, inputSchema: { type: "object", properties: {} } });
|
|
const TOOLS = ["put", "get", "del", "keys"].map(tool);
|
|
|
|
createInterface({ input: process.stdin }).on("line", async (line) => {
|
|
const m = JSON.parse(line);
|
|
if (m.method === undefined && waiting.has(m.id)) {
|
|
const w = waiting.get(m.id); waiting.delete(m.id);
|
|
m.error ? w.reject(new Error(m.error.message)) : w.resolve(m.result);
|
|
return;
|
|
}
|
|
const p = m.params ?? {};
|
|
switch (m.method) {
|
|
case "initialize":
|
|
say({ id: m.id, result: { protocolVersion: "2025-03-26", capabilities: { tools: {} }, serverInfo: { name: "keeper", version: "1" } } });
|
|
// Watched once the runtime has the handshake: the current values, then every change.
|
|
ask("mesh/state.watch", { state: process.env.STATE_NAME })
|
|
.then(() => log("current"), (e) => log("watch refused: " + e.message));
|
|
return;
|
|
case "tools/list":
|
|
say({ id: m.id, result: { tools: TOOLS } });
|
|
return;
|
|
case "mesh/state": {
|
|
const c = p.change;
|
|
log(`${c.current ? "was" : "now"} ${c.op} ${c.key}${c.value !== undefined ? " " + JSON.stringify(c.value) : ""}`);
|
|
say({ id: m.id, result: {} });
|
|
return;
|
|
}
|
|
case "tools/call": {
|
|
const a = p.arguments ?? {};
|
|
const verb = { put: "put", get: "get", del: "delete", keys: "keys" }[p.name];
|
|
try {
|
|
say({ id: m.id, result: text(await ask(`mesh/state.${verb}`, { state: a.state, key: a.key, value: a.value })) });
|
|
} catch (e) {
|
|
say({ id: m.id, result: { content: [{ type: "text", text: e.message }], isError: true } });
|
|
}
|
|
return;
|
|
}
|
|
default:
|
|
if (m.id !== undefined) say({ id: m.id, error: { code: -32601, message: "no " + m.method } });
|
|
}
|
|
});
|