import { spawn } from "node:child_process"
const PALACE = "__PALACE_BINARY__"
const INVESTIGATION_TOOLS = new Set(["read", "grep", "glob", "bash"])
function palaceHook(event, payload) {
return new Promise((resolve) => {
let out = ""
let child
try {
child = spawn(PALACE, ["hook", event, "--client", "opencode"], {
stdio: ["pipe", "pipe", "ignore"],
windowsHide: true,
})
} catch {
resolve({})
return
}
child.stdout.on("data", (chunk) => (out += chunk))
child.on("error", () => resolve({}))
child.on("close", () => {
try {
resolve(JSON.parse(out.trim() || "{}"))
} catch {
resolve({})
}
})
child.stdin.on("error", () => {})
child.stdin.end(JSON.stringify(payload))
})
}
export const PalacePlugin = async ({ client, directory }) => {
const pending = new Map()
const nudged = new Set()
const queue = (sessionID, text) => {
if (!sessionID || !text) return
const prior = pending.get(sessionID)
pending.set(sessionID, prior ? `${prior}\n\n${text}` : text)
}
return {
event: async ({ event }) => {
if (event.type === "session.created") {
const id = event.properties?.info?.id
const res = await palaceHook("session-start", { cwd: directory, session_id: id })
queue(id, res.additional_context)
} else if (event.type === "session.idle") {
const id = event.properties?.sessionID
if (!id || nudged.has(id)) return
const res = await palaceHook("stop", { cwd: directory, session_id: id })
if (res.followup_message) {
nudged.add(id)
client.session
.prompt({ path: { id }, body: { parts: [{ type: "text", text: res.followup_message }] } })
.catch(() => {})
}
}
},
"chat.message": async (input, output) => {
const prompt = (output.parts ?? [])
.filter((part) => part.type === "text")
.map((part) => part.text)
.join("\n")
const res = await palaceHook("user-prompt-submit", {
cwd: directory,
session_id: input.sessionID,
prompt,
})
queue(input.sessionID, res.additional_context)
},
"experimental.chat.system.transform": async (input, output) => {
const text = input.sessionID && pending.get(input.sessionID)
if (text) {
output.system.push(text)
pending.delete(input.sessionID)
}
},
"tool.execute.after": async (input, output) => {
if (!INVESTIGATION_TOOLS.has(input.tool) || typeof output?.output !== "string") return
const res = await palaceHook("post-tool-use", {
cwd: directory,
session_id: input.sessionID,
tool_name: input.tool,
tool_input: input.args ?? {},
})
if (res.additional_context) output.output += `\n\n${res.additional_context}`
},
}
}