zot/examples/rpc/node/zot-client.js
patriceckhart a670442c9e feat: zotcore SDK + zot rpc subprocess protocol
two new ways to embed the zot agent runtime in third-party apps:

1. pkg/zotcore - public Go SDK
   - Runtime type: New(Config), Prompt(ctx,text,imgs)->chan Event,
     Cancel, Compact, SetModel, State, Messages, Cost, ListModels,
     Close. Concurrent-safe; one prompt at a time per Runtime,
     ErrBusy if you try to overlap. Spawn multiple Runtimes for
     multiple projects.
   - Public types mirror the JSON-RPC wire schema 1:1 so consumers
     can share parsing code with the out-of-process clients.
   - Internal core/agent/provider stay internal; SDK is a thin
     facade that exposes only what's stable.

2. zot rpc subcommand - newline-delimited JSON on stdin/stdout
   - 'zot rpc' (or 'zot --rpc') turns the agent runtime into a
     subprocess that any language can drive via pipes.
   - Commands: hello, prompt, abort, compact, get_state,
     get_messages, clear, set_model, get_models, ping. Each
     optionally carries an id; the matching response echoes it.
   - Stream notifications: turn_start, user_message,
     assistant_start, text_delta, tool_call, tool_progress,
     tool_result, assistant_message, usage, turn_end, done,
     error, compact_done. Same shape as the existing --json mode
     events (modes.EventToJSON / ContentToJSON were exported
     for reuse).
   - Auth: optional ZOTCORE_RPC_TOKEN env var; first command
     must be hello {token: ...} when set. Without the env var
     the spawning process is implicitly trusted.
   - Concurrency: one prompt or compact at a time per process,
     enforced by a turnMu mutex. abort fires immediately
     regardless. Stdin close exits the process.

3. docs/rpc.md - full schema reference
4. examples/rpc/{python,node,shell,go} - reference clients
5. examples/sdk - in-process Go embedding example
6. README updated with a new modes entry and an embedding section
2026-04-19 12:26:48 +02:00

106 lines
2.6 KiB
JavaScript

// Minimal Node client for the `zot rpc` JSON protocol.
//
// Usage:
// node zot-client.js "fix the failing test"
//
// Spawns `zot rpc`, sends one prompt, prints assistant text as it
// streams, and exits when the turn finishes. Pure stdlib — no
// dependencies. See docs/rpc.md for the full schema.
"use strict";
const { spawn } = require("node:child_process");
const readline = require("node:readline");
const { randomBytes } = require("node:crypto");
class ZotClient {
constructor(...flags) {
this.proc = spawn("zot", ["rpc", ...flags], {
stdio: ["pipe", "pipe", "inherit"],
env: process.env,
});
this.rl = readline.createInterface({
input: this.proc.stdout,
crlfDelay: Infinity,
});
}
send(command) {
if (!command.id) command.id = randomBytes(4).toString("hex");
this.proc.stdin.write(JSON.stringify(command) + "\n");
return command.id;
}
async *events() {
for await (const line of this.rl) {
const trimmed = line.trim();
if (!trimmed) continue;
try {
yield JSON.parse(trimmed);
} catch {
// ignore garbled lines
}
}
}
close() {
try {
this.proc.stdin.end();
} catch {}
}
}
async function main() {
const prompt = process.argv.slice(2).join(" ");
if (!prompt) {
console.error("usage: node zot-client.js <prompt>");
process.exit(2);
}
const client = new ZotClient();
const token = process.env.ZOTCORE_RPC_TOKEN;
if (token) client.send({ type: "hello", token });
client.send({ type: "prompt", message: prompt });
let exitCode = 0;
try {
for await (const ev of client.events()) {
switch (ev.type) {
case "text_delta":
process.stdout.write(ev.delta || "");
break;
case "tool_call":
process.stderr.write(
`\n[tool] ${ev.name}(${JSON.stringify(ev.args || {})})`,
);
break;
case "tool_result":
if (ev.is_error) process.stderr.write("\n[tool error]");
break;
case "usage": {
const cum = ev.cumulative || {};
process.stderr.write(
`\n[usage] cum input=${cum.input} output=${cum.output} cost=$${(cum.cost_usd || 0).toFixed(4)}`,
);
break;
}
case "error":
process.stderr.write(`\n[error] ${ev.message}\n`);
exitCode = 1;
break;
case "done":
process.stdout.write("\n");
client.close();
process.exit(exitCode);
}
}
} finally {
client.close();
}
}
main().catch((e) => {
console.error(e);
process.exit(1);
});