pushary 1.8.0

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.
Files changed (186) hide show
  1. package/CHANGELOG.md +1943 -0
  2. package/LICENSE +21 -0
  3. package/README.md +330 -0
  4. package/data/SKILL.md +591 -0
  5. package/data/cowork/SKILL.md +77 -0
  6. package/data/cursor-plugin/.cursor-plugin/plugin.json +25 -0
  7. package/data/cursor-plugin/CHANGELOG.md +70 -0
  8. package/data/cursor-plugin/CONTRIBUTING.md +40 -0
  9. package/data/cursor-plugin/LICENSE +21 -0
  10. package/data/cursor-plugin/README.md +113 -0
  11. package/data/cursor-plugin/SECURITY.md +33 -0
  12. package/data/cursor-plugin/assets/logo.png +0 -0
  13. package/data/cursor-plugin/commands/notify-when-done.md +14 -0
  14. package/data/cursor-plugin/commands/pushary-test.md +13 -0
  15. package/data/cursor-plugin/hooks/hooks.json +75 -0
  16. package/data/cursor-plugin/mcp.json +11 -0
  17. package/data/cursor-plugin/rules/pushary.mdc +40 -0
  18. package/data/cursor-plugin/scripts/pushary-gate.mjs +724 -0
  19. package/data/cursor-plugin/scripts/pushary-gate.test.mjs +137 -0
  20. package/data/cursor-plugin/scripts/redaction.mjs +51 -0
  21. package/data/cursor-plugin/skills/pushary/SKILL.md +584 -0
  22. package/data/vscode-plugin/.claude-plugin/plugin.json +28 -0
  23. package/data/vscode-plugin/.mcp.json +11 -0
  24. package/data/vscode-plugin/CHANGELOG.md +49 -0
  25. package/data/vscode-plugin/CONTRIBUTING.md +40 -0
  26. package/data/vscode-plugin/LICENSE +21 -0
  27. package/data/vscode-plugin/README.md +128 -0
  28. package/data/vscode-plugin/SECURITY.md +56 -0
  29. package/data/vscode-plugin/assets/logo.png +0 -0
  30. package/data/vscode-plugin/commands/notify-when-done.md +14 -0
  31. package/data/vscode-plugin/commands/pushary-test.md +13 -0
  32. package/data/vscode-plugin/hooks/hooks.json +60 -0
  33. package/data/vscode-plugin/scripts/pushary-gate.mjs +762 -0
  34. package/data/vscode-plugin/scripts/redaction.mjs +51 -0
  35. package/data/vscode-plugin/skills/pushary/SKILL.md +584 -0
  36. package/dist/bin/pushary-bell-hook.d.ts +1 -0
  37. package/dist/bin/pushary-bell-hook.js +46 -0
  38. package/dist/bin/pushary-bell.d.ts +1 -0
  39. package/dist/bin/pushary-bell.js +146 -0
  40. package/dist/bin/pushary-claude.d.ts +1 -0
  41. package/dist/bin/pushary-claude.js +1916 -0
  42. package/dist/bin/pushary-clean.d.ts +1 -0
  43. package/dist/bin/pushary-clean.js +1300 -0
  44. package/dist/bin/pushary-codex-bridge.d.ts +1 -0
  45. package/dist/bin/pushary-codex-bridge.js +599 -0
  46. package/dist/bin/pushary-codex-hook.d.ts +1 -0
  47. package/dist/bin/pushary-codex-hook.js +886 -0
  48. package/dist/bin/pushary-codex.d.ts +1 -0
  49. package/dist/bin/pushary-codex.js +125 -0
  50. package/dist/bin/pushary-connect.d.ts +1 -0
  51. package/dist/bin/pushary-connect.js +100 -0
  52. package/dist/bin/pushary-cowork.d.ts +1 -0
  53. package/dist/bin/pushary-cowork.js +36 -0
  54. package/dist/bin/pushary-daemon-supervisor.d.ts +1 -0
  55. package/dist/bin/pushary-daemon-supervisor.js +27 -0
  56. package/dist/bin/pushary-daemon.d.ts +1 -0
  57. package/dist/bin/pushary-daemon.js +878 -0
  58. package/dist/bin/pushary-disconnect.d.ts +1 -0
  59. package/dist/bin/pushary-disconnect.js +264 -0
  60. package/dist/bin/pushary-doctor.d.ts +1 -0
  61. package/dist/bin/pushary-doctor.js +1433 -0
  62. package/dist/bin/pushary-elicitation-hook.d.ts +1 -0
  63. package/dist/bin/pushary-elicitation-hook.js +149 -0
  64. package/dist/bin/pushary-gemini-bridge.d.ts +1 -0
  65. package/dist/bin/pushary-gemini-bridge.js +339 -0
  66. package/dist/bin/pushary-gemini-hook.d.ts +1 -0
  67. package/dist/bin/pushary-gemini-hook.js +381 -0
  68. package/dist/bin/pushary-hook.d.ts +1 -0
  69. package/dist/bin/pushary-hook.js +95 -0
  70. package/dist/bin/pushary-login.d.ts +1 -0
  71. package/dist/bin/pushary-login.js +198 -0
  72. package/dist/bin/pushary-logout.d.ts +1 -0
  73. package/dist/bin/pushary-logout.js +147 -0
  74. package/dist/bin/pushary-mcp.d.ts +1 -0
  75. package/dist/bin/pushary-mcp.js +114 -0
  76. package/dist/bin/pushary-mode.d.ts +1 -0
  77. package/dist/bin/pushary-mode.js +130 -0
  78. package/dist/bin/pushary-notification-hook.d.ts +1 -0
  79. package/dist/bin/pushary-notification-hook.js +56 -0
  80. package/dist/bin/pushary-opencode-hook.d.ts +1 -0
  81. package/dist/bin/pushary-opencode-hook.js +338 -0
  82. package/dist/bin/pushary-permission-denied-hook.d.ts +1 -0
  83. package/dist/bin/pushary-permission-denied-hook.js +60 -0
  84. package/dist/bin/pushary-permission-hook.d.ts +1 -0
  85. package/dist/bin/pushary-permission-hook.js +65 -0
  86. package/dist/bin/pushary-post-hook.d.ts +1 -0
  87. package/dist/bin/pushary-post-hook.js +70 -0
  88. package/dist/bin/pushary-prompt-hook.d.ts +1 -0
  89. package/dist/bin/pushary-prompt-hook.js +61 -0
  90. package/dist/bin/pushary-session-end-hook.d.ts +1 -0
  91. package/dist/bin/pushary-session-end-hook.js +59 -0
  92. package/dist/bin/pushary-session-start-hook.d.ts +1 -0
  93. package/dist/bin/pushary-session-start-hook.js +72 -0
  94. package/dist/bin/pushary-setup.d.ts +1 -0
  95. package/dist/bin/pushary-setup.js +2660 -0
  96. package/dist/bin/pushary-stats.d.ts +1 -0
  97. package/dist/bin/pushary-stats.js +44 -0
  98. package/dist/bin/pushary-status.d.ts +1 -0
  99. package/dist/bin/pushary-status.js +262 -0
  100. package/dist/bin/pushary-stop-hook.d.ts +1 -0
  101. package/dist/bin/pushary-stop-hook.js +63 -0
  102. package/dist/bin/pushary-stopfailure-hook.d.ts +1 -0
  103. package/dist/bin/pushary-stopfailure-hook.js +56 -0
  104. package/dist/bin/pushary-suggestions.d.ts +1 -0
  105. package/dist/bin/pushary-suggestions.js +100 -0
  106. package/dist/bin/pushary-transcript-register.d.ts +1 -0
  107. package/dist/bin/pushary-transcript-register.js +40 -0
  108. package/dist/bin/pushary-transcripts.d.ts +1 -0
  109. package/dist/bin/pushary-transcripts.js +77 -0
  110. package/dist/bin/pushary-upgrade.d.ts +1 -0
  111. package/dist/bin/pushary-upgrade.js +599 -0
  112. package/dist/bin/pushary-wait.d.ts +1 -0
  113. package/dist/bin/pushary-wait.js +122 -0
  114. package/dist/bin/pushary.d.ts +1 -0
  115. package/dist/bin/pushary.js +74 -0
  116. package/dist/chunk-2ABTSGFT.js +173 -0
  117. package/dist/chunk-2OY3ZZ5N.js +124 -0
  118. package/dist/chunk-3EGEA4KH.js +44 -0
  119. package/dist/chunk-3EVPNIBE.js +185 -0
  120. package/dist/chunk-5OP5MQI7.js +45 -0
  121. package/dist/chunk-6J2JC2RT.js +149 -0
  122. package/dist/chunk-7QLSKOSU.js +19 -0
  123. package/dist/chunk-7Z7Q4GGS.js +88 -0
  124. package/dist/chunk-A2A2YTMG.js +835 -0
  125. package/dist/chunk-AHV4LNB5.js +377 -0
  126. package/dist/chunk-AXPCESYO.js +197 -0
  127. package/dist/chunk-C2WCPBF7.js +86 -0
  128. package/dist/chunk-CJVVKOSR.js +73 -0
  129. package/dist/chunk-CULUZTWQ.js +2117 -0
  130. package/dist/chunk-CVQYGMSR.js +142 -0
  131. package/dist/chunk-CVWG2N3Y.js +16 -0
  132. package/dist/chunk-D2FX3EPW.js +15 -0
  133. package/dist/chunk-DB3ONZB4.js +44 -0
  134. package/dist/chunk-DE4Y6TCC.js +160 -0
  135. package/dist/chunk-E4FJTUH4.js +249 -0
  136. package/dist/chunk-EPCXUP3P.js +41 -0
  137. package/dist/chunk-EX3GAPA4.js +44 -0
  138. package/dist/chunk-EYK3GSRH.js +143 -0
  139. package/dist/chunk-GMXKITVA.js +70 -0
  140. package/dist/chunk-GPEGOGRJ.js +49 -0
  141. package/dist/chunk-GU3EJCVV.js +43 -0
  142. package/dist/chunk-HQ6G3B5R.js +59 -0
  143. package/dist/chunk-HZVVNGEW.js +22 -0
  144. package/dist/chunk-IPVKH2ST.js +231 -0
  145. package/dist/chunk-J5TIV3J6.js +238 -0
  146. package/dist/chunk-JU6W3XAU.js +64 -0
  147. package/dist/chunk-JYG4B33G.js +227 -0
  148. package/dist/chunk-JZTHGAPA.js +503 -0
  149. package/dist/chunk-KGDWDSNL.js +102 -0
  150. package/dist/chunk-KVQBWI5T.js +99 -0
  151. package/dist/chunk-KZERVKTD.js +16 -0
  152. package/dist/chunk-LXQ5FBBD.js +47 -0
  153. package/dist/chunk-M5ICYGHT.js +138 -0
  154. package/dist/chunk-M5JDZ45W.js +943 -0
  155. package/dist/chunk-MDEPU45E.js +1226 -0
  156. package/dist/chunk-MPWXGONV.js +157 -0
  157. package/dist/chunk-N6IOSZOE.js +185 -0
  158. package/dist/chunk-NGDD6TXY.js +192 -0
  159. package/dist/chunk-NQTHFF4W.js +2830 -0
  160. package/dist/chunk-OKXS7WDZ.js +68 -0
  161. package/dist/chunk-OLQRRFBX.js +21 -0
  162. package/dist/chunk-P7WQEVWA.js +307 -0
  163. package/dist/chunk-POFBE6ZY.js +12 -0
  164. package/dist/chunk-PQ4K74WL.js +30 -0
  165. package/dist/chunk-PUZZELE6.js +37 -0
  166. package/dist/chunk-Q7E4I6UA.js +767 -0
  167. package/dist/chunk-QCGNGEGE.js +15 -0
  168. package/dist/chunk-R7437YI3.js +151 -0
  169. package/dist/chunk-RHR6NARM.js +305 -0
  170. package/dist/chunk-SAXBC4AZ.js +60 -0
  171. package/dist/chunk-SGZJ5LYH.js +76 -0
  172. package/dist/chunk-SIFVFP6T.js +70 -0
  173. package/dist/chunk-SLFABHW3.js +150 -0
  174. package/dist/chunk-T7E75EKA.js +67 -0
  175. package/dist/chunk-TY4JT7W3.js +38 -0
  176. package/dist/chunk-U4TMAANR.js +111 -0
  177. package/dist/chunk-UI36QSBI.js +177 -0
  178. package/dist/chunk-VQKRG3AC.js +339 -0
  179. package/dist/chunk-WE2J62NC.js +1131 -0
  180. package/dist/chunk-XHKBHWLX.js +14 -0
  181. package/dist/chunk-ZSZSSJCV.js +275 -0
  182. package/dist/chunk-ZVSKMPBZ.js +135 -0
  183. package/dist/reapply-CW3MA66V.js +34 -0
  184. package/dist/src/index.d.ts +204 -0
  185. package/dist/src/index.js +64 -0
  186. package/package.json +108 -0
@@ -0,0 +1,377 @@
1
+ import {
2
+ HOOK_BUDGETS,
3
+ OPENCODE_APPROVAL_EVENT
4
+ } from "./chunk-NQTHFF4W.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
+ // Current v2 uses flat parameters; older generated clients read path/body.
178
+ requestID: asked.id, reply: allow ? "once" : "reject",
179
+ path: { requestID: asked.id },
180
+ body: { reply: allow ? "once" : "reject" },
181
+ }, { throwOnError: true })
182
+ : null
183
+
184
+ for (const attempt of [legacy, current]) {
185
+ if (!attempt) continue
186
+ try {
187
+ const result = await attempt()
188
+ if (result && result.error) continue
189
+ return
190
+ } catch {
191
+ // The person answered first, or this client does not serve that route.
192
+ // Either way the next shape is the only thing left to try.
193
+ }
194
+ }
195
+ }
196
+
197
+ const gate = async (asked) => {
198
+ const reply = await run({
199
+ ...base(asked.sessionID),
200
+ hook_event_name: "${OPENCODE_APPROVAL_EVENT}",
201
+ tool_name: tools.get(asked.callID) ?? asked.permission,
202
+ tool_input: inputFrom(asked.permission, asked.metadata),
203
+ permission_id: asked.id,
204
+ permission_type: asked.permission,
205
+ title: asked.title,
206
+ }, APPROVAL_TIMEOUT_MS, (child) => remember(waiting, asked.id, child))
207
+
208
+ // Gone means the terminal already answered while we were asking. Replying now
209
+ // would be answering a question nobody is holding open any more.
210
+ if (!waiting.delete(asked.id)) return
211
+
212
+ const status = reply && typeof reply === "object" ? reply.status : undefined
213
+ if (status !== "allow" && status !== "deny") return
214
+ await respond(asked, status === "allow")
215
+ }
216
+
217
+ const withdraw = (id) => {
218
+ const child = id && waiting.get(id)
219
+ if (!child) return
220
+ waiting.delete(id)
221
+ // SIGTERM rather than SIGKILL: the hook cancels its outstanding question on
222
+ // the way out, so the phone stops showing a decision the terminal has taken.
223
+ try {
224
+ child.kill("SIGTERM")
225
+ } catch {}
226
+ }
227
+
228
+ return {
229
+ "tool.execute.before": async (input) => {
230
+ remember(tools, input && input.callID, input && input.tool)
231
+ },
232
+
233
+ "tool.execute.after": async (input, output) => {
234
+ const call = input && input.callID
235
+ if (call) tools.delete(call)
236
+ tell({
237
+ ...base(input && input.sessionID),
238
+ hook_event_name: "tool.execute.after",
239
+ tool_name: text(input && input.tool) ?? "unknown",
240
+ tool_input: input && input.args && typeof input.args === "object" ? input.args : {},
241
+ tool_result: { title: text(output && output.title), output: text(output && output.output) },
242
+ })
243
+ },
244
+
245
+ "chat.message": async (input, output) => {
246
+ const parts = output && Array.isArray(output.parts) ? output.parts : []
247
+ const prompt = parts
248
+ .filter((part) => part && part.type === "text" && typeof part.text === "string")
249
+ .map((part) => part.text)
250
+ .join("\\n")
251
+ .trim()
252
+ if (!prompt) return
253
+ tell({ ...base(input && input.sessionID), hook_event_name: "chat.message", prompt })
254
+ },
255
+
256
+ event: async ({ event }) => {
257
+ // Never allowed to throw: the host calls this hook without awaiting it, so a
258
+ // rejection here is an unhandled rejection in somebody's editor.
259
+ try {
260
+ const type = event && event.type
261
+ const properties = event && event.properties
262
+ if (${JSON.stringify(OPENCODE_ASKED_EVENTS)}.includes(type)) {
263
+ const asked = askedFrom(properties)
264
+ if (asked) void gate(asked)
265
+ return
266
+ }
267
+ if (type === ${JSON.stringify(OPENCODE_REPLIED_EVENT)}) {
268
+ withdraw(repliedID(properties))
269
+ return
270
+ }
271
+ // session.idle is the turn boundary \u2014 OpenCode's equivalent of Claude's
272
+ // Stop, not of its SessionEnd. Nothing else on the stream is relayed: the
273
+ // receipts already arrive through the hooks above, and reporting the same
274
+ // moment twice would double every row on the account.
275
+ if (type === ${JSON.stringify(OPENCODE_IDLE_EVENT)}) {
276
+ tell({
277
+ ...base(properties && properties.sessionID),
278
+ hook_event_name: ${JSON.stringify(OPENCODE_IDLE_EVENT)},
279
+ })
280
+ }
281
+ } catch {}
282
+ },
283
+
284
+ dispose: async () => {
285
+ for (const id of Array.from(waiting.keys())) withdraw(id)
286
+ tools.clear()
287
+ },
288
+ }
289
+ }
290
+ `;
291
+ var openCodePluginModule = () => ({
292
+ marker: OPENCODE_PLUGIN_MARKER,
293
+ source: openCodePluginSource(),
294
+ placeholder: OPENCODE_PLUGIN_PLACEHOLDER,
295
+ sibling: {
296
+ path: OPENCODE_PLUGIN_SIBLING,
297
+ contents: OPENCODE_PLUGIN_SIBLING_CONTENTS
298
+ }
299
+ });
300
+ var renderOpenCodePlugin = (module, argv) => module.source.replace(module.placeholder, JSON.stringify(argv));
301
+ var isOpenCodePluginOurs = (contents) => contents.includes(OPENCODE_PLUGIN_MARKER);
302
+
303
+ // src/opencode-config.ts
304
+ import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from "fs";
305
+ import { homedir } from "os";
306
+ import { join } from "path";
307
+ var OPENCODE_HOOK_BINARY = "pushary-opencode-hook";
308
+ var openCodeConfigDirFrom = (home, env = process.env) => env.OPENCODE_CONFIG_DIR?.trim() || join(env.XDG_CONFIG_HOME?.trim() || join(home, ".config"), "opencode");
309
+ var openCodeConfigDir = () => openCodeConfigDirFrom(homedir());
310
+ var OPENCODE_PLUGIN_PATH = `${OPENCODE_PLUGIN_DIR}/${OPENCODE_PLUGIN_FILE}`;
311
+ var openCodePluginDir = () => join(openCodeConfigDir(), OPENCODE_PLUGIN_DIR);
312
+ var openCodePluginFile = () => join(openCodePluginDir(), OPENCODE_PLUGIN_FILE);
313
+ var OPENCODE_HOOK_EVENTS = [
314
+ { event: OPENCODE_APPROVAL_EVENT, delivery: "approval", timeout: HOOK_BUDGETS.opencode.budgetSeconds },
315
+ { event: "tool.execute.after", delivery: "telemetry", timeout: 10 },
316
+ { event: "chat.message", delivery: "telemetry", timeout: 10 },
317
+ { event: OPENCODE_IDLE_EVENT, delivery: "telemetry", timeout: 10 }
318
+ ];
319
+ var hasForeignPlugins = (entries) => entries.some((entry) => entry !== OPENCODE_PLUGIN_FILE && /\.(ts|js)$/.test(entry));
320
+ var installOpenCodePlugin = (argv, dir = openCodePluginDir()) => {
321
+ mkdirSync(dir, { recursive: true });
322
+ const plugin = join(dir, OPENCODE_PLUGIN_FILE);
323
+ writeFileSync(plugin, renderOpenCodePlugin(openCodePluginModule(), argv), "utf-8");
324
+ const sibling = join(dir, OPENCODE_PLUGIN_SIBLING);
325
+ if (existsSync(sibling)) return { plugin, wroteSibling: false };
326
+ writeFileSync(sibling, OPENCODE_PLUGIN_SIBLING_CONTENTS, "utf-8");
327
+ return { plugin, wroteSibling: true };
328
+ };
329
+ var openCodeWired = (dir = openCodePluginDir()) => {
330
+ try {
331
+ return isOpenCodePluginOurs(readFileSync(join(dir, OPENCODE_PLUGIN_FILE), "utf-8"));
332
+ } catch {
333
+ return false;
334
+ }
335
+ };
336
+ var OPENCODE_MCP_URL = "https://pushary.com/api/mcp/mcp";
337
+ var OPENCODE_CONFIG_FILE = "opencode.json";
338
+ var openCodeConfigFile = () => join(openCodeConfigDir(), OPENCODE_CONFIG_FILE);
339
+ var OPENCODE_INSTRUCTIONS_FILE = "AGENTS.md";
340
+ var openCodeInstructionsFile = () => join(openCodeConfigDir(), OPENCODE_INSTRUCTIONS_FILE);
341
+ var asRecord = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
342
+ var addOpenCodeMcpServer = (config, apiKey) => {
343
+ const servers = asRecord(config.mcp) ?? {};
344
+ servers.pushary = {
345
+ type: "remote",
346
+ url: OPENCODE_MCP_URL,
347
+ enabled: true,
348
+ headers: { Authorization: `Bearer ${apiKey}` }
349
+ };
350
+ config.mcp = servers;
351
+ };
352
+ var removeOpenCodeMcpServer = (config) => {
353
+ const servers = asRecord(config.mcp);
354
+ if (!servers || !("pushary" in servers)) return false;
355
+ delete servers.pushary;
356
+ if (Object.keys(servers).length === 0) delete config.mcp;
357
+ return true;
358
+ };
359
+
360
+ export {
361
+ OPENCODE_PLUGIN_PLACEHOLDER,
362
+ OPENCODE_PLUGIN_SIBLING,
363
+ OPENCODE_PLUGIN_SIBLING_CONTENTS,
364
+ isOpenCodePluginOurs,
365
+ OPENCODE_HOOK_BINARY,
366
+ openCodeConfigDirFrom,
367
+ openCodeConfigDir,
368
+ openCodePluginDir,
369
+ openCodePluginFile,
370
+ hasForeignPlugins,
371
+ installOpenCodePlugin,
372
+ openCodeWired,
373
+ openCodeConfigFile,
374
+ openCodeInstructionsFile,
375
+ addOpenCodeMcpServer,
376
+ removeOpenCodeMcpServer
377
+ };
@@ -0,0 +1,197 @@
1
+ import {
2
+ CLAUDE_HOOK_EVENTS,
3
+ supportedClaudeEvents
4
+ } from "./chunk-3EVPNIBE.js";
5
+ import {
6
+ guardBinary,
7
+ holdBudgetOf,
8
+ isProtectedByScope,
9
+ registerHookEntry
10
+ } from "./chunk-A2A2YTMG.js";
11
+ import {
12
+ HOOK_LOCATOR_BINARY,
13
+ withHookHoldBudget
14
+ } from "./chunk-NQTHFF4W.js";
15
+
16
+ // src/claude-config.ts
17
+ import { join } from "path";
18
+ import { isDeepStrictEqual } from "util";
19
+ var PUSHARY_MCP_URL = "https://pushary.com/api/mcp/mcp";
20
+ var PUSHARY_PERMISSION_RULE = "mcp__pushary__*";
21
+ var asRecord = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
22
+ var ensureRecord = (target, key) => {
23
+ const existing = asRecord(target[key]);
24
+ if (existing) return existing;
25
+ const created = {};
26
+ target[key] = created;
27
+ return created;
28
+ };
29
+ var isPusharyPermission = (rule) => typeof rule === "string" && (rule.includes("pushary") || rule.includes("MCP(pushary"));
30
+ var HOOK_BINARIES = /* @__PURE__ */ new Set([
31
+ ...CLAUDE_HOOK_EVENTS.map((hook) => hook.binary),
32
+ HOOK_LOCATOR_BINARY
33
+ ]);
34
+ var namesPusharyBinary = (command) => command.split(/[\s/\\"']+/).some((segment) => HOOK_BINARIES.has(segment.replace(/\.(cmd|exe)$/i, "")));
35
+ var isPusharyHook = (entry2, scope) => {
36
+ const hooks = asRecord(entry2)?.hooks;
37
+ if (!Array.isArray(hooks)) return false;
38
+ return hooks.some((hook) => {
39
+ const command = String(asRecord(hook)?.command ?? "");
40
+ if (isProtectedByScope(command, scope)) return false;
41
+ return namesPusharyBinary(command);
42
+ });
43
+ };
44
+ var pusharyMcpServer = (apiKey) => ({
45
+ type: "http",
46
+ url: PUSHARY_MCP_URL,
47
+ headers: { Authorization: `Bearer ${apiKey}` }
48
+ });
49
+ var addClaudeMcpServer = (config, apiKey) => {
50
+ const mcpServers = ensureRecord(config, "mcpServers");
51
+ mcpServers.pushary = pusharyMcpServer(apiKey);
52
+ };
53
+ var isClaudeMcpSnapshot = (value) => {
54
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
55
+ const snapshot = value;
56
+ return typeof snapshot.hadMcpServers === "boolean" && typeof snapshot.mcpServersWasRecord === "boolean" && (!snapshot.hadMcpServers || snapshot.mcpServersWasRecord || Object.hasOwn(snapshot, "mcpServers")) && typeof snapshot.hadPushary === "boolean" && (!snapshot.hadPushary || Object.hasOwn(snapshot, "pushary")) && (!Object.hasOwn(snapshot, "installedPushary") || asRecord(snapshot.installedPushary)?.type === "http" && asRecord(snapshot.installedPushary)?.url === PUSHARY_MCP_URL);
57
+ };
58
+ var captureClaudeMcpServer = (config, apiKey) => {
59
+ const mcpServers = asRecord(config.mcpServers);
60
+ const hadMcpServers = Object.hasOwn(config, "mcpServers");
61
+ return {
62
+ hadMcpServers,
63
+ mcpServersWasRecord: Boolean(mcpServers),
64
+ ...hadMcpServers && !mcpServers ? { mcpServers: structuredClone(config.mcpServers) } : {},
65
+ hadPushary: Boolean(mcpServers && Object.hasOwn(mcpServers, "pushary")),
66
+ ...mcpServers && Object.hasOwn(mcpServers, "pushary") ? { pushary: structuredClone(mcpServers.pushary) } : {},
67
+ ...apiKey ? { installedPushary: pusharyMcpServer(apiKey) } : {}
68
+ };
69
+ };
70
+ var updateClaudeMcpSnapshot = (snapshot, apiKey) => ({
71
+ ...snapshot,
72
+ installedPushary: pusharyMcpServer(apiKey)
73
+ });
74
+ var restoreClaudeMcpServer = (config, snapshot) => {
75
+ const mcpServers = asRecord(config.mcpServers);
76
+ const current = asRecord(mcpServers?.pushary);
77
+ if (Object.hasOwn(snapshot, "installedPushary")) {
78
+ if (!isDeepStrictEqual(mcpServers?.pushary, snapshot.installedPushary)) return false;
79
+ } else if (current?.type !== "http" || current.url !== PUSHARY_MCP_URL) {
80
+ return false;
81
+ }
82
+ if (!snapshot.mcpServersWasRecord) {
83
+ if (Object.keys(mcpServers).length === 1) {
84
+ if (snapshot.hadMcpServers) config.mcpServers = structuredClone(snapshot.mcpServers);
85
+ else delete config.mcpServers;
86
+ } else {
87
+ delete mcpServers.pushary;
88
+ }
89
+ } else if (snapshot.hadPushary) {
90
+ mcpServers.pushary = structuredClone(snapshot.pushary);
91
+ } else {
92
+ delete mcpServers.pushary;
93
+ if (!snapshot.hadMcpServers && Object.keys(mcpServers).length === 0) delete config.mcpServers;
94
+ }
95
+ return true;
96
+ };
97
+ var removeClaudeMcpServers = (config) => {
98
+ let changed = false;
99
+ const removeFrom = (target) => {
100
+ const mcpServers = asRecord(target.mcpServers);
101
+ if (!mcpServers?.pushary) return;
102
+ delete mcpServers.pushary;
103
+ if (Object.keys(mcpServers).length === 0) delete target.mcpServers;
104
+ changed = true;
105
+ };
106
+ removeFrom(config);
107
+ const projects = asRecord(config.projects);
108
+ if (projects) {
109
+ for (const project of Object.values(projects)) {
110
+ const projectConfig = asRecord(project);
111
+ if (projectConfig) removeFrom(projectConfig);
112
+ }
113
+ }
114
+ return changed;
115
+ };
116
+ var removePusharyToolPermissions = (settings) => {
117
+ const permissions = asRecord(settings.permissions);
118
+ if (!permissions || !Array.isArray(permissions.allow)) return false;
119
+ const filtered = permissions.allow.filter((rule) => !isPusharyPermission(rule));
120
+ if (filtered.length === permissions.allow.length) return false;
121
+ if (filtered.length === 0) delete permissions.allow;
122
+ else permissions.allow = filtered;
123
+ if (Object.keys(permissions).length === 0) delete settings.permissions;
124
+ return true;
125
+ };
126
+ var addPusharyToolPermissions = (settings) => {
127
+ const permissions = ensureRecord(settings, "permissions");
128
+ const allow = Array.isArray(permissions.allow) ? permissions.allow : [];
129
+ const filtered = allow.filter((rule) => !isPusharyPermission(rule));
130
+ if (!filtered.includes(PUSHARY_PERMISSION_RULE)) {
131
+ filtered.push(PUSHARY_PERMISSION_RULE);
132
+ }
133
+ permissions.allow = filtered;
134
+ };
135
+ var addPusharyHooks = (settings, binDir, version = null) => {
136
+ const resolve = (name) => guardBinary(binDir ? join(binDir, name) : name);
137
+ const hooks = ensureRecord(settings, "hooks");
138
+ const supported = new Map(supportedClaudeEvents(version).map((hook) => [hook.event, hook]));
139
+ for (const known of CLAUDE_HOOK_EVENTS) {
140
+ const current = Array.isArray(hooks[known.event]) ? hooks[known.event] : [];
141
+ const definition = supported.get(known.event);
142
+ if (definition) {
143
+ hooks[known.event] = registerHookEntry(current, isPusharyHook, entry(definition, resolve));
144
+ continue;
145
+ }
146
+ const others = current.filter((item) => !isPusharyHook(item));
147
+ if (others.length > 0) {
148
+ hooks[known.event] = others;
149
+ } else {
150
+ delete hooks[known.event];
151
+ }
152
+ }
153
+ };
154
+ var entry = (definition, resolve) => ({
155
+ ...definition.matcher ? { matcher: definition.matcher } : {},
156
+ hooks: [{
157
+ type: "command",
158
+ command: withHookHoldBudget(resolve(definition.binary), holdBudgetOf(definition)),
159
+ timeout: definition.timeout
160
+ }]
161
+ });
162
+ var removePusharySettings = (settings, scope) => {
163
+ let changed = scope?.keepMcpServer === true ? false : removeClaudeMcpServers(settings);
164
+ if (scope?.keepMcpServer !== true && removePusharyToolPermissions(settings)) changed = true;
165
+ const hooks = asRecord(settings.hooks);
166
+ if (hooks) {
167
+ for (const { event: key } of CLAUDE_HOOK_EVENTS) {
168
+ const entries = hooks[key];
169
+ if (!Array.isArray(entries)) continue;
170
+ const filtered = entries.filter((entry2) => !isPusharyHook(entry2, scope));
171
+ if (filtered.length !== entries.length) {
172
+ if (filtered.length === 0) {
173
+ delete hooks[key];
174
+ } else {
175
+ hooks[key] = filtered;
176
+ }
177
+ changed = true;
178
+ }
179
+ }
180
+ if (Object.keys(hooks).length === 0) delete settings.hooks;
181
+ }
182
+ return changed;
183
+ };
184
+
185
+ export {
186
+ isPusharyHook,
187
+ addClaudeMcpServer,
188
+ isClaudeMcpSnapshot,
189
+ captureClaudeMcpServer,
190
+ updateClaudeMcpSnapshot,
191
+ restoreClaudeMcpServer,
192
+ removeClaudeMcpServers,
193
+ removePusharyToolPermissions,
194
+ addPusharyToolPermissions,
195
+ addPusharyHooks,
196
+ removePusharySettings
197
+ };
@@ -0,0 +1,86 @@
1
+ import {
2
+ HOOK_LOCATOR_BINARY
3
+ } from "./chunk-NQTHFF4W.js";
4
+
5
+ // src/diagnostics/bridge.ts
6
+ import { execFileSync } from "child_process";
7
+ import { existsSync, readFileSync } from "fs";
8
+ import { homedir } from "os";
9
+ import { dirname, join } from "path";
10
+ var BRIDGE_BUNDLE_IDENTIFIER = "com.pushary.app";
11
+ var BRIDGE_HELPER_SUFFIX = "/Contents/Helpers/pushary-bridge";
12
+ var BRIDGE_BUNDLE_ROOTS = ["/Applications/Pushary.app", "$HOME/Applications/Pushary.app"];
13
+ var expandHome = (path, home) => path.replaceAll("$HOME", home);
14
+ var parseBridgeStub = (contents, stubPath) => {
15
+ const helperSuffix = contents.match(/^H=(\S+)$/m)?.[1] ?? BRIDGE_HELPER_SUFFIX;
16
+ const cachePath = contents.match(/^C="([^"\n]+)"$/m)?.[1] ?? join(dirname(stubPath), ".bridge-cache");
17
+ const rootsLine = contents.match(/^for P in ((?:"[^"\n]*"\s*)+); do$/m)?.[1];
18
+ const bundleRoots = rootsLine ? [...rootsLine.matchAll(/"([^"]*)"/g)].map((match) => match[1]) : BRIDGE_BUNDLE_ROOTS;
19
+ const bundleIdentifier = contents.match(/kMDItemCFBundleIdentifier == "([^"]+)"/)?.[1] ?? BRIDGE_BUNDLE_IDENTIFIER;
20
+ return { helperSuffix, cachePath, bundleRoots, bundleIdentifier };
21
+ };
22
+ var resolveBridgeStub = (stubPath, probe) => {
23
+ const contents = probe.readFile(stubPath);
24
+ if (contents === null) return { kind: "no-stub", stub: stubPath };
25
+ const shape = parseBridgeStub(contents, stubPath);
26
+ const helperIn = (bundle) => `${bundle}${shape.helperSuffix}`;
27
+ const looked = [];
28
+ for (const root of shape.bundleRoots.map((root2) => expandHome(root2, probe.home))) {
29
+ looked.push(root);
30
+ if (probe.exists(helperIn(root))) return { kind: "bundle", helper: helperIn(root), via: "root" };
31
+ }
32
+ const cached = shape.cachePath ? probe.readFile(shape.cachePath)?.split("\n")[0]?.trim() : void 0;
33
+ if (cached) {
34
+ looked.push(cached);
35
+ if (probe.exists(helperIn(cached))) return { kind: "bundle", helper: helperIn(cached), via: "cache" };
36
+ }
37
+ for (const found of probe.mdfind(shape.bundleIdentifier)) {
38
+ looked.push(found);
39
+ if (probe.exists(helperIn(found))) return { kind: "bundle", helper: helperIn(found), via: "spotlight" };
40
+ }
41
+ return { kind: "missing", stub: stubPath, looked };
42
+ };
43
+ var BRIDGE_GONE_MESSAGE = "the Mac app is gone but its hooks remain; run pushary setup or reinstall the app";
44
+ var describeBridgeResolution = (resolution) => {
45
+ switch (resolution.kind) {
46
+ case "bundle":
47
+ return `${resolution.helper} (${resolution.via === "root" ? "installed" : resolution.via === "cache" ? "cached path" : "found by Spotlight"})`;
48
+ case "missing":
49
+ return BRIDGE_GONE_MESSAGE;
50
+ case "no-stub":
51
+ return `${resolution.stub} is not there; ${BRIDGE_GONE_MESSAGE}`;
52
+ }
53
+ };
54
+ var isBridgeLocator = (executable) => executable !== null && executable.split(/[/\\]/).pop() === HOOK_LOCATOR_BINARY;
55
+ var spotlight = (bundleIdentifier) => {
56
+ if (process.platform !== "darwin") return [];
57
+ try {
58
+ return execFileSync("/usr/bin/mdfind", [`kMDItemCFBundleIdentifier == "${bundleIdentifier}"`], {
59
+ encoding: "utf-8",
60
+ timeout: 5e3,
61
+ stdio: ["ignore", "pipe", "ignore"]
62
+ }).split("\n").map((line) => line.trim()).filter((line) => line !== "");
63
+ } catch {
64
+ return [];
65
+ }
66
+ };
67
+ var readOrNull = (path) => {
68
+ try {
69
+ return readFileSync(path, "utf-8");
70
+ } catch {
71
+ return null;
72
+ }
73
+ };
74
+ var liveBridgeProbe = () => ({
75
+ exists: existsSync,
76
+ readFile: readOrNull,
77
+ mdfind: spotlight,
78
+ home: homedir()
79
+ });
80
+
81
+ export {
82
+ resolveBridgeStub,
83
+ describeBridgeResolution,
84
+ isBridgeLocator,
85
+ liveBridgeProbe
86
+ };