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" };
const CHILDREN = new Map();
const CHILDREN_MAX = 1024;
const LOOKUP_MS = 1000;
function remember(id, parent) {
CHILDREN.delete(id);
if (CHILDREN.size >= CHILDREN_MAX) CHILDREN.delete(CHILDREN.keys().next().value);
CHILDREN.set(id, parent);
}
const DELETIONS = new Map();
function deleted(id) {
const count = (DELETIONS.get(id) || 0) + 1;
DELETIONS.delete(id);
if (DELETIONS.size >= CHILDREN_MAX) DELETIONS.delete(DELETIONS.keys().next().value);
DELETIONS.set(id, count);
}
async function lookup(client, id) {
let timer = null;
try {
const asked = Promise.resolve(client.session.get({ path: { id } })).then(
(res) => {
const info = res && res.data;
if (!info || typeof info !== "object") return undefined;
return typeof info.parentID === "string" ? info.parentID : info.parentID === undefined ? null : undefined;
},
() => undefined,
);
const late = new Promise((resolve) => { timer = setTimeout(() => resolve(undefined), LOOKUP_MS); });
return await Promise.race([asked, late]);
} catch {
return undefined;
} finally {
if (timer) clearTimeout(timer);
}
}
const allSynthetic = (parts) => {
const text = parts.filter((p) => p && p.type === "text");
return text.length > 0 && text.every((p) => p.synthetic === true);
};
function loaderEnv() {
const env = process.env;
const set = (name) => (typeof env[name] === "string" && env[name] !== "" ? env[name] : undefined);
const flag = (name) => ["true", "1", "yes", "on", "y"].includes(String(set(name) || "").toLowerCase());
return {
config_dir: set("OPENCODE_CONFIG_DIR"),
config_file: set("OPENCODE_CONFIG"),
config_content_set: set("OPENCODE_CONFIG_CONTENT") !== undefined,
disable_claude_code: flag("OPENCODE_DISABLE_CLAUDE_CODE"),
disable_claude_code_prompt: flag("OPENCODE_DISABLE_CLAUDE_CODE_PROMPT"),
disable_claude_code_skills: flag("OPENCODE_DISABLE_CLAUDE_CODE_SKILLS"),
disable_external_skills: flag("OPENCODE_DISABLE_EXTERNAL_SKILLS"),
};
}
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,
loader_env: loaderEnv(),
}, 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 : [];
if (allSynthetic(parts)) return;
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 {
const payload = {
...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,
};
const meta = output && output.metadata;
if (payload.tool === "task" && meta && typeof meta.sessionId === "string") {
payload.child_session_id = meta.sessionId;
}
call("post_tool_use", payload, true);
} catch {}
},
"experimental.session.compacting": async (input) => {
try {
const sessionId = input && input.sessionID;
if (typeof sessionId === "string" && sessionId) {
call("pre_compact", common("experimental.session.compacting", sessionId), 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") {
const parent = props.info && typeof props.info.parentID === "string" ? props.info.parentID : null;
if (parent) payload.parent_session_id = parent;
remember(sessionId, parent);
}
if (wire === "session_end") {
CHILDREN.delete(sessionId);
deleted(sessionId);
}
call(wire, payload, true);
if (wire !== "stop") return;
let parent = CHILDREN.get(sessionId);
if (parent === undefined) {
const generation = DELETIONS.get(sessionId);
parent = await lookup(ctx.client, sessionId);
if (DELETIONS.get(sessionId) !== generation) return;
if (parent !== undefined) remember(sessionId, parent);
}
if (parent) call("subagent_stop", { ...common(event.type, sessionId), parent_session_id: parent }, true);
} catch {}
},
};
}