Files
codingetandClaude a5f62512e8 initial implementation: brain-dump session server with dual LLM backends
Headless Koa v3 server exposing REST + SSE around the brain-dump skill:
- AgentBackend adapter seam with two implementations: Claude Agent SDK
  (skill loading, session resume via sdk_session_id) and a hand-rolled
  OpenAI-compatible tool-calling loop (vault-scoped file tools, SKILL.md
  injected into the system prompt, history persisted in SQLite)
- session CRUD + one-turn-at-a-time SSE streaming (text deltas, file_write
  events, turn_complete), per-session lock, abort on disconnect
- node:sqlite storage (sessions, messages, turn_events) with migrations
- vault allow-list + symlink-aware path-escape prevention (incl. dangling
  symlink defense), read-only git status/diff endpoints, configurable CORS
- deterministic mock chat-completions server (scripts/mock-openai.ts) for
  end-to-end testing without a real model

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 13:48:17 +00:00

120 lines
4.3 KiB
TypeScript

/**
* Deterministic mock of an OpenAI-compatible /v1/chat/completions endpoint that
* speaks streaming SSE, for end-to-end testing the openai-compat backend without
* a real model. tsx-runnable:
*
* PORT=11500 tsx scripts/mock-openai.ts
*
* Scripting (decided per request from the message history it receives):
* - If the latest user message contains "ESCAPE", the first assistant turn
* emits a write_file tool_call with path "../escape.md" (vault-escape probe).
* - Otherwise the first assistant turn streams some text and a write_file
* tool_call for projects/mock/mock.md (arguments split across fragments, and
* id/name only on the first fragment, to exercise streamed-tool_call parsing).
* - Once the history's last message is a tool result, it streams a final text
* and finishes with finish_reason "stop".
*
* It logs a compact summary of every request's message roles to stderr so an
* e2e harness can assert that a second turn replays prior history.
*/
import http from "node:http";
const PORT = Number(process.env.PORT ?? 11500);
interface InMsg {
role: string;
content: string | null;
tool_calls?: unknown[];
tool_call_id?: string;
}
function sseChunk(obj: unknown): string {
return `data: ${JSON.stringify(obj)}\n\n`;
}
function baseDelta(delta: unknown, finish: string | null = null) {
return {
id: "chatcmpl-mock",
object: "chat.completion.chunk",
created: Math.floor(Date.now() / 1000),
model: "mock-model",
choices: [{ index: 0, delta, finish_reason: finish }],
};
}
const server = http.createServer((req, res) => {
if (req.method !== "POST" || !req.url?.includes("/chat/completions")) {
res.writeHead(404).end("not found");
return;
}
let body = "";
req.on("data", (c) => (body += c));
req.on("end", () => {
let messages: InMsg[] = [];
try {
messages = (JSON.parse(body).messages ?? []) as InMsg[];
} catch {
/* ignore */
}
const roles = messages.map((m) => m.role).join(",");
const userMsgs = messages.filter((m) => m.role === "user").length;
process.stderr.write(`[mock] request roles=[${roles}] userMessages=${userMsgs}\n`);
const last = messages[messages.length - 1];
const lastUser = [...messages].reverse().find((m) => m.role === "user");
const wantEscape = typeof lastUser?.content === "string" && lastUser.content.includes("ESCAPE");
res.writeHead(200, {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
Connection: "keep-alive",
});
if (last?.role === "tool") {
// Second iteration: finish the turn.
res.write(sseChunk(baseDelta({ role: "assistant", content: "" })));
res.write(sseChunk(baseDelta({ content: "Done — I captured that into the vault. " })));
res.write(sseChunk(baseDelta({ content: "What else should we add? (1) goals? (2) stack?" })));
res.write(sseChunk(baseDelta({}, "stop")));
res.write("data: [DONE]\n\n");
res.end();
return;
}
// First iteration: stream text + a write_file tool_call across fragments.
const targetPath = wantEscape ? "../escape.md" : "projects/mock/mock.md";
const fileContent = "---\ntags:\n - mock\n - mock/moc\n---\n\n# Mock\n\nCaptured by the mock backend.\n\n## TODO / Ideas\n\n<!-- TODO: confirm details -->\n";
const argsFull = JSON.stringify({ path: targetPath, content: fileContent });
const mid = Math.floor(argsFull.length / 2);
res.write(sseChunk(baseDelta({ role: "assistant", content: "" })));
res.write(sseChunk(baseDelta({ content: "Got it. Let me start the note. " })));
// tool_call fragment 1: id + name + first half of arguments.
res.write(
sseChunk(
baseDelta({
tool_calls: [
{ index: 0, id: "call_mock_1", type: "function", function: { name: "write_file", arguments: argsFull.slice(0, mid) } },
],
})
)
);
// tool_call fragment 2: remaining arguments only (no id/name).
res.write(
sseChunk(
baseDelta({
tool_calls: [{ index: 0, function: { arguments: argsFull.slice(mid) } }],
})
)
);
res.write(sseChunk(baseDelta({}, "tool_calls")));
res.write("data: [DONE]\n\n");
res.end();
});
});
server.listen(PORT, () => {
process.stderr.write(`[mock] listening on http://localhost:${PORT}\n`);
});