The runtime serves a bundle its module's state (novox/hq ADR 0201)
mesh/state.get, put, delete, keys and watch on the stdio channel, from the buckets the membership issues. A watch hands the current values without deletions, then every change, and is answered once the current values are delivered. Refused with the reason: state not issued, a reader's write, a value with a credential-named field — the bus alone would answer a refused write with a timeout.
This commit is contained in:
+58
@@ -0,0 +1,58 @@
|
||||
#!/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 } });
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user