@pushary/agent-hooks 0.87.1 → 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.
Files changed (63) hide show
  1. package/data/cursor-plugin/hooks/hooks.json +7 -0
  2. package/data/cursor-plugin/scripts/pushary-gate.mjs +85 -28
  3. package/data/cursor-plugin/scripts/pushary-gate.test.mjs +42 -1
  4. package/dist/bin/pushary-bell-hook.js +4 -4
  5. package/dist/bin/pushary-bell.js +7 -6
  6. package/dist/bin/pushary-claude.js +14 -11
  7. package/dist/bin/pushary-clean.js +36 -20
  8. package/dist/bin/pushary-codex-bridge.js +29 -11
  9. package/dist/bin/pushary-codex-hook.js +129 -52
  10. package/dist/bin/pushary-codex.js +4 -3
  11. package/dist/bin/pushary-connect.js +6 -6
  12. package/dist/bin/pushary-daemon.js +5 -4
  13. package/dist/bin/pushary-disconnect.js +65 -8
  14. package/dist/bin/pushary-doctor.js +79 -18
  15. package/dist/bin/pushary-elicitation-hook.js +1 -1
  16. package/dist/bin/pushary-gemini-bridge.js +6 -5
  17. package/dist/bin/pushary-gemini-hook.js +12 -10
  18. package/dist/bin/pushary-hook.js +6 -5
  19. package/dist/bin/pushary-login.js +4 -4
  20. package/dist/bin/pushary-logout.js +5 -4
  21. package/dist/bin/pushary-mode.js +3 -3
  22. package/dist/bin/pushary-notification-hook.js +4 -3
  23. package/dist/bin/pushary-opencode-hook.d.ts +1 -0
  24. package/dist/bin/pushary-opencode-hook.js +329 -0
  25. package/dist/bin/pushary-permission-denied-hook.js +6 -5
  26. package/dist/bin/pushary-permission-hook.js +6 -5
  27. package/dist/bin/pushary-post-hook.js +4 -3
  28. package/dist/bin/pushary-prompt-hook.js +4 -3
  29. package/dist/bin/pushary-session-end-hook.js +4 -3
  30. package/dist/bin/pushary-session-start-hook.js +4 -3
  31. package/dist/bin/pushary-setup.js +240 -85
  32. package/dist/bin/pushary-stats.js +2 -2
  33. package/dist/bin/pushary-status.js +5 -5
  34. package/dist/bin/pushary-stop-hook.js +4 -3
  35. package/dist/bin/pushary-stopfailure-hook.js +4 -3
  36. package/dist/bin/pushary-upgrade.js +7 -6
  37. package/dist/bin/pushary-wait.js +3 -3
  38. package/dist/{chunk-5646VKR4.js → chunk-2JYQAW4B.js} +5 -53
  39. package/dist/chunk-7J67NYK4.js +287 -0
  40. package/dist/{chunk-M7RYIZX6.js → chunk-B7ZBHWOW.js} +10 -1
  41. package/dist/{chunk-7YJS2VTG.js → chunk-DMW54VWK.js} +12 -1
  42. package/dist/{chunk-LT2VLPUK.js → chunk-DPUOW6NS.js} +18 -2
  43. package/dist/{chunk-A3PFIWWN.js → chunk-EJL3PJU7.js} +56 -6
  44. package/dist/chunk-GIRT7677.js +11 -0
  45. package/dist/{chunk-HAUTLCMA.js → chunk-ICERLAE2.js} +1 -1
  46. package/dist/{chunk-ZZE6FN7T.js → chunk-K42RXJPG.js} +52 -1
  47. package/dist/{chunk-Y3UTTFTZ.js → chunk-KBS53WOE.js} +1 -1
  48. package/dist/{chunk-SJVLFIYP.js → chunk-KCAFCGGY.js} +1 -1
  49. package/dist/{chunk-A4I5JGCW.js → chunk-KFAH6KRE.js} +1 -1
  50. package/dist/{chunk-QBWYNVKY.js → chunk-LGVHYFIB.js} +1 -1
  51. package/dist/{chunk-HJ44HIUW.js → chunk-OMA2OZRE.js} +11 -9
  52. package/dist/{chunk-ECEUMWBH.js → chunk-QLMX5LLV.js} +1 -1
  53. package/dist/{chunk-XW5CHAQX.js → chunk-TAYHEJFF.js} +2 -2
  54. package/dist/{chunk-JAICP7BA.js → chunk-TD3M232T.js} +1 -1
  55. package/dist/{chunk-WLLBFDVY.js → chunk-TV3UFGT7.js} +1 -1
  56. package/dist/{chunk-7MBDFNUC.js → chunk-VIHRP25S.js} +1 -1
  57. package/dist/{chunk-B3V2H6SA.js → chunk-VL5ZNK6V.js} +43 -12
  58. package/dist/{chunk-V2XPF77V.js → chunk-VOE4SYPR.js} +42 -279
  59. package/dist/{chunk-OT4YD54Q.js → chunk-YX6ZKCC5.js} +1 -1
  60. package/dist/{chunk-2T3GX7WG.js → chunk-YYTLTEGU.js} +1 -1
  61. package/dist/chunk-ZGC3W23N.js +371 -0
  62. package/dist/src/index.js +6 -5
  63. package/package.json +2 -1
@@ -6,19 +6,20 @@ import {
6
6
  postLiveSession,
7
7
  postLiveSessionUntilAccepted,
8
8
  startLiveSessionHeartbeat
9
- } from "../chunk-WLLBFDVY.js";
10
- import "../chunk-ECEUMWBH.js";
9
+ } from "../chunk-TV3UFGT7.js";
10
+ import "../chunk-QLMX5LLV.js";
11
11
  import {
12
12
  startProcessOwnership,
13
13
  withSpawnProcessOwnership
14
- } from "../chunk-QBWYNVKY.js";
14
+ } from "../chunk-LGVHYFIB.js";
15
15
  import {
16
16
  spawnClaude
17
17
  } from "../chunk-XHKBHWLX.js";
18
18
  import {
19
19
  resolveBinary
20
20
  } from "../chunk-VT4IVERX.js";
21
- import "../chunk-B3V2H6SA.js";
21
+ import "../chunk-VL5ZNK6V.js";
22
+ import "../chunk-GIRT7677.js";
22
23
  import {
23
24
  getMachineId
24
25
  } from "../chunk-RN3NOEJF.js";
@@ -27,7 +28,7 @@ import {
27
28
  getApiKey,
28
29
  getBaseUrl
29
30
  } from "../chunk-2UMNXADU.js";
30
- import "../chunk-7YJS2VTG.js";
31
+ import "../chunk-DMW54VWK.js";
31
32
 
32
33
  // bin/pushary-codex-bridge.ts
33
34
  import { existsSync } from "fs";
@@ -36,6 +37,8 @@ import { existsSync } from "fs";
36
37
  import { spawn } from "child_process";
37
38
  import { basename } from "path";
38
39
  var MAX_RPC_LINE = 1024 * 1024;
40
+ var STDERR_TAIL_LIMIT = 2e3;
41
+ var FAILURE_MESSAGE_MAX = 240;
39
42
  var MAX_PENDING_RPC = 8;
40
43
  var MAX_TRANSCRIPT_TEXT = 32 * 1024;
41
44
  var MAX_MESSAGE_BUFFERS = 16;
@@ -46,20 +49,34 @@ var JsonRpcClient = class {
46
49
  nextId = 1;
47
50
  buffer = "";
48
51
  closed = false;
52
+ stderrTail = "";
53
+ failure = null;
49
54
  constructor(command, cwd, timeoutMs) {
50
55
  const options = { cwd, env: process.env, stdio: ["pipe", "pipe", "pipe"] };
51
56
  this.child = process.platform === "win32" ? spawnClaude(command.command, command.args ?? [], options) : spawn(command.command, command.args ?? [], options);
52
57
  this.child.stdout?.setEncoding("utf8");
53
58
  this.child.stdout?.on("data", (chunk) => this.read(String(chunk)));
54
- this.child.stderr?.resume();
59
+ this.child.stderr?.setEncoding("utf8");
60
+ this.child.stderr?.on("data", (chunk) => this.keepStderrTail(String(chunk)));
55
61
  this.child.once("error", (error) => this.fail(error));
56
- this.child.once("exit", () => this.fail(new Error("Codex app-server exited")));
62
+ this.child.once("exit", (code, signal) => this.fail(new Error(this.exitMessage(code, signal))));
57
63
  this.timeoutMs = timeoutMs;
58
64
  }
65
+ keepStderrTail(chunk) {
66
+ this.stderrTail = (this.stderrTail + chunk).slice(-STDERR_TAIL_LIMIT);
67
+ }
68
+ exitMessage(code, signal) {
69
+ const how = signal ? `signal ${signal}` : `code ${code ?? "unknown"}`;
70
+ const tail = this.stderrTail.trim();
71
+ return tail ? `Codex app-server exited (${how}): ${tail}` : `Codex app-server exited (${how})`;
72
+ }
59
73
  timeoutMs;
60
74
  get isClosed() {
61
75
  return this.closed;
62
76
  }
77
+ get closedReason() {
78
+ return this.failure?.message ?? "Codex app-server exited";
79
+ }
63
80
  onNotification(listener) {
64
81
  this.notifications.add(listener);
65
82
  return () => this.notifications.delete(listener);
@@ -142,6 +159,7 @@ var JsonRpcClient = class {
142
159
  fail(error) {
143
160
  if (this.closed) return;
144
161
  this.closed = true;
162
+ this.failure = error;
145
163
  this.rejectPending(error);
146
164
  if (this.child.exitCode == null) this.child.kill();
147
165
  }
@@ -281,7 +299,7 @@ var runCodexBridge = async (options) => {
281
299
  await relay?.flush();
282
300
  await liveSessionSleep(options.pollIntervalMs ?? 2e3, options.signal);
283
301
  }
284
- if (rpc.isClosed && !options.signal.aborted) throw new Error("Codex app-server exited");
302
+ if (rpc.isClosed && !options.signal.aborted) throw new Error(rpc.closedReason);
285
303
  if (activeTurnId) {
286
304
  await rpc.request("turn/interrupt", { threadId, turnId: activeTurnId }).catch(() => null);
287
305
  }
@@ -289,8 +307,8 @@ var runCodexBridge = async (options) => {
289
307
  await report("session_closed", "Session stopped").catch(() => null);
290
308
  return "stopped";
291
309
  } catch (error) {
310
+ const message = error instanceof Error ? error.message : String(error);
292
311
  if (!linked && options.claimId) {
293
- const message = error instanceof Error ? error.message : String(error);
294
312
  for (let attempt = 0; attempt < 3; attempt++) {
295
313
  const acknowledged = await postLiveSession(options, "/api/agent/spawn/ack", {
296
314
  spawnId: options.spawnId,
@@ -299,7 +317,7 @@ var runCodexBridge = async (options) => {
299
317
  state: "failed",
300
318
  failure: {
301
319
  code: "bridge_failed",
302
- message: message.slice(0, 240),
320
+ message: message.slice(0, FAILURE_MESSAGE_MAX),
303
321
  retryable: false
304
322
  }
305
323
  }).catch(() => null);
@@ -307,7 +325,7 @@ var runCodexBridge = async (options) => {
307
325
  await liveSessionSleep(200 * (attempt + 1), options.signal);
308
326
  }
309
327
  }
310
- await report("error", "Live bridge failed").catch(() => null);
328
+ await report("error", `Live bridge failed: ${message}`.slice(0, FAILURE_MESSAGE_MAX)).catch(() => null);
311
329
  return "failed";
312
330
  } finally {
313
331
  stopHeartbeat();
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  isGatingMoment,
4
4
  recordKeylessMoment
5
- } from "../chunk-7MBDFNUC.js";
5
+ } from "../chunk-VIHRP25S.js";
6
6
  import {
7
7
  readHookInput
8
8
  } from "../chunk-6CUUA7HO.js";
@@ -10,19 +10,19 @@ import {
10
10
  denyReasonFrom,
11
11
  isDeferAnswer
12
12
  } from "../chunk-YHG74UFF.js";
13
+ import {
14
+ canonicalJson
15
+ } from "../chunk-GIRT7677.js";
13
16
  import {
14
17
  CODEX_AGENT,
15
18
  codexAllow,
16
19
  codexDeny,
17
20
  codexPass,
18
- deriveActionBody,
19
- deriveBlocker,
20
- deriveToolTarget,
21
21
  describeApplyPatch,
22
- describeToolCall,
23
22
  fetchModeState,
24
23
  getPolicy,
25
24
  handlePostToolUse,
25
+ handleSessionEnd,
26
26
  handleStop,
27
27
  handleUserPrompt,
28
28
  permissionTimeoutDecision,
@@ -31,11 +31,10 @@ import {
31
31
  readLastPrompt,
32
32
  repoKeyFor,
33
33
  reportEvent,
34
- scopePathFor,
35
34
  toCodexWire,
36
35
  toPolicyLookup,
37
36
  toPolicyLookups
38
- } from "../chunk-V2XPF77V.js";
37
+ } from "../chunk-VOE4SYPR.js";
39
38
  import {
40
39
  DEFAULT_SESSION,
41
40
  askUser,
@@ -44,12 +43,19 @@ import {
44
43
  savePendingQuestion,
45
44
  sendNotification
46
45
  } from "../chunk-J3YDT4HM.js";
46
+ import {
47
+ deriveActionBody,
48
+ deriveBlocker,
49
+ deriveToolTarget,
50
+ describeToolCall,
51
+ scopePathFor
52
+ } from "../chunk-7J67NYK4.js";
47
53
  import {
48
54
  getMachineId
49
55
  } from "../chunk-RN3NOEJF.js";
50
56
  import "../chunk-BSZYIAZL.js";
51
57
  import "../chunk-SAF6HGAA.js";
52
- import "../chunk-Y3UTTFTZ.js";
58
+ import "../chunk-KBS53WOE.js";
53
59
  import "../chunk-7QLSKOSU.js";
54
60
  import "../chunk-KZERVKTD.js";
55
61
  import {
@@ -62,57 +68,107 @@ import {
62
68
  hookWaitClamped,
63
69
  hookWaitDeadline,
64
70
  resolveGate
65
- } from "../chunk-7YJS2VTG.js";
71
+ } from "../chunk-DMW54VWK.js";
66
72
 
67
73
  // bin/pushary-codex-hook.ts
68
- import { basename, join } from "path";
74
+ import { basename } from "path";
75
+
76
+ // src/codex-call-decision.ts
77
+ import { createHash } from "crypto";
78
+ import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "fs";
69
79
  import { tmpdir, userInfo } from "os";
70
- import { existsSync, mkdirSync, statSync, unlinkSync, writeFileSync } from "fs";
71
- var START_MS = Date.now();
72
- var APPROVAL_TTL_MS = 10 * 60 * 1e3;
73
- var sanitizeId = (value) => value.replace(/[^A-Za-z0-9_-]/g, "_").slice(0, 128);
80
+ import { join } from "path";
81
+ var CODEX_DECISION_TTL_MS = 60 * 1e3;
74
82
  var userScope = () => {
75
83
  try {
76
84
  const info = userInfo();
77
85
  const id = typeof info.uid === "number" && info.uid >= 0 ? String(info.uid) : info.username;
78
- return sanitizeId(id) || "default";
86
+ return id.replace(/[^A-Za-z0-9_-]/g, "_").slice(0, 128) || "default";
79
87
  } catch {
80
88
  return "default";
81
89
  }
82
90
  };
83
- var APPROVAL_DIR = join(tmpdir(), `pushary-codex-approvals-${userScope()}`);
84
- var markApproved = (toolUseId) => {
85
- if (!toolUseId) return;
91
+ var decisionDir = () => join(tmpdir(), `pushary-codex-decisions-${userScope()}`);
92
+ var decisionFile = (key) => join(decisionDir(), key);
93
+ var PRESENTATION_ONLY_INPUT_KEYS = ["description"];
94
+ var identifyingToolInput = (toolInput) => {
95
+ if (!toolInput) return {};
96
+ const identifying = {};
97
+ for (const [key, value] of Object.entries(toolInput)) {
98
+ if (PRESENTATION_ONLY_INPUT_KEYS.includes(key)) continue;
99
+ identifying[key] = value;
100
+ }
101
+ return identifying;
102
+ };
103
+ var codexCallKey = (identity) => {
104
+ if (!identity.sessionId || !identity.turnId || !identity.toolName) return null;
105
+ const material = canonicalJson([
106
+ identity.sessionId,
107
+ identity.turnId,
108
+ identity.toolName,
109
+ identifyingToolInput(identity.toolInput)
110
+ ]);
111
+ return createHash("sha256").update(material, "utf-8").digest("hex").slice(0, 32);
112
+ };
113
+ var parseDecision = (raw) => {
114
+ try {
115
+ const parsed = JSON.parse(raw);
116
+ if (parsed.kind === "approved") return { kind: "approved" };
117
+ if (parsed.kind === "handled") return { kind: "handled" };
118
+ if (parsed.kind === "denied") {
119
+ return { kind: "denied", reason: typeof parsed.reason === "string" ? parsed.reason : "" };
120
+ }
121
+ return null;
122
+ } catch {
123
+ return null;
124
+ }
125
+ };
126
+ var sweepExpired = (dir, nowMs) => {
86
127
  try {
87
- if (!existsSync(APPROVAL_DIR)) mkdirSync(APPROVAL_DIR, { recursive: true, mode: 448 });
88
- writeFileSync(join(APPROVAL_DIR, sanitizeId(toolUseId)), "", { encoding: "utf-8", mode: 384 });
128
+ for (const name of readdirSync(dir)) {
129
+ const path = join(dir, name);
130
+ try {
131
+ if (nowMs - statSync(path).mtimeMs >= CODEX_DECISION_TTL_MS) rmSync(path, { force: true });
132
+ } catch {
133
+ continue;
134
+ }
135
+ }
89
136
  } catch {
137
+ return;
90
138
  }
91
139
  };
92
- var consumeApproval = (toolUseId) => {
93
- if (!toolUseId) return false;
94
- const path = join(APPROVAL_DIR, sanitizeId(toolUseId));
140
+ var recordCodexCallDecision = (key, decision) => {
141
+ if (!key) return;
95
142
  try {
96
- const fresh = Date.now() - statSync(path).mtimeMs < APPROVAL_TTL_MS;
97
- unlinkSync(path);
98
- return fresh;
143
+ const dir = decisionDir();
144
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true, mode: 448 });
145
+ else sweepExpired(dir, Date.now());
146
+ writeFileSync(decisionFile(key), JSON.stringify(decision), { encoding: "utf-8", mode: 384 });
99
147
  } catch {
100
- return false;
148
+ return;
101
149
  }
102
150
  };
103
- var NOTIFY_TTL_MS = 60 * 1e3;
104
- var claimNotify = (toolUseId) => {
105
- if (!toolUseId) return true;
106
- const path = join(APPROVAL_DIR, `notify-${sanitizeId(toolUseId)}`);
151
+ var consumeCodexCallDecision = (key) => {
152
+ if (!key) return null;
153
+ const path = decisionFile(key);
107
154
  try {
108
- if (existsSync(path) && Date.now() - statSync(path).mtimeMs < NOTIFY_TTL_MS) return false;
109
- if (!existsSync(APPROVAL_DIR)) mkdirSync(APPROVAL_DIR, { recursive: true, mode: 448 });
110
- writeFileSync(path, "", { encoding: "utf-8", mode: 384 });
111
- return true;
155
+ const fresh = Date.now() - statSync(path).mtimeMs < CODEX_DECISION_TTL_MS;
156
+ const raw = readFileSync(path, "utf-8");
157
+ rmSync(path, { force: true });
158
+ return fresh ? parseDecision(raw) : null;
112
159
  } catch {
113
- return true;
160
+ return null;
114
161
  }
115
162
  };
163
+
164
+ // bin/pushary-codex-hook.ts
165
+ var callKeyFor = (input) => codexCallKey({
166
+ sessionId: input.session_id,
167
+ turnId: input.turn_id,
168
+ toolName: input.tool_name,
169
+ toolInput: input.tool_input
170
+ });
171
+ var START_MS = Date.now();
116
172
  var agentNameFor = (input) => `Codex - ${basename(input.cwd ?? process.cwd())}`;
117
173
  var describeFor = (input, lookup) => input.tool_name === "apply_patch" ? describeApplyPatch(input.tool_input?.command) ?? describeToolCall(lookup.tool, lookup.input, "hook") : describeToolCall(lookup.tool, lookup.input, "hook");
118
174
  var pushQuestion = async (apiKey, input, lookup, waitSeconds, mode) => {
@@ -184,7 +240,14 @@ var codexGate = async (apiKey, input) => {
184
240
  });
185
241
  return { verdict, lookup };
186
242
  };
243
+ var fromRecordedDecision = (decision) => {
244
+ if (decision.kind === "approved") return codexAllow();
245
+ if (decision.kind === "denied") return codexDeny(decision.reason);
246
+ return codexPass();
247
+ };
187
248
  var decidePermissionRequest = async (input) => {
249
+ const recorded = consumeCodexCallDecision(callKeyFor(input));
250
+ if (recorded) return fromRecordedDecision(recorded);
188
251
  try {
189
252
  const apiKey = getApiKey();
190
253
  const { verdict, lookup } = await codexGate(apiKey, input);
@@ -195,7 +258,7 @@ var decidePermissionRequest = async (input) => {
195
258
  const toolPolicy = verdict.policy;
196
259
  if (toolPolicy.mode === "terminal_only") return codexPass();
197
260
  if (toolPolicy.mode === "notify_only") {
198
- if (claimNotify(input.tool_use_id)) await notifyApprovalNeeded(apiKey, input, lookup);
261
+ await notifyApprovalNeeded(apiKey, input, lookup);
199
262
  return codexPass();
200
263
  }
201
264
  const waitSeconds = toolPolicy.mode === "push_first" ? toolPolicy.pushFirstSeconds : effectiveWaitSeconds(toolPolicy.timeoutAction, toolPolicy.timeoutSeconds, "codex");
@@ -207,22 +270,24 @@ var decidePermissionRequest = async (input) => {
207
270
  }
208
271
  if (answer.answered) {
209
272
  if (isDeferAnswer(answer.value)) return codexPass();
210
- if (answer.value === "yes") {
211
- if (toolPolicy.mode === "push_only") markApproved(input.tool_use_id);
212
- return codexAllow();
213
- }
273
+ if (answer.value === "yes") return codexAllow();
214
274
  return codexDeny(denyReasonFrom(answer.value, answer.note));
215
275
  }
216
276
  savePendingQuestion(input.session_id || DEFAULT_SESSION, correlationId);
217
277
  if (toolPolicy.mode === "push_first") return codexPass();
218
278
  if (clampCut) return codexPass();
219
- const decision = permissionTimeoutDecision(toolPolicy.timeoutAction);
220
- if (decision.kind === "allow") markApproved(input.tool_use_id);
221
- return decision;
279
+ return permissionTimeoutDecision(toolPolicy.timeoutAction);
222
280
  } catch {
223
281
  return codexPass();
224
282
  }
225
283
  };
284
+ var recordPreToolUse = (key, outcome) => {
285
+ recordCodexCallDecision(
286
+ key,
287
+ outcome.kind === "deny" ? { kind: "denied", reason: outcome.reason } : { kind: "handled" }
288
+ );
289
+ return outcome;
290
+ };
226
291
  var decidePreToolUse = async (input) => {
227
292
  let apiKey;
228
293
  try {
@@ -249,30 +314,35 @@ var decidePreToolUse = async (input) => {
249
314
  if (verdict.kind === "deny") return codexDeny(verdict.reason);
250
315
  if (verdict.kind === "defer") return codexPass();
251
316
  const toolPolicy = verdict.policy;
317
+ const callKey = callKeyFor(input);
252
318
  if (toolPolicy.mode === "push_only") {
253
- if (consumeApproval(input.tool_use_id)) return codexPass();
254
319
  let pushed;
255
320
  try {
256
321
  pushed = await pushQuestion(apiKey, input, lookup, effectiveWaitSeconds(toolPolicy.timeoutAction, toolPolicy.timeoutSeconds, "codex"), "push_only");
257
322
  } catch {
258
- return preToolUseTimeoutDecision(toolPolicy.timeoutAction, "Push notification failed, denying per policy");
323
+ return recordPreToolUse(callKey, preToolUseTimeoutDecision(toolPolicy.timeoutAction, "Push notification failed, denying per policy"));
259
324
  }
260
325
  const { answer, correlationId, suppressed, clampCut } = pushed;
261
326
  if (suppressed) {
262
327
  await cancelQuestion(apiKey, correlationId).catch(() => {
263
328
  });
264
- return codexPass();
329
+ return recordPreToolUse(callKey, codexPass());
265
330
  }
266
331
  if (answer.answered) {
267
- if (isDeferAnswer(answer.value)) return codexPass();
268
- return answer.value === "yes" ? codexPass() : codexDeny(denyReasonFrom(answer.value, answer.note));
332
+ if (isDeferAnswer(answer.value)) return recordPreToolUse(callKey, codexPass());
333
+ if (answer.value === "yes") {
334
+ recordCodexCallDecision(callKey, { kind: "approved" });
335
+ return codexPass();
336
+ }
337
+ return recordPreToolUse(callKey, codexDeny(denyReasonFrom(answer.value, answer.note)));
269
338
  }
270
339
  savePendingQuestion(input.session_id || DEFAULT_SESSION, correlationId);
271
- if (clampCut) return codexPass();
272
- return preToolUseTimeoutDecision(toolPolicy.timeoutAction);
340
+ if (clampCut) return recordPreToolUse(callKey, codexPass());
341
+ return recordPreToolUse(callKey, preToolUseTimeoutDecision(toolPolicy.timeoutAction));
273
342
  }
274
343
  if (toolPolicy.mode === "notify_only") {
275
- if (claimNotify(input.tool_use_id)) await notifyApprovalNeeded(apiKey, input, lookup);
344
+ await notifyApprovalNeeded(apiKey, input, lookup);
345
+ recordCodexCallDecision(callKey, { kind: "handled" });
276
346
  return codexPass();
277
347
  }
278
348
  return codexPass();
@@ -336,6 +406,13 @@ var main = async () => {
336
406
  case "SessionStart":
337
407
  await reportSessionStart(input);
338
408
  break;
409
+ case "SessionEnd":
410
+ await handleSessionEnd({
411
+ cwd: input.cwd,
412
+ session_id: input.session_id,
413
+ reason: input.reason
414
+ }, CODEX_AGENT, { reconcile: false, reportAttempts: 1, reportTimeoutMs: 1200 });
415
+ break;
339
416
  default:
340
417
  break;
341
418
  }
@@ -1,23 +1,24 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  reportEvent
4
- } from "../chunk-V2XPF77V.js";
4
+ } from "../chunk-VOE4SYPR.js";
5
5
  import {
6
6
  askUser,
7
7
  waitForAnswer
8
8
  } from "../chunk-J3YDT4HM.js";
9
+ import "../chunk-7J67NYK4.js";
9
10
  import {
10
11
  getMachineId
11
12
  } from "../chunk-RN3NOEJF.js";
12
13
  import "../chunk-BSZYIAZL.js";
13
14
  import "../chunk-SAF6HGAA.js";
14
- import "../chunk-Y3UTTFTZ.js";
15
+ import "../chunk-KBS53WOE.js";
15
16
  import "../chunk-7QLSKOSU.js";
16
17
  import "../chunk-KZERVKTD.js";
17
18
  import {
18
19
  getApiKey
19
20
  } from "../chunk-2UMNXADU.js";
20
- import "../chunk-7YJS2VTG.js";
21
+ import "../chunk-DMW54VWK.js";
21
22
 
22
23
  // bin/pushary-codex.ts
23
24
  import { basename } from "path";
@@ -1,22 +1,22 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  printKeyCheck
4
- } from "../chunk-JAICP7BA.js";
4
+ } from "../chunk-TD3M232T.js";
5
5
  import {
6
6
  confirmAppConnection,
7
7
  connectDevice,
8
8
  printConnectInstructions
9
- } from "../chunk-A3PFIWWN.js";
9
+ } from "../chunk-EJL3PJU7.js";
10
10
  import "../chunk-3EGEA4KH.js";
11
11
  import {
12
12
  checkApiKey
13
- } from "../chunk-A4I5JGCW.js";
13
+ } from "../chunk-KFAH6KRE.js";
14
14
  import {
15
15
  createIo
16
16
  } from "../chunk-5RUIFDTP.js";
17
17
  import {
18
18
  readKeySource
19
- } from "../chunk-OT4YD54Q.js";
19
+ } from "../chunk-YX6ZKCC5.js";
20
20
  import {
21
21
  parseFlags
22
22
  } from "../chunk-GMXKITVA.js";
@@ -29,7 +29,7 @@ import "../chunk-DINDL2CF.js";
29
29
  import "../chunk-6MTNS63X.js";
30
30
  import "../chunk-BSZYIAZL.js";
31
31
  import "../chunk-SAF6HGAA.js";
32
- import "../chunk-Y3UTTFTZ.js";
32
+ import "../chunk-KBS53WOE.js";
33
33
  import {
34
34
  UsageError
35
35
  } from "../chunk-7QLSKOSU.js";
@@ -37,7 +37,7 @@ import {
37
37
  EXIT
38
38
  } from "../chunk-KZERVKTD.js";
39
39
  import "../chunk-2UMNXADU.js";
40
- import "../chunk-7YJS2VTG.js";
40
+ import "../chunk-DMW54VWK.js";
41
41
 
42
42
  // bin/pushary-connect.ts
43
43
  exitOnHelpFlag("connect");
@@ -8,7 +8,7 @@ import {
8
8
  acquireDaemonLease,
9
9
  readDaemonStatus,
10
10
  requestDaemonHandoff
11
- } from "../chunk-SJVLFIYP.js";
11
+ } from "../chunk-KCAFCGGY.js";
12
12
  import {
13
13
  CODEX_BRIDGE_CAPABILITY,
14
14
  CODEX_RESUME_CAPABILITY,
@@ -17,7 +17,7 @@ import {
17
17
  import {
18
18
  findLiveProcessBySpawnId,
19
19
  ownsLiveProcess
20
- } from "../chunk-QBWYNVKY.js";
20
+ } from "../chunk-LGVHYFIB.js";
21
21
  import {
22
22
  spawnClaude
23
23
  } from "../chunk-XHKBHWLX.js";
@@ -26,7 +26,8 @@ import {
26
26
  resolveGlobalPackageVersion,
27
27
  resolvePackageVersionFromBinary
28
28
  } from "../chunk-VT4IVERX.js";
29
- import "../chunk-B3V2H6SA.js";
29
+ import "../chunk-VL5ZNK6V.js";
30
+ import "../chunk-GIRT7677.js";
30
31
  import {
31
32
  getMachineId
32
33
  } from "../chunk-RN3NOEJF.js";
@@ -43,7 +44,7 @@ import {
43
44
  getApiKey,
44
45
  getBaseUrl
45
46
  } from "../chunk-2UMNXADU.js";
46
- import "../chunk-7YJS2VTG.js";
47
+ import "../chunk-DMW54VWK.js";
47
48
 
48
49
  // src/spawn-daemon.ts
49
50
  import { execFileSync, spawn } from "child_process";
@@ -2,12 +2,22 @@
2
2
  import {
3
3
  removeClaudeMcpServers,
4
4
  removePusharySettings
5
- } from "../chunk-5646VKR4.js";
5
+ } from "../chunk-2JYQAW4B.js";
6
+ import {
7
+ OPENCODE_PLUGIN_SIBLING,
8
+ OPENCODE_PLUGIN_SIBLING_CONTENTS,
9
+ hasForeignPlugins,
10
+ isOpenCodePluginOurs,
11
+ openCodeConfigFile,
12
+ openCodePluginDir,
13
+ openCodePluginFile,
14
+ removeOpenCodeMcpServer
15
+ } from "../chunk-ZGC3W23N.js";
6
16
  import {
7
17
  removeGeminiSettings
8
- } from "../chunk-ZZE6FN7T.js";
18
+ } from "../chunk-K42RXJPG.js";
9
19
  import "../chunk-ATBKJNWS.js";
10
- import "../chunk-M7RYIZX6.js";
20
+ import "../chunk-B7ZBHWOW.js";
11
21
  import "../chunk-VT4IVERX.js";
12
22
  import {
13
23
  claudeJson,
@@ -21,21 +31,23 @@ import {
21
31
  geminiSettings,
22
32
  removeCodexHooks,
23
33
  removeCodexSettings
24
- } from "../chunk-B3V2H6SA.js";
34
+ } from "../chunk-VL5ZNK6V.js";
35
+ import "../chunk-GIRT7677.js";
25
36
  import {
26
37
  exitOnHelpFlag
27
38
  } from "../chunk-SQOFGPNX.js";
28
39
  import "../chunk-GOEXZ5QJ.js";
29
40
  import "../chunk-DINDL2CF.js";
30
41
  import "../chunk-KZERVKTD.js";
31
- import "../chunk-7YJS2VTG.js";
42
+ import "../chunk-DMW54VWK.js";
32
43
 
33
44
  // bin/pushary-disconnect.ts
34
- import { readFileSync, writeFileSync } from "fs";
45
+ import { readdirSync, readFileSync, rmSync, writeFileSync } from "fs";
35
46
  import { parse as parseTOML, stringify as stringifyTOML } from "smol-toml";
36
47
 
37
48
  // src/disconnect.ts
38
- var DISCONNECTABLE_AGENTS = ["claude", "codex", "gemini", "cursor"];
49
+ import { dirname } from "path";
50
+ var DISCONNECTABLE_AGENTS = ["claude", "codex", "gemini", "cursor", "opencode"];
39
51
  var isDisconnectableAgent = (value) => DISCONNECTABLE_AGENTS.includes(value);
40
52
  var removeCursorHooks = (config) => {
41
53
  const hooks = config.hooks;
@@ -85,8 +97,24 @@ var disconnectPlan = (agent) => {
85
97
  { path: cursorUserHooks(), format: "json", remove: removeCursorHooks },
86
98
  { path: cursorUserMcp(), format: "json", remove: removeClaudeMcpServers }
87
99
  ];
100
+ // The plugin itself is a whole file of ours and comes out in
101
+ // `disconnectRemovals` below; the MCP entry is an ordinary edit to a config
102
+ // that is otherwise the person's own.
103
+ case "opencode":
104
+ return [{ path: openCodeConfigFile(), format: "json", remove: removeOpenCodeMcpServer }];
88
105
  }
89
106
  };
107
+ var disconnectRemovals = (agent) => {
108
+ if (agent !== "opencode") return [];
109
+ return [
110
+ { path: openCodePluginFile(), ownedBy: isOpenCodePluginOurs },
111
+ {
112
+ path: `${openCodePluginDir()}/${OPENCODE_PLUGIN_SIBLING}`,
113
+ ownedBy: (contents) => contents === OPENCODE_PLUGIN_SIBLING_CONTENTS,
114
+ onlyIfLast: true
115
+ }
116
+ ];
117
+ };
90
118
  var disconnectAgent = (agent, io) => {
91
119
  const changed = [];
92
120
  const untouched = [];
@@ -109,6 +137,27 @@ var disconnectAgent = (agent, io) => {
109
137
  untouched.push(edit.path);
110
138
  }
111
139
  }
140
+ for (const removal of disconnectRemovals(agent)) {
141
+ const raw = io.read(removal.path);
142
+ if (raw === null) continue;
143
+ if (!removal.ownedBy(raw)) {
144
+ untouched.push(removal.path);
145
+ continue;
146
+ }
147
+ if (removal.onlyIfLast) {
148
+ const entries = io.list?.(dirname(removal.path));
149
+ if (entries === void 0 || hasForeignPlugins(entries)) {
150
+ untouched.push(removal.path);
151
+ continue;
152
+ }
153
+ }
154
+ try {
155
+ io.remove?.(removal.path);
156
+ changed.push(removal.path);
157
+ } catch {
158
+ untouched.push(removal.path);
159
+ }
160
+ }
112
161
  return { agent, changed, untouched };
113
162
  };
114
163
 
@@ -130,7 +179,15 @@ var result = disconnectAgent(requested, {
130
179
  },
131
180
  write: (path, contents) => writeFileSync(path, contents, "utf-8"),
132
181
  parseToml: (raw) => parseTOML(raw),
133
- stringifyToml: (config) => stringifyTOML(config)
182
+ stringifyToml: (config) => stringifyTOML(config),
183
+ remove: (path) => rmSync(path, { force: true }),
184
+ list: (dir) => {
185
+ try {
186
+ return readdirSync(dir);
187
+ } catch {
188
+ return [];
189
+ }
190
+ }
134
191
  });
135
192
  if (result.changed.length === 0) {
136
193
  console.log(`${requested} was not connected.`);