const OL_DIR = __OPENLATCH_DIR__;
const BUDGET_MS = 1500;
const TOAST_MS = 300;
const CAP_BYTES = __CAP_BYTES__;
const WIN = process.platform === "win32";
const HOOK = OL_DIR + (WIN ? "\\bin\\openlatch-hook.exe" : "/bin/openlatch-hook");
const TRUNCATE_ORDER = ["output", "args", "prompt"];
const SEEN = Symbol.for("openlatch.opencode.load-report");
const enc = new TextEncoder();
const size = (value) => enc.encode(JSON.stringify(value)).length;
function longest(holder, key, best) {
const v = holder[key];
if (typeof v === "string") {
if (!best || v.length > best[0][best[1]].length) return [holder, key];
return best;
}
if (v && typeof v === "object") {
for (const k of Object.keys(v)) best = longest(v, k, best);
}
return best;
}
function fit(payload) {
if (size(payload) <= CAP_BYTES) return payload;
const copy = JSON.parse(JSON.stringify(payload));
const cut = [];
for (const field of TRUNCATE_ORDER) {
while (size(copy) > CAP_BYTES) {
const target = longest(copy, field, null);
if (!target || target[0][target[1]].length === 0) break;
const s = target[0][target[1]];
target[0][target[1]] = s.slice(0, Math.floor(s.length / 2));
if (!cut.includes(field)) cut.push(field);
copy.truncated = cut;
}
if (size(copy) <= CAP_BYTES) break;
}
return copy;
}
async function firstLine(stream) {
const reader = stream.getReader();
const dec = new TextDecoder();
let text = "";
try {
for (;;) {
const { value, done } = await reader.read();
if (done) break;
text += dec.decode(value, { stream: true });
const nl = text.indexOf("\n");
if (nl >= 0) {
text = text.slice(0, nl);
break;
}
}
} finally {
try { Promise.resolve(reader.cancel()).catch(() => {}); } catch {}
}
return text.trim();
}
async function call(event, payload, captureOnly) {
let proc = null;
let timer = null;
try {
const argv = [HOOK, "--agent", "opencode", "--event", event, "--openlatch-dir", OL_DIR,
"--plugin-host-pid", String(process.pid)];
if (captureOnly) argv.push("--capture-only");
const body = JSON.stringify(fit(payload));
proc = Bun.spawn(argv, {
stdin: captureOnly ? "pipe" : new Blob([body]),
stdout: captureOnly ? "ignore" : "pipe",
stderr: "ignore",
windowsHide: true,
});
if (captureOnly) {
proc.stdin.write(body);
proc.stdin.end();
return null;
}
const answer = Promise.all([firstLine(proc.stdout), proc.exited]);
const budget = new Promise((resolve) => { timer = setTimeout(() => resolve(null), BUDGET_MS); });
const done = await Promise.race([answer, budget]);
if (!done) {
try { proc.kill("SIGKILL"); } catch {}
return null;
}
const [line, code] = done;
if (code !== 0 || !line) return null;
const verdict = JSON.parse(line);
return verdict && typeof verdict === "object" ? verdict : null;
} catch {
try { if (proc) proc.kill("SIGKILL"); } catch {}
return null;
} finally {
if (timer) clearTimeout(timer);
}
}
const isBlock = (v) => !!v && v.decision === "block" && typeof v.reason === "string" && v.reason.length > 0;
const LIFECYCLE = { "session.created": "session_start", "session.idle": "stop", "session.deleted": "session_end" };
export default async function openlatch(ctx) {
let mcpServers = [];
const common = (hook, sessionId) => ({
opencode_hook: hook,
session_id: sessionId,
directory: ctx.directory,
worktree: ctx.worktree,
project_id: ctx.project ? ctx.project.id : undefined,
opencode_client: process.env.OPENCODE_CLIENT || "cli",
mcp_servers: mcpServers,
});
const loadReport = (stage, cfg) => {
try {
const seen = (globalThis[SEEN] = globalThis[SEEN] || {});
if (seen[stage]) return;
seen[stage] = true;
call("plugin_loaded", {
opencode_hook: "plugin_loaded",
opencode_client: process.env.OPENCODE_CLIENT || "cli",
directory: ctx.directory,
worktree: ctx.worktree,
project_id: ctx.project ? ctx.project.id : undefined,
mcp_servers: mcpServers,
plugins: cfg ? (Array.isArray(cfg.plugin) ? cfg.plugin.map(String) : []) : undefined,
}, true);
} catch {}
};
loadReport("module", undefined);
return {
config: async (cfg) => {
try {
if (cfg && cfg.mcp && typeof cfg.mcp === "object") mcpServers = Object.keys(cfg.mcp);
loadReport("config", cfg);
} catch {}
},
"tool.execute.before": async (input, output) => {
const verdict = await call("pre_tool_use", {
...common("tool.execute.before", input && input.sessionID),
call_id: input && input.callID,
tool: input && input.tool,
args: output && output.args,
}, false);
if (isBlock(verdict)) throw new Error(verdict.reason);
},
"chat.message": async (input, output) => {
let verdict = null;
try {
const message = (output && output.message) || {};
const model = (input && input.model) || message.model;
const parts = (output && Array.isArray(output.parts)) ? output.parts : [];
verdict = await call("user_prompt_submit", {
...common("chat.message", input && input.sessionID),
message_id: message.id || (input && input.messageID),
agent: message.agent || (input && input.agent),
model: model && model.providerID ? `${model.providerID}/${model.modelID}` : undefined,
prompt: parts.filter((p) => p && p.type === "text").map((p) => p.text).join("\n"),
}, false);
} catch {
verdict = null;
}
if (isBlock(verdict)) {
try {
await Promise.race([
ctx.client.tui.showToast({ body: { variant: "error", message: verdict.reason } }),
new Promise((resolve) => setTimeout(resolve, TOAST_MS)),
]);
} catch {}
throw new Error(verdict.reason);
}
},
"tool.execute.after": async (input, output) => {
try {
call("post_tool_use", {
...common("tool.execute.after", input && input.sessionID),
call_id: input && input.callID,
tool: input && input.tool,
args: input && input.args,
output: output && output.output,
title: output && output.title,
}, true);
} catch {}
},
event: async ({ event }) => {
try {
const wire = event && LIFECYCLE[event.type];
if (!wire) return;
const props = event.properties || {};
const sessionId = props.sessionID || (props.info && props.info.id);
if (!sessionId) return;
const payload = common(event.type, sessionId);
if (wire === "session_start" && props.info && typeof props.info.parentID === "string") {
payload.parent_session_id = props.info.parentID;
}
call(wire, payload, true);
} catch {}
},
};
}