@jovan158/vantage
Version:
See and control what your coding agent does
67 lines (66 loc) • 2.31 kB
JavaScript
// Alerts for Claude Code's own chat UI.
//
// While that UI owns the terminal, `vantage run` cannot print (see
// src/terminal.ts). Instead it posts each alert here, in the session folder.
// The hook Claude Code starts when a reply is finished (Stop), and before each
// tool when rules or a budget are enforced (PreToolUse), takes what is waiting
// and returns it as a `systemMessage`, which Claude Code shows in the
// conversation. Whatever is still waiting when Claude Code exits is printed
// then.
import fs from "node:fs";
import path from "node:path";
import { plain } from "./sanitize.js";
export function outboxPath(dir) {
return path.join(dir, "outbox.jsonl");
}
export function postAlert(file, alert) {
try {
fs.appendFileSync(file, JSON.stringify({ level: alert.level, message: plain(alert.message) }) + "\n");
}
catch {
/* the alert is still in the event log and in `vantage watch` */
}
}
// Takes every waiting alert, exactly once: the file is renamed away first, so
// an alert posted meanwhile starts a new file and waits for the next take.
export function takeAlerts(file) {
if (!file)
return [];
const taking = `${file}.${process.pid}.taking`;
try {
fs.renameSync(file, taking);
}
catch {
return []; // nothing waiting
}
let text = "";
try {
text = fs.readFileSync(taking, "utf8");
}
catch {
/* taken but unreadable: dropped */
}
finally {
fs.rmSync(taking, { force: true });
}
const alerts = [];
for (const line of text.split("\n")) {
if (!line.trim())
continue;
try {
const a = JSON.parse(line);
if (typeof a.message !== "string" || alerts.some((x) => x.message === a.message))
continue;
alerts.push({ level: a.level === "critical" ? "critical" : "warn", message: a.message });
}
catch {
/* a torn line */
}
}
return alerts;
}
// As shown in the chat, one line per alert. Claude Code puts the hook's name
// in front ("Stop says: …"), so each line says it is from Vantage.
export function chatText(alerts) {
return alerts.map((a) => `Vantage${a.level === "critical" ? " ALERT" : ""}: ${a.message}`).join("\n");
}