@pushary/agent-hooks 0.87.2 → 0.87.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/data/cursor-plugin/hooks/hooks.json +7 -0
- package/data/cursor-plugin/scripts/pushary-gate.mjs +85 -28
- package/data/cursor-plugin/scripts/pushary-gate.test.mjs +42 -1
- package/dist/bin/pushary-bell-hook.js +4 -4
- package/dist/bin/pushary-bell.js +7 -6
- package/dist/bin/pushary-claude.js +14 -11
- package/dist/bin/pushary-clean.js +36 -20
- package/dist/bin/pushary-codex-bridge.js +6 -5
- package/dist/bin/pushary-codex-hook.js +129 -52
- package/dist/bin/pushary-codex.js +4 -3
- package/dist/bin/pushary-connect.js +6 -6
- package/dist/bin/pushary-daemon.js +5 -4
- package/dist/bin/pushary-disconnect.js +65 -8
- package/dist/bin/pushary-doctor.js +79 -18
- package/dist/bin/pushary-elicitation-hook.js +1 -1
- package/dist/bin/pushary-gemini-bridge.js +6 -5
- package/dist/bin/pushary-gemini-hook.js +12 -10
- package/dist/bin/pushary-hook.js +6 -5
- package/dist/bin/pushary-login.js +4 -4
- package/dist/bin/pushary-logout.js +5 -4
- package/dist/bin/pushary-mode.js +3 -3
- package/dist/bin/pushary-notification-hook.js +4 -3
- package/dist/bin/pushary-opencode-hook.d.ts +1 -0
- package/dist/bin/pushary-opencode-hook.js +329 -0
- package/dist/bin/pushary-permission-denied-hook.js +6 -5
- package/dist/bin/pushary-permission-hook.js +6 -5
- package/dist/bin/pushary-post-hook.js +4 -3
- package/dist/bin/pushary-prompt-hook.js +4 -3
- package/dist/bin/pushary-session-end-hook.js +4 -3
- package/dist/bin/pushary-session-start-hook.js +4 -3
- package/dist/bin/pushary-setup.js +238 -84
- package/dist/bin/pushary-stats.js +2 -2
- package/dist/bin/pushary-status.js +5 -5
- package/dist/bin/pushary-stop-hook.js +4 -3
- package/dist/bin/pushary-stopfailure-hook.js +4 -3
- package/dist/bin/pushary-upgrade.js +7 -6
- package/dist/bin/pushary-wait.js +3 -3
- package/dist/{chunk-5646VKR4.js → chunk-2JYQAW4B.js} +5 -53
- package/dist/chunk-7J67NYK4.js +287 -0
- package/dist/{chunk-M7RYIZX6.js → chunk-B7ZBHWOW.js} +10 -1
- package/dist/{chunk-7YJS2VTG.js → chunk-DMW54VWK.js} +12 -1
- package/dist/{chunk-LT2VLPUK.js → chunk-DPUOW6NS.js} +18 -2
- package/dist/{chunk-3LKYBFWA.js → chunk-EJL3PJU7.js} +1 -1
- package/dist/chunk-GIRT7677.js +11 -0
- package/dist/{chunk-HAUTLCMA.js → chunk-ICERLAE2.js} +1 -1
- package/dist/{chunk-ZZE6FN7T.js → chunk-K42RXJPG.js} +52 -1
- package/dist/{chunk-Y3UTTFTZ.js → chunk-KBS53WOE.js} +1 -1
- package/dist/{chunk-SJVLFIYP.js → chunk-KCAFCGGY.js} +1 -1
- package/dist/{chunk-A4I5JGCW.js → chunk-KFAH6KRE.js} +1 -1
- package/dist/{chunk-QBWYNVKY.js → chunk-LGVHYFIB.js} +1 -1
- package/dist/{chunk-HJ44HIUW.js → chunk-OMA2OZRE.js} +11 -9
- package/dist/{chunk-ECEUMWBH.js → chunk-QLMX5LLV.js} +1 -1
- package/dist/{chunk-XW5CHAQX.js → chunk-TAYHEJFF.js} +2 -2
- package/dist/{chunk-JAICP7BA.js → chunk-TD3M232T.js} +1 -1
- package/dist/{chunk-WLLBFDVY.js → chunk-TV3UFGT7.js} +1 -1
- package/dist/{chunk-7MBDFNUC.js → chunk-VIHRP25S.js} +1 -1
- package/dist/{chunk-B3V2H6SA.js → chunk-VL5ZNK6V.js} +43 -12
- package/dist/{chunk-V2XPF77V.js → chunk-VOE4SYPR.js} +42 -279
- package/dist/{chunk-OT4YD54Q.js → chunk-YX6ZKCC5.js} +1 -1
- package/dist/{chunk-2T3GX7WG.js → chunk-YYTLTEGU.js} +1 -1
- package/dist/chunk-ZGC3W23N.js +371 -0
- package/dist/src/index.js +6 -5
- package/package.json +2 -1
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
import {
|
|
2
|
+
HOOK_BUDGETS,
|
|
3
|
+
OPENCODE_APPROVAL_EVENT
|
|
4
|
+
} from "./chunk-DMW54VWK.js";
|
|
5
|
+
|
|
6
|
+
// src/opencode-plugin.ts
|
|
7
|
+
var OPENCODE_PLUGIN_MARKER = "pushary-opencode-plugin w1";
|
|
8
|
+
var OPENCODE_PLUGIN_PLACEHOLDER = "__PUSHARY_COMMAND__";
|
|
9
|
+
var OPENCODE_PLUGIN_FILE = "pushary.js";
|
|
10
|
+
var OPENCODE_PLUGIN_DIR = "plugins";
|
|
11
|
+
var OPENCODE_PLUGIN_SIBLING = "package.json";
|
|
12
|
+
var OPENCODE_PLUGIN_SIBLING_CONTENTS = `${JSON.stringify({ type: "module" }, null, 2)}
|
|
13
|
+
`;
|
|
14
|
+
var OPENCODE_ASKED_EVENTS = ["permission.asked", "permission.updated"];
|
|
15
|
+
var OPENCODE_REPLIED_EVENT = "permission.replied";
|
|
16
|
+
var OPENCODE_IDLE_EVENT = "session.idle";
|
|
17
|
+
var approvalTimeoutMs = () => (HOOK_BUDGETS.opencode.budgetSeconds - HOOK_BUDGETS.opencode.guardSeconds) * 1e3;
|
|
18
|
+
var openCodePluginSource = () => `// Pushary for OpenCode. Generated by @pushary/agent-hooks \u2014 do not edit.
|
|
19
|
+
// ${OPENCODE_PLUGIN_MARKER}
|
|
20
|
+
//
|
|
21
|
+
// OpenCode finds a plugin by globbing {plugin,plugins}/*.{ts,js} under its config
|
|
22
|
+
// directory and importing it, so this file is the whole install.
|
|
23
|
+
//
|
|
24
|
+
// It decides nothing. Each hook builds the payload OpenCode handed it, writes it
|
|
25
|
+
// to the Pushary hook command on stdin, and reads a verdict back. The policy
|
|
26
|
+
// engine, the phone, the wait and the settlement all live behind that command.
|
|
27
|
+
//
|
|
28
|
+
// Approvals ride the event stream rather than the "permission.ask" hook, which is
|
|
29
|
+
// declared in @opencode-ai/plugin and called by nothing. Permission.ask publishes
|
|
30
|
+
// "permission.asked" and blocks on a Deferred that only a reply resolves, so this
|
|
31
|
+
// answers that reply on the person's behalf when Pushary has a verdict.
|
|
32
|
+
//
|
|
33
|
+
// Failure is silence. A missing command, a crash, a timeout, unreadable bytes and
|
|
34
|
+
// an unparseable reply all leave OpenCode's own prompt exactly as it was. The
|
|
35
|
+
// worst this file may do to somebody is nothing at all.
|
|
36
|
+
|
|
37
|
+
import { spawn } from "node:child_process"
|
|
38
|
+
|
|
39
|
+
const COMMAND = ${OPENCODE_PLUGIN_PLACEHOLDER}
|
|
40
|
+
const APPROVAL_TIMEOUT_MS = ${approvalTimeoutMs()}
|
|
41
|
+
const TELEMETRY_TIMEOUT_MS = 10000
|
|
42
|
+
const MAX_REPLY_BYTES = 65536
|
|
43
|
+
// One entry per call in flight. Bounded because a long session must not grow them
|
|
44
|
+
// without limit, and evicted oldest-first because whatever is still waiting on an
|
|
45
|
+
// answer is the newest thing in here, never the oldest.
|
|
46
|
+
const MAX_TRACKED = 256
|
|
47
|
+
|
|
48
|
+
const text = (value) => (typeof value === "string" && value.length > 0 ? value : undefined)
|
|
49
|
+
|
|
50
|
+
const remember = (map, key, value) => {
|
|
51
|
+
if (!key || value === undefined) return
|
|
52
|
+
if (map.size >= MAX_TRACKED) map.delete(map.keys().next().value)
|
|
53
|
+
map.set(key, value)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const run = (payload, timeoutMs, onSpawn) =>
|
|
57
|
+
new Promise((resolve) => {
|
|
58
|
+
let settled = false
|
|
59
|
+
const finish = (value) => {
|
|
60
|
+
if (settled) return
|
|
61
|
+
settled = true
|
|
62
|
+
clearTimeout(timer)
|
|
63
|
+
resolve(value)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
let child
|
|
67
|
+
try {
|
|
68
|
+
child = spawn(COMMAND[0], COMMAND.slice(1), { stdio: ["pipe", "pipe", "ignore"] })
|
|
69
|
+
} catch {
|
|
70
|
+
return finish(null)
|
|
71
|
+
}
|
|
72
|
+
if (onSpawn) onSpawn(child)
|
|
73
|
+
|
|
74
|
+
const timer = setTimeout(() => {
|
|
75
|
+
try {
|
|
76
|
+
child.kill("SIGKILL")
|
|
77
|
+
} catch {}
|
|
78
|
+
finish(null)
|
|
79
|
+
}, timeoutMs)
|
|
80
|
+
if (typeof timer.unref === "function") timer.unref()
|
|
81
|
+
|
|
82
|
+
let out = ""
|
|
83
|
+
child.stdout.on("data", (chunk) => {
|
|
84
|
+
if (out.length < MAX_REPLY_BYTES) out += chunk
|
|
85
|
+
})
|
|
86
|
+
child.on("error", () => finish(null))
|
|
87
|
+
child.on("close", () => {
|
|
88
|
+
const body = out.trim()
|
|
89
|
+
if (!body) return finish(null)
|
|
90
|
+
try {
|
|
91
|
+
finish(JSON.parse(body))
|
|
92
|
+
} catch {
|
|
93
|
+
finish(null)
|
|
94
|
+
}
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
try {
|
|
98
|
+
child.stdin.on("error", () => {})
|
|
99
|
+
child.stdin.end(JSON.stringify(payload))
|
|
100
|
+
} catch {
|
|
101
|
+
finish(null)
|
|
102
|
+
}
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
// Telemetry is fire and forget: the agent has already acted and is only being
|
|
106
|
+
// observed, so nothing here may hold up the next turn.
|
|
107
|
+
const tell = (payload) => {
|
|
108
|
+
void run(payload, TELEMETRY_TIMEOUT_MS)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// What OpenCode gates on, and what it hands over with it. Verified against its own
|
|
112
|
+
// tools: bash asks with { command }, write and edit both ask under "edit" with
|
|
113
|
+
// { filepath, diff }, webfetch with { url }, and a shell command reaching outside
|
|
114
|
+
// the worktree asks a second time under "external_directory".
|
|
115
|
+
const inputFrom = (permission, metadata) => {
|
|
116
|
+
const meta = metadata && typeof metadata === "object" ? metadata : {}
|
|
117
|
+
const command = text(meta.command)
|
|
118
|
+
if (command) return { command }
|
|
119
|
+
const filepath = text(meta.filepath) ?? text(meta.file_path)
|
|
120
|
+
// The diff rides along, because it is what the person is actually approving.
|
|
121
|
+
// OpenCode computes it before raising the permission and hands it over; without
|
|
122
|
+
// it the phone shows a filename and nothing to judge.
|
|
123
|
+
if (filepath) return { file_path: filepath, ...(text(meta.diff) ? { diff: meta.diff } : {}) }
|
|
124
|
+
const url = text(meta.url)
|
|
125
|
+
if (url) return { url }
|
|
126
|
+
return { permission }
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Two spellings of one request: the runtime publishes PermissionV1.Request
|
|
130
|
+
// ({ permission, patterns }), the generated SDK types say Permission ({ type,
|
|
131
|
+
// pattern }). Whichever arrives, the id and the session are what we need.
|
|
132
|
+
const askedFrom = (properties) => {
|
|
133
|
+
if (!properties || typeof properties !== "object") return null
|
|
134
|
+
const id = text(properties.id)
|
|
135
|
+
const sessionID = text(properties.sessionID)
|
|
136
|
+
if (!id || !sessionID) return null
|
|
137
|
+
return {
|
|
138
|
+
id,
|
|
139
|
+
sessionID,
|
|
140
|
+
permission: text(properties.permission) ?? text(properties.type) ?? "unknown",
|
|
141
|
+
metadata: properties.metadata,
|
|
142
|
+
title: text(properties.title),
|
|
143
|
+
callID: text(properties.callID) ?? text(properties.tool && properties.tool.callID),
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const repliedID = (properties) => {
|
|
148
|
+
if (!properties || typeof properties !== "object") return undefined
|
|
149
|
+
return text(properties.requestID) ?? text(properties.permissionID) ?? text(properties.id)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export const PusharyPlugin = async ({ client, directory, worktree }) => {
|
|
153
|
+
const cwd = worktree || directory || process.cwd()
|
|
154
|
+
// OpenCode raises a permission under a coarse key \u2014 "edit" covers both write and
|
|
155
|
+
// edit \u2014 and names the actual tool only on tool.execute.before. Keeping the
|
|
156
|
+
// pairing is what lets a rule written for Write mean Write here too.
|
|
157
|
+
const tools = new Map()
|
|
158
|
+
// Every gate still waiting. Held so a person answering in the terminal can end
|
|
159
|
+
// ours: whoever answers first wins, and the loser stops asking.
|
|
160
|
+
const waiting = new Map()
|
|
161
|
+
|
|
162
|
+
const base = (sessionID) => ({ session_id: sessionID ?? "", cwd })
|
|
163
|
+
|
|
164
|
+
// Replying is the whole approval. Two shapes, because the client handed to a
|
|
165
|
+
// plugin is generated: v1 exposes postSessionIdPermissionsPermissionId against
|
|
166
|
+
// /session/{id}/permissions/{permissionID}, v2 exposes permission.reply against
|
|
167
|
+
// /permission/{requestID}/reply. Both routes are live in the server today.
|
|
168
|
+
const respond = async (asked, allow) => {
|
|
169
|
+
const legacy = client && typeof client.postSessionIdPermissionsPermissionId === "function"
|
|
170
|
+
? () => client.postSessionIdPermissionsPermissionId({
|
|
171
|
+
path: { id: asked.sessionID, permissionID: asked.id },
|
|
172
|
+
body: { response: allow ? "once" : "reject" },
|
|
173
|
+
})
|
|
174
|
+
: null
|
|
175
|
+
const current = client && client.permission && typeof client.permission.reply === "function"
|
|
176
|
+
? () => client.permission.reply({
|
|
177
|
+
path: { requestID: asked.id },
|
|
178
|
+
body: { reply: allow ? "once" : "reject" },
|
|
179
|
+
})
|
|
180
|
+
: null
|
|
181
|
+
|
|
182
|
+
for (const attempt of [legacy, current]) {
|
|
183
|
+
if (!attempt) continue
|
|
184
|
+
try {
|
|
185
|
+
await attempt()
|
|
186
|
+
return
|
|
187
|
+
} catch {
|
|
188
|
+
// The person answered first, or this client does not serve that route.
|
|
189
|
+
// Either way the next shape is the only thing left to try.
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const gate = async (asked) => {
|
|
195
|
+
const reply = await run({
|
|
196
|
+
...base(asked.sessionID),
|
|
197
|
+
hook_event_name: "${OPENCODE_APPROVAL_EVENT}",
|
|
198
|
+
tool_name: tools.get(asked.callID) ?? asked.permission,
|
|
199
|
+
tool_input: inputFrom(asked.permission, asked.metadata),
|
|
200
|
+
permission_id: asked.id,
|
|
201
|
+
permission_type: asked.permission,
|
|
202
|
+
title: asked.title,
|
|
203
|
+
}, APPROVAL_TIMEOUT_MS, (child) => remember(waiting, asked.id, child))
|
|
204
|
+
|
|
205
|
+
// Gone means the terminal already answered while we were asking. Replying now
|
|
206
|
+
// would be answering a question nobody is holding open any more.
|
|
207
|
+
if (!waiting.delete(asked.id)) return
|
|
208
|
+
|
|
209
|
+
const status = reply && typeof reply === "object" ? reply.status : undefined
|
|
210
|
+
if (status !== "allow" && status !== "deny") return
|
|
211
|
+
await respond(asked, status === "allow")
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const withdraw = (id) => {
|
|
215
|
+
const child = id && waiting.get(id)
|
|
216
|
+
if (!child) return
|
|
217
|
+
waiting.delete(id)
|
|
218
|
+
// SIGTERM rather than SIGKILL: the hook cancels its outstanding question on
|
|
219
|
+
// the way out, so the phone stops showing a decision the terminal has taken.
|
|
220
|
+
try {
|
|
221
|
+
child.kill("SIGTERM")
|
|
222
|
+
} catch {}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return {
|
|
226
|
+
"tool.execute.before": async (input) => {
|
|
227
|
+
remember(tools, input && input.callID, input && input.tool)
|
|
228
|
+
},
|
|
229
|
+
|
|
230
|
+
"tool.execute.after": async (input, output) => {
|
|
231
|
+
const call = input && input.callID
|
|
232
|
+
if (call) tools.delete(call)
|
|
233
|
+
tell({
|
|
234
|
+
...base(input && input.sessionID),
|
|
235
|
+
hook_event_name: "tool.execute.after",
|
|
236
|
+
tool_name: text(input && input.tool) ?? "unknown",
|
|
237
|
+
tool_input: input && input.args && typeof input.args === "object" ? input.args : {},
|
|
238
|
+
tool_result: { title: text(output && output.title), output: text(output && output.output) },
|
|
239
|
+
})
|
|
240
|
+
},
|
|
241
|
+
|
|
242
|
+
"chat.message": async (input, output) => {
|
|
243
|
+
const parts = output && Array.isArray(output.parts) ? output.parts : []
|
|
244
|
+
const prompt = parts
|
|
245
|
+
.filter((part) => part && part.type === "text" && typeof part.text === "string")
|
|
246
|
+
.map((part) => part.text)
|
|
247
|
+
.join("\\n")
|
|
248
|
+
.trim()
|
|
249
|
+
if (!prompt) return
|
|
250
|
+
tell({ ...base(input && input.sessionID), hook_event_name: "chat.message", prompt })
|
|
251
|
+
},
|
|
252
|
+
|
|
253
|
+
event: async ({ event }) => {
|
|
254
|
+
// Never allowed to throw: the host calls this hook without awaiting it, so a
|
|
255
|
+
// rejection here is an unhandled rejection in somebody's editor.
|
|
256
|
+
try {
|
|
257
|
+
const type = event && event.type
|
|
258
|
+
const properties = event && event.properties
|
|
259
|
+
if (${JSON.stringify(OPENCODE_ASKED_EVENTS)}.includes(type)) {
|
|
260
|
+
const asked = askedFrom(properties)
|
|
261
|
+
if (asked) void gate(asked)
|
|
262
|
+
return
|
|
263
|
+
}
|
|
264
|
+
if (type === ${JSON.stringify(OPENCODE_REPLIED_EVENT)}) {
|
|
265
|
+
withdraw(repliedID(properties))
|
|
266
|
+
return
|
|
267
|
+
}
|
|
268
|
+
// session.idle is the turn boundary \u2014 OpenCode's equivalent of Claude's
|
|
269
|
+
// Stop, not of its SessionEnd. Nothing else on the stream is relayed: the
|
|
270
|
+
// receipts already arrive through the hooks above, and reporting the same
|
|
271
|
+
// moment twice would double every row on the account.
|
|
272
|
+
if (type === ${JSON.stringify(OPENCODE_IDLE_EVENT)}) {
|
|
273
|
+
tell({
|
|
274
|
+
...base(properties && properties.sessionID),
|
|
275
|
+
hook_event_name: ${JSON.stringify(OPENCODE_IDLE_EVENT)},
|
|
276
|
+
})
|
|
277
|
+
}
|
|
278
|
+
} catch {}
|
|
279
|
+
},
|
|
280
|
+
|
|
281
|
+
dispose: async () => {
|
|
282
|
+
for (const id of Array.from(waiting.keys())) withdraw(id)
|
|
283
|
+
tools.clear()
|
|
284
|
+
},
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
`;
|
|
288
|
+
var openCodePluginModule = () => ({
|
|
289
|
+
marker: OPENCODE_PLUGIN_MARKER,
|
|
290
|
+
source: openCodePluginSource(),
|
|
291
|
+
placeholder: OPENCODE_PLUGIN_PLACEHOLDER,
|
|
292
|
+
sibling: {
|
|
293
|
+
path: OPENCODE_PLUGIN_SIBLING,
|
|
294
|
+
contents: OPENCODE_PLUGIN_SIBLING_CONTENTS
|
|
295
|
+
}
|
|
296
|
+
});
|
|
297
|
+
var renderOpenCodePlugin = (module, argv) => module.source.replace(module.placeholder, JSON.stringify(argv));
|
|
298
|
+
var isOpenCodePluginOurs = (contents) => contents.includes(OPENCODE_PLUGIN_MARKER);
|
|
299
|
+
|
|
300
|
+
// src/opencode-config.ts
|
|
301
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from "fs";
|
|
302
|
+
import { homedir } from "os";
|
|
303
|
+
import { join } from "path";
|
|
304
|
+
var OPENCODE_HOOK_BINARY = "pushary-opencode-hook";
|
|
305
|
+
var openCodeConfigDirFrom = (home, env = process.env) => env.OPENCODE_CONFIG_DIR?.trim() || join(env.XDG_CONFIG_HOME?.trim() || join(home, ".config"), "opencode");
|
|
306
|
+
var openCodeConfigDir = () => openCodeConfigDirFrom(homedir());
|
|
307
|
+
var OPENCODE_PLUGIN_PATH = `${OPENCODE_PLUGIN_DIR}/${OPENCODE_PLUGIN_FILE}`;
|
|
308
|
+
var openCodePluginDir = () => join(openCodeConfigDir(), OPENCODE_PLUGIN_DIR);
|
|
309
|
+
var openCodePluginFile = () => join(openCodePluginDir(), OPENCODE_PLUGIN_FILE);
|
|
310
|
+
var OPENCODE_HOOK_EVENTS = [
|
|
311
|
+
{ event: OPENCODE_APPROVAL_EVENT, delivery: "approval", timeout: HOOK_BUDGETS.opencode.budgetSeconds },
|
|
312
|
+
{ event: "tool.execute.after", delivery: "telemetry", timeout: 10 },
|
|
313
|
+
{ event: "chat.message", delivery: "telemetry", timeout: 10 },
|
|
314
|
+
{ event: OPENCODE_IDLE_EVENT, delivery: "telemetry", timeout: 10 }
|
|
315
|
+
];
|
|
316
|
+
var hasForeignPlugins = (entries) => entries.some((entry) => entry !== OPENCODE_PLUGIN_FILE && /\.(ts|js)$/.test(entry));
|
|
317
|
+
var installOpenCodePlugin = (argv, dir = openCodePluginDir()) => {
|
|
318
|
+
mkdirSync(dir, { recursive: true });
|
|
319
|
+
const plugin = join(dir, OPENCODE_PLUGIN_FILE);
|
|
320
|
+
writeFileSync(plugin, renderOpenCodePlugin(openCodePluginModule(), argv), "utf-8");
|
|
321
|
+
const sibling = join(dir, OPENCODE_PLUGIN_SIBLING);
|
|
322
|
+
if (existsSync(sibling)) return { plugin, wroteSibling: false };
|
|
323
|
+
writeFileSync(sibling, OPENCODE_PLUGIN_SIBLING_CONTENTS, "utf-8");
|
|
324
|
+
return { plugin, wroteSibling: true };
|
|
325
|
+
};
|
|
326
|
+
var openCodeWired = (dir = openCodePluginDir()) => {
|
|
327
|
+
try {
|
|
328
|
+
return isOpenCodePluginOurs(readFileSync(join(dir, OPENCODE_PLUGIN_FILE), "utf-8"));
|
|
329
|
+
} catch {
|
|
330
|
+
return false;
|
|
331
|
+
}
|
|
332
|
+
};
|
|
333
|
+
var OPENCODE_MCP_URL = "https://pushary.com/api/mcp/mcp";
|
|
334
|
+
var OPENCODE_CONFIG_FILE = "opencode.json";
|
|
335
|
+
var openCodeConfigFile = () => join(openCodeConfigDir(), OPENCODE_CONFIG_FILE);
|
|
336
|
+
var asRecord = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
337
|
+
var addOpenCodeMcpServer = (config, apiKey) => {
|
|
338
|
+
const servers = asRecord(config.mcp) ?? {};
|
|
339
|
+
servers.pushary = {
|
|
340
|
+
type: "remote",
|
|
341
|
+
url: OPENCODE_MCP_URL,
|
|
342
|
+
enabled: true,
|
|
343
|
+
headers: { Authorization: `Bearer ${apiKey}` }
|
|
344
|
+
};
|
|
345
|
+
config.mcp = servers;
|
|
346
|
+
};
|
|
347
|
+
var removeOpenCodeMcpServer = (config) => {
|
|
348
|
+
const servers = asRecord(config.mcp);
|
|
349
|
+
if (!servers || !("pushary" in servers)) return false;
|
|
350
|
+
delete servers.pushary;
|
|
351
|
+
if (Object.keys(servers).length === 0) delete config.mcp;
|
|
352
|
+
return true;
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
export {
|
|
356
|
+
OPENCODE_PLUGIN_PLACEHOLDER,
|
|
357
|
+
OPENCODE_PLUGIN_SIBLING,
|
|
358
|
+
OPENCODE_PLUGIN_SIBLING_CONTENTS,
|
|
359
|
+
isOpenCodePluginOurs,
|
|
360
|
+
OPENCODE_HOOK_BINARY,
|
|
361
|
+
openCodeConfigDirFrom,
|
|
362
|
+
openCodeConfigDir,
|
|
363
|
+
openCodePluginDir,
|
|
364
|
+
openCodePluginFile,
|
|
365
|
+
hasForeignPlugins,
|
|
366
|
+
installOpenCodePlugin,
|
|
367
|
+
openCodeWired,
|
|
368
|
+
openCodeConfigFile,
|
|
369
|
+
addOpenCodeMcpServer,
|
|
370
|
+
removeOpenCodeMcpServer
|
|
371
|
+
};
|
package/dist/src/index.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import {
|
|
2
2
|
handlePreToolUse
|
|
3
|
-
} from "../chunk-
|
|
3
|
+
} from "../chunk-OMA2OZRE.js";
|
|
4
4
|
import "../chunk-ATBKJNWS.js";
|
|
5
|
-
import "../chunk-
|
|
5
|
+
import "../chunk-VIHRP25S.js";
|
|
6
6
|
import "../chunk-YHG74UFF.js";
|
|
7
7
|
import {
|
|
8
8
|
fetchModeOverride,
|
|
@@ -12,16 +12,17 @@ import {
|
|
|
12
12
|
handlePostToolUse,
|
|
13
13
|
handleStop,
|
|
14
14
|
reportEvent
|
|
15
|
-
} from "../chunk-
|
|
15
|
+
} from "../chunk-VOE4SYPR.js";
|
|
16
16
|
import {
|
|
17
17
|
askUser,
|
|
18
18
|
cancelQuestion,
|
|
19
19
|
waitForAnswer
|
|
20
20
|
} from "../chunk-J3YDT4HM.js";
|
|
21
|
+
import "../chunk-7J67NYK4.js";
|
|
21
22
|
import "../chunk-RN3NOEJF.js";
|
|
22
23
|
import "../chunk-BSZYIAZL.js";
|
|
23
24
|
import "../chunk-SAF6HGAA.js";
|
|
24
|
-
import "../chunk-
|
|
25
|
+
import "../chunk-KBS53WOE.js";
|
|
25
26
|
import "../chunk-7QLSKOSU.js";
|
|
26
27
|
import "../chunk-KZERVKTD.js";
|
|
27
28
|
import {
|
|
@@ -30,7 +31,7 @@ import {
|
|
|
30
31
|
} from "../chunk-2UMNXADU.js";
|
|
31
32
|
import {
|
|
32
33
|
resolvePolicy
|
|
33
|
-
} from "../chunk-
|
|
34
|
+
} from "../chunk-DMW54VWK.js";
|
|
34
35
|
export {
|
|
35
36
|
askUser,
|
|
36
37
|
cancelQuestion,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pushary/agent-hooks",
|
|
3
|
-
"version": "0.87.
|
|
3
|
+
"version": "0.87.3",
|
|
4
4
|
"description": "Permission hooks for AI coding agents: route tool approvals through Pushary push notifications",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pushary",
|
|
@@ -56,6 +56,7 @@
|
|
|
56
56
|
"pushary-codex-hook": "./dist/bin/pushary-codex-hook.js",
|
|
57
57
|
"pushary-gemini-hook": "./dist/bin/pushary-gemini-hook.js",
|
|
58
58
|
"pushary-gemini-bridge": "./dist/bin/pushary-gemini-bridge.js",
|
|
59
|
+
"pushary-opencode-hook": "./dist/bin/pushary-opencode-hook.js",
|
|
59
60
|
"pushary-setup": "./dist/bin/pushary-setup.js",
|
|
60
61
|
"pushary-clean": "./dist/bin/pushary-clean.js",
|
|
61
62
|
"pushary-status": "./dist/bin/pushary-status.js",
|