@pushary/agent-hooks 0.89.2 → 0.89.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/CHANGELOG.md CHANGED
@@ -1,5 +1,14 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.89.3
4
+
5
+ ### Doctor recognizes hooks installed by the macOS app
6
+
7
+ Claude Code, Codex and Gemini hooks that point at the shared native
8
+ `pushary-bridge` now pass the same presence, executable and timeout checks as
9
+ their npm hook binaries. Doctor no longer tells a Mac-managed install to repair
10
+ hooks that are already installed and running.
11
+
3
12
  ## 0.89.2
4
13
 
5
14
  ### Multi-question prompts keep their real answer shape
@@ -29,12 +29,17 @@ import {
29
29
  getPolicy,
30
30
  repoKeyFor,
31
31
  reportEvent
32
- } from "../chunk-6XSC6Z2K.js";
32
+ } from "../chunk-3UBYTSCY.js";
33
33
  import {
34
+ DEFAULT_SESSION,
34
35
  askUser,
35
36
  cancelQuestion,
37
+ markPendingCommandDelivered,
38
+ readPendingCommand,
39
+ removePendingCommand,
40
+ savePendingCommand,
36
41
  waitForAnswer
37
- } from "../chunk-J3YDT4HM.js";
42
+ } from "../chunk-HUJAAWNL.js";
38
43
  import {
39
44
  deriveToolTarget,
40
45
  describeToolCall,
@@ -644,20 +649,58 @@ var startCommandPoller = (opts) => {
644
649
 
645
650
  // src/wrapper/pollClient.ts
646
651
  var APPROVAL_MODES = /* @__PURE__ */ new Set(["push_only", "terminal_only", "push_first", "notify_only"]);
647
- var pollAgent = async (apiKey, sessionId, wantCommand, signal, baseUrl = getBaseUrl()) => {
652
+ var pollAgent = async (apiKey, sessionId, wantCommand, signal, baseUrl = getBaseUrl(), machineId) => {
648
653
  try {
654
+ const sessionKey = sessionId || DEFAULT_SESSION;
655
+ const receiptNamespace = `${apiKey}:${machineId}:claude_code`;
656
+ const storedCommand = wantCommand && machineId ? readPendingCommand(sessionKey, receiptNamespace) : null;
649
657
  const response = await fetch(`${baseUrl}/api/agent/poll`, {
650
658
  method: "POST",
651
659
  headers: { "Content-Type": "application/json", Authorization: `Bearer ${apiKey}` },
652
660
  body: JSON.stringify({
653
661
  ...sessionId ? { sessionId } : {},
654
- ...wantCommand ? {} : { drain: false }
662
+ ...machineId ? { machineId } : {},
663
+ ...wantCommand && !storedCommand ? {} : { drain: false },
664
+ ...wantCommand && !storedCommand && machineId ? { ackMode: true } : {}
655
665
  }),
656
666
  signal
657
667
  });
658
668
  if (!response.ok) return { command: null, mode: null };
659
669
  const data = await response.json();
660
- const command = typeof data.command === "string" && data.command.length > 0 ? data.command : null;
670
+ const acknowledge = async (id) => {
671
+ try {
672
+ const acknowledged = await fetch(`${baseUrl}/api/agent/command/ack`, {
673
+ method: "POST",
674
+ headers: { "Content-Type": "application/json", Authorization: `Bearer ${apiKey}` },
675
+ body: JSON.stringify({ commandId: id, sessionId: sessionId ?? null, machineId, state: "delivered" }),
676
+ signal
677
+ });
678
+ return acknowledged.ok;
679
+ } catch {
680
+ return false;
681
+ }
682
+ };
683
+ let command = typeof data.command === "string" && data.command.length > 0 ? data.command : null;
684
+ if (storedCommand) {
685
+ const acknowledged = await acknowledge(storedCommand.id);
686
+ if (acknowledged && storedCommand.delivered) removePendingCommand(sessionKey, storedCommand.id, receiptNamespace);
687
+ if (acknowledged && !storedCommand.delivered && markPendingCommandDelivered(sessionKey, storedCommand.id, receiptNamespace)) {
688
+ command = storedCommand.text;
689
+ } else {
690
+ command = null;
691
+ }
692
+ } else if (data.command && typeof data.command === "object") {
693
+ const claimed = data.command;
694
+ if (typeof claimed.id === "string" && typeof claimed.text === "string") {
695
+ const saved = savePendingCommand(sessionKey, {
696
+ id: claimed.id,
697
+ text: claimed.text,
698
+ expiresAtMs: typeof claimed.expiresAtMs === "number" ? claimed.expiresAtMs : Date.now() + 36e5,
699
+ delivered: false
700
+ }, receiptNamespace);
701
+ command = saved && await acknowledge(claimed.id) && markPendingCommandDelivered(sessionKey, claimed.id, receiptNamespace) ? claimed.text : null;
702
+ }
703
+ }
661
704
  const m = data.override?.mode;
662
705
  const mode = {
663
706
  mode: typeof m === "string" && APPROVAL_MODES.has(m) ? m : null,
@@ -799,12 +842,15 @@ var createRelayClient = (options) => {
799
842
  log("[pushary] relay connected");
800
843
  break;
801
844
  case "command":
802
- sendFrame({ v: PROTOCOL_VERSION, t: "ack", id: frame.id });
803
- if (rememberCommand(frame.id)) {
804
- try {
805
- options.onCommand(frame.text);
806
- } catch {
807
- }
845
+ if (seenSet.has(frame.id)) {
846
+ sendFrame({ v: PROTOCOL_VERSION, t: "ack", id: frame.id });
847
+ break;
848
+ }
849
+ try {
850
+ options.onCommand(frame.text);
851
+ rememberCommand(frame.id);
852
+ sendFrame({ v: PROTOCOL_VERSION, t: "ack", id: frame.id });
853
+ } catch {
808
854
  }
809
855
  break;
810
856
  case "ping":
@@ -1378,7 +1424,7 @@ var runDualMode = async (binary, args2, startingMode) => {
1378
1424
  shouldDrainCommands: () => !(relay?.connected() ?? false),
1379
1425
  // Unified poll: one request per tick for both command + mode/kill (half the
1380
1426
  // requests and the per-poll DB write of the old drain + mode pair).
1381
- poll: pollAgent,
1427
+ poll: (key, id, wantCommand, signal) => pollAgent(key, id, wantCommand, signal, void 0, machineId),
1382
1428
  fetchModeState,
1383
1429
  // Relay-primary: idle the safety-net poll at 60s (see RELAY_FALLBACK_POLL_MS).
1384
1430
  // No relay advertised -> the poller IS the command path, so keep the default.
@@ -276,7 +276,11 @@ var runCodexBridge = async (options) => {
276
276
  await liveSessionSleep(options.pollIntervalMs ?? 2e3, options.signal);
277
277
  continue;
278
278
  }
279
- const response = await postLiveSession(options, "/api/agent/command/drain", { sessionId: threadId, ackMode: true });
279
+ const response = await postLiveSession(options, "/api/agent/command/drain", {
280
+ sessionId: threadId,
281
+ machineId: options.machineId,
282
+ ackMode: true
283
+ });
280
284
  if (response.ok) {
281
285
  const command = object((await response.json()).command);
282
286
  const commandId = string(command?.id);
@@ -284,10 +288,17 @@ var runCodexBridge = async (options) => {
284
288
  if (commandId && text) {
285
289
  try {
286
290
  await startTurn(text, commandId);
287
- pendingAck = { commandId, state: "delivered" };
291
+ pendingAck = {
292
+ commandId,
293
+ sessionId: threadId,
294
+ machineId: options.machineId,
295
+ state: "delivered"
296
+ };
288
297
  } catch {
289
298
  pendingAck = {
290
299
  commandId,
300
+ sessionId: threadId,
301
+ machineId: options.machineId,
291
302
  state: "failed",
292
303
  failureCode: "codex_rejected"
293
304
  };
@@ -34,7 +34,7 @@ import {
34
34
  toCodexWire,
35
35
  toPolicyLookup,
36
36
  toPolicyLookups
37
- } from "../chunk-6XSC6Z2K.js";
37
+ } from "../chunk-3UBYTSCY.js";
38
38
  import {
39
39
  DEFAULT_SESSION,
40
40
  askUser,
@@ -42,7 +42,7 @@ import {
42
42
  pollForAnswer,
43
43
  savePendingQuestion,
44
44
  sendNotification
45
- } from "../chunk-J3YDT4HM.js";
45
+ } from "../chunk-HUJAAWNL.js";
46
46
  import {
47
47
  deriveActionBody,
48
48
  deriveBlocker,
@@ -1,11 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  reportEvent
4
- } from "../chunk-6XSC6Z2K.js";
4
+ } from "../chunk-3UBYTSCY.js";
5
5
  import {
6
6
  askUser,
7
7
  waitForAnswer
8
- } from "../chunk-J3YDT4HM.js";
8
+ } from "../chunk-HUJAAWNL.js";
9
9
  import "../chunk-LGXTBUT4.js";
10
10
  import {
11
11
  getMachineId
@@ -5,8 +5,9 @@ import {
5
5
  import {
6
6
  claudeWired,
7
7
  codexWired,
8
- geminiWired
9
- } from "../chunk-4VYUMYWD.js";
8
+ geminiWired,
9
+ pusharyHookExecutable
10
+ } from "../chunk-3O2XPYLF.js";
10
11
  import {
11
12
  readDaemonStatus
12
13
  } from "../chunk-K42SZKYF.js";
@@ -111,14 +112,15 @@ import {
111
112
  getBaseUrl
112
113
  } from "../chunk-2UMNXADU.js";
113
114
  import {
114
- CURSOR_GATE_EVENTS
115
+ CURSOR_GATE_EVENTS,
116
+ HOOK_LOCATOR_BINARY
115
117
  } from "../chunk-L4C2JXJS.js";
116
118
 
117
119
  // bin/pushary-doctor.ts
118
120
  import { existsSync, readFileSync, writeFileSync } from "fs";
119
121
  import { join } from "path";
120
122
  import { homedir as homedir2 } from "os";
121
- import { execSync } from "child_process";
123
+ import { execFileSync } from "child_process";
122
124
  import { confirm } from "@inquirer/prompts";
123
125
  import { parse as parseTOML } from "smol-toml";
124
126
 
@@ -219,10 +221,11 @@ var readJson = (path) => {
219
221
  }
220
222
  };
221
223
  var commandResolves = (command) => {
222
- if (command.includes("/") || command.includes("\\")) return existsSync(command);
224
+ const target = pusharyHookExecutable(command) ?? command;
225
+ if (target.includes("/") || target.includes("\\")) return existsSync(target);
223
226
  try {
224
227
  const whichCmd = process.platform === "win32" ? "where" : "which";
225
- execSync(`${whichCmd} ${command}`, { stdio: "ignore", timeout: 5e3 });
228
+ execFileSync(whichCmd, [target], { stdio: "ignore", timeout: 5e3 });
226
229
  return true;
227
230
  } catch {
228
231
  return false;
@@ -240,6 +243,7 @@ var extractHookCommand = (entries, needle) => {
240
243
  }
241
244
  return null;
242
245
  };
246
+ var extractAgentHookCommand = (entries, needle) => extractHookCommand(entries, needle) ?? extractHookCommand(entries, HOOK_LOCATOR_BINARY);
243
247
  var extractHookTimeout = (entries, needle) => {
244
248
  if (!Array.isArray(entries)) return null;
245
249
  for (const entry of entries) {
@@ -395,10 +399,11 @@ var main = async () => {
395
399
  { event: "PermissionDenied", needle: "pushary-permission-denied-hook", label: "PermissionDenied hook" }
396
400
  ];
397
401
  for (const { event, needle, label } of CLAUDE_HOOK_CHECKS) {
398
- const present = JSON.stringify(hooks?.[event] ?? []).includes(needle);
402
+ const configured = JSON.stringify(hooks?.[event] ?? []);
403
+ const present = configured.includes(needle) || configured.includes(HOOK_LOCATOR_BINARY);
399
404
  check(present, `Claude Code: ${label}`, present ? void 0 : "missing \u2014 run npx @pushary/agent-hooks@latest upgrade");
400
405
  }
401
- const preHookCommand = extractHookCommand(hooks?.PreToolUse, "pushary-hook");
406
+ const preHookCommand = extractAgentHookCommand(hooks?.PreToolUse, "pushary-hook");
402
407
  if (preHookCommand) {
403
408
  const resolves = commandResolves(preHookCommand);
404
409
  check(resolves, "Claude Code: hook command resolves", resolves ? preHookCommand : `not on PATH \u2014 ${preHookCommand}`);
@@ -464,7 +469,7 @@ var main = async () => {
464
469
  if (hooksInstalled) {
465
470
  const missingEvents = missingCodexHookEvents(codexHooksConfig);
466
471
  check(missingEvents.length === 0, "Codex: native hooks installed", missingEvents.length === 0 ? "all 6 events" : `missing ${missingEvents.join(", ")}, re-run setup`);
467
- const hookCommand = extractHookCommand(codexHooksConfig.hooks?.PreToolUse, "pushary-codex-hook") ?? extractHookCommand(codexHooksConfig.hooks?.PermissionRequest, "pushary-codex-hook");
472
+ const hookCommand = extractAgentHookCommand(codexHooksConfig.hooks?.PreToolUse, "pushary-codex-hook") ?? extractAgentHookCommand(codexHooksConfig.hooks?.PermissionRequest, "pushary-codex-hook");
468
473
  if (hookCommand) {
469
474
  const resolves = commandResolves(hookCommand);
470
475
  check(resolves, "Codex: hook command resolves", resolves ? hookCommand : `not on PATH: ${hookCommand}`);
@@ -525,12 +530,12 @@ var main = async () => {
525
530
  const missingEvents = missingGeminiHookEvents(geminiSettings2);
526
531
  check(missingEvents.length === 0, "Gemini CLI: native hooks installed", missingEvents.length === 0 ? "all 5 events" : `missing ${missingEvents.join(", ")}, re-run setup`);
527
532
  const geminiHooks = geminiSettings2.hooks;
528
- const hookCommand = extractHookCommand(geminiHooks?.BeforeTool, GEMINI_HOOK_BINARY) ?? extractHookCommand(geminiHooks?.AfterTool, GEMINI_HOOK_BINARY) ?? extractHookCommand(geminiHooks?.SessionStart, GEMINI_HOOK_BINARY);
533
+ const hookCommand = extractAgentHookCommand(geminiHooks?.BeforeTool, GEMINI_HOOK_BINARY) ?? extractAgentHookCommand(geminiHooks?.AfterTool, GEMINI_HOOK_BINARY) ?? extractAgentHookCommand(geminiHooks?.SessionStart, GEMINI_HOOK_BINARY);
529
534
  if (hookCommand) {
530
535
  const resolves = commandResolves(hookCommand);
531
536
  check(resolves, "Gemini CLI: hook command resolves", resolves ? hookCommand : `not on PATH: ${hookCommand}`);
532
537
  }
533
- const geminiTimeout = extractHookTimeout(geminiHooks?.BeforeTool, GEMINI_HOOK_BINARY);
538
+ const geminiTimeout = extractHookTimeout(geminiHooks?.BeforeTool, GEMINI_HOOK_BINARY) ?? extractHookTimeout(geminiHooks?.BeforeTool, HOOK_LOCATOR_BINARY);
534
539
  if (typeof geminiTimeout === "number") {
535
540
  const ok = geminiTimeout >= 3e4;
536
541
  check(ok, "Gemini CLI: hook timeout", ok ? `${geminiTimeout}ms` : `${geminiTimeout}ms too short (Gemini timeouts are milliseconds) \u2014 re-run setup`);
@@ -11,7 +11,7 @@ import {
11
11
  cancelQuestion,
12
12
  pollForAnswer,
13
13
  savePendingQuestion
14
- } from "../chunk-J3YDT4HM.js";
14
+ } from "../chunk-HUJAAWNL.js";
15
15
  import {
16
16
  getMachineId
17
17
  } from "../chunk-RN3NOEJF.js";
@@ -216,7 +216,11 @@ var runGeminiBridge = async (options) => {
216
216
  await liveSessionSleep(options.pollIntervalMs ?? 2e3, options.signal);
217
217
  continue;
218
218
  }
219
- const response = await postLiveSession(options, "/api/agent/command/drain", { sessionId, ackMode: true });
219
+ const response = await postLiveSession(options, "/api/agent/command/drain", {
220
+ sessionId,
221
+ machineId: options.machineId,
222
+ ackMode: true
223
+ });
220
224
  if (response.ok) {
221
225
  const command = object((await response.json()).command);
222
226
  const commandId = string(command?.id);
@@ -225,10 +229,17 @@ var runGeminiBridge = async (options) => {
225
229
  try {
226
230
  const outcome2 = await turn(text, commandId);
227
231
  if (outcome2 === "aborted") break;
228
- pendingAck = { commandId, state: "delivered" };
232
+ pendingAck = {
233
+ commandId,
234
+ sessionId,
235
+ machineId: options.machineId,
236
+ state: "delivered"
237
+ };
229
238
  } catch (error) {
230
239
  pendingAck = {
231
240
  commandId,
241
+ sessionId,
242
+ machineId: options.machineId,
232
243
  state: "failed",
233
244
  failureCode: "gemini_rejected",
234
245
  failureMessage: (error instanceof Error ? error.message : String(error)).slice(0, 240)
@@ -21,7 +21,7 @@ import {
21
21
  readLastUserPrompt,
22
22
  repoKeyFor,
23
23
  reportEvent
24
- } from "../chunk-6XSC6Z2K.js";
24
+ } from "../chunk-3UBYTSCY.js";
25
25
  import {
26
26
  DEFAULT_SESSION,
27
27
  askUser,
@@ -29,7 +29,7 @@ import {
29
29
  savePendingQuestion,
30
30
  sendNotification,
31
31
  waitForAnswer
32
- } from "../chunk-J3YDT4HM.js";
32
+ } from "../chunk-HUJAAWNL.js";
33
33
  import {
34
34
  deriveActionBody,
35
35
  deriveBlocker,
@@ -1,15 +1,15 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  handlePreToolUse
4
- } from "../chunk-HEQVIIBN.js";
4
+ } from "../chunk-JT6TIBYH.js";
5
5
  import "../chunk-ATBKJNWS.js";
6
6
  import "../chunk-VZKXBFUA.js";
7
7
  import {
8
8
  readHookInput
9
9
  } from "../chunk-6CUUA7HO.js";
10
10
  import "../chunk-YHG74UFF.js";
11
- import "../chunk-6XSC6Z2K.js";
12
- import "../chunk-J3YDT4HM.js";
11
+ import "../chunk-3UBYTSCY.js";
12
+ import "../chunk-HUJAAWNL.js";
13
13
  import "../chunk-LGXTBUT4.js";
14
14
  import "../chunk-RN3NOEJF.js";
15
15
  import {
@@ -4,8 +4,8 @@ import {
4
4
  } from "../chunk-6CUUA7HO.js";
5
5
  import {
6
6
  handleNotification
7
- } from "../chunk-6XSC6Z2K.js";
8
- import "../chunk-J3YDT4HM.js";
7
+ } from "../chunk-3UBYTSCY.js";
8
+ import "../chunk-HUJAAWNL.js";
9
9
  import "../chunk-LGXTBUT4.js";
10
10
  import "../chunk-RN3NOEJF.js";
11
11
  import "../chunk-BSZYIAZL.js";
@@ -19,7 +19,7 @@ import {
19
19
  policyRequestFor,
20
20
  readLastPrompt,
21
21
  repoKeyFor
22
- } from "../chunk-6XSC6Z2K.js";
22
+ } from "../chunk-3UBYTSCY.js";
23
23
  import {
24
24
  DEFAULT_SESSION,
25
25
  askUser,
@@ -27,7 +27,7 @@ import {
27
27
  savePendingQuestion,
28
28
  sendNotification,
29
29
  waitForAnswer
30
- } from "../chunk-J3YDT4HM.js";
30
+ } from "../chunk-HUJAAWNL.js";
31
31
  import {
32
32
  OPENCODE_TOOL_NAMES,
33
33
  deriveActionBody,
@@ -1,15 +1,15 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  handlePermissionDenied
4
- } from "../chunk-HEQVIIBN.js";
4
+ } from "../chunk-JT6TIBYH.js";
5
5
  import "../chunk-ATBKJNWS.js";
6
6
  import "../chunk-VZKXBFUA.js";
7
7
  import {
8
8
  readHookInput
9
9
  } from "../chunk-6CUUA7HO.js";
10
10
  import "../chunk-YHG74UFF.js";
11
- import "../chunk-6XSC6Z2K.js";
12
- import "../chunk-J3YDT4HM.js";
11
+ import "../chunk-3UBYTSCY.js";
12
+ import "../chunk-HUJAAWNL.js";
13
13
  import "../chunk-LGXTBUT4.js";
14
14
  import "../chunk-RN3NOEJF.js";
15
15
  import "../chunk-BSZYIAZL.js";
@@ -1,15 +1,15 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  handlePermissionRequest
4
- } from "../chunk-HEQVIIBN.js";
4
+ } from "../chunk-JT6TIBYH.js";
5
5
  import "../chunk-ATBKJNWS.js";
6
6
  import "../chunk-VZKXBFUA.js";
7
7
  import {
8
8
  readHookInput
9
9
  } from "../chunk-6CUUA7HO.js";
10
10
  import "../chunk-YHG74UFF.js";
11
- import "../chunk-6XSC6Z2K.js";
12
- import "../chunk-J3YDT4HM.js";
11
+ import "../chunk-3UBYTSCY.js";
12
+ import "../chunk-HUJAAWNL.js";
13
13
  import "../chunk-LGXTBUT4.js";
14
14
  import "../chunk-RN3NOEJF.js";
15
15
  import "../chunk-BSZYIAZL.js";
@@ -4,8 +4,8 @@ import {
4
4
  } from "../chunk-6CUUA7HO.js";
5
5
  import {
6
6
  handlePostToolUse
7
- } from "../chunk-6XSC6Z2K.js";
8
- import "../chunk-J3YDT4HM.js";
7
+ } from "../chunk-3UBYTSCY.js";
8
+ import "../chunk-HUJAAWNL.js";
9
9
  import "../chunk-LGXTBUT4.js";
10
10
  import "../chunk-RN3NOEJF.js";
11
11
  import "../chunk-BSZYIAZL.js";
@@ -4,8 +4,8 @@ import {
4
4
  } from "../chunk-6CUUA7HO.js";
5
5
  import {
6
6
  handleUserPrompt
7
- } from "../chunk-6XSC6Z2K.js";
8
- import "../chunk-J3YDT4HM.js";
7
+ } from "../chunk-3UBYTSCY.js";
8
+ import "../chunk-HUJAAWNL.js";
9
9
  import "../chunk-LGXTBUT4.js";
10
10
  import "../chunk-RN3NOEJF.js";
11
11
  import "../chunk-BSZYIAZL.js";
@@ -4,8 +4,8 @@ import {
4
4
  } from "../chunk-6CUUA7HO.js";
5
5
  import {
6
6
  handleSessionEnd
7
- } from "../chunk-6XSC6Z2K.js";
8
- import "../chunk-J3YDT4HM.js";
7
+ } from "../chunk-3UBYTSCY.js";
8
+ import "../chunk-HUJAAWNL.js";
9
9
  import "../chunk-LGXTBUT4.js";
10
10
  import "../chunk-RN3NOEJF.js";
11
11
  import "../chunk-BSZYIAZL.js";
@@ -4,8 +4,8 @@ import {
4
4
  } from "../chunk-6CUUA7HO.js";
5
5
  import {
6
6
  handleSessionStart
7
- } from "../chunk-6XSC6Z2K.js";
8
- import "../chunk-J3YDT4HM.js";
7
+ } from "../chunk-3UBYTSCY.js";
8
+ import "../chunk-HUJAAWNL.js";
9
9
  import "../chunk-LGXTBUT4.js";
10
10
  import "../chunk-RN3NOEJF.js";
11
11
  import "../chunk-BSZYIAZL.js";
@@ -6,7 +6,7 @@ import {
6
6
  claudeWired,
7
7
  codexWired,
8
8
  geminiWired
9
- } from "../chunk-4VYUMYWD.js";
9
+ } from "../chunk-3O2XPYLF.js";
10
10
  import {
11
11
  runBrowserLogin
12
12
  } from "../chunk-Z3Z4VK5F.js";
@@ -82,8 +82,8 @@ import {
82
82
  import "../chunk-GIRT7677.js";
83
83
  import {
84
84
  reportEvent
85
- } from "../chunk-6XSC6Z2K.js";
86
- import "../chunk-J3YDT4HM.js";
85
+ } from "../chunk-3UBYTSCY.js";
86
+ import "../chunk-HUJAAWNL.js";
87
87
  import "../chunk-LGXTBUT4.js";
88
88
  import "../chunk-RN3NOEJF.js";
89
89
  import {
@@ -4,8 +4,8 @@ import {
4
4
  } from "../chunk-6CUUA7HO.js";
5
5
  import {
6
6
  handleStop
7
- } from "../chunk-6XSC6Z2K.js";
8
- import "../chunk-J3YDT4HM.js";
7
+ } from "../chunk-3UBYTSCY.js";
8
+ import "../chunk-HUJAAWNL.js";
9
9
  import "../chunk-LGXTBUT4.js";
10
10
  import "../chunk-RN3NOEJF.js";
11
11
  import "../chunk-BSZYIAZL.js";
@@ -4,8 +4,8 @@ import {
4
4
  } from "../chunk-6CUUA7HO.js";
5
5
  import {
6
6
  handleStopFailure
7
- } from "../chunk-6XSC6Z2K.js";
8
- import "../chunk-J3YDT4HM.js";
7
+ } from "../chunk-3UBYTSCY.js";
8
+ import "../chunk-HUJAAWNL.js";
9
9
  import "../chunk-LGXTBUT4.js";
10
10
  import "../chunk-RN3NOEJF.js";
11
11
  import "../chunk-BSZYIAZL.js";
@@ -4,15 +4,29 @@ import {
4
4
  import {
5
5
  hasCodexHooks
6
6
  } from "./chunk-VKVHY5HD.js";
7
+ import {
8
+ HOOK_LOCATOR_BINARY
9
+ } from "./chunk-L4C2JXJS.js";
7
10
 
8
11
  // src/diagnostics/wiring.ts
9
12
  var record = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : null;
10
13
  var CLAUDE_HOOK_BINARY = /pushary-[a-z-]*hook/;
14
+ var EXACT_HOOK_BINARY = /^pushary-[a-z-]*hook$/;
15
+ var pusharyHookExecutable = (command) => {
16
+ const tokens = command.match(/"[^"]*"|'[^']*'|\S+/g) ?? [];
17
+ for (const raw of tokens.reverse()) {
18
+ const token = raw[0] === '"' && raw.at(-1) === '"' || raw[0] === "'" && raw.at(-1) === "'" ? raw.slice(1, -1) : raw;
19
+ const name = token.split(/[/\\]/).pop()?.replace(/\.(cmd|exe)$/i, "") ?? "";
20
+ if (name === HOOK_LOCATOR_BINARY || EXACT_HOOK_BINARY.test(name)) return token;
21
+ }
22
+ return null;
23
+ };
11
24
  var claudeWired = ({ claudeJson, settings, skillExists }) => {
12
25
  if (record(record(claudeJson)?.mcpServers)?.pushary) return true;
13
26
  const s = record(settings);
14
27
  if (s) {
15
- if (CLAUDE_HOOK_BINARY.test(JSON.stringify(s.hooks ?? {}))) return true;
28
+ const hooks = JSON.stringify(s.hooks ?? {});
29
+ if (CLAUDE_HOOK_BINARY.test(hooks) || hooks.includes(HOOK_LOCATOR_BINARY)) return true;
16
30
  const allow = record(s.permissions)?.allow;
17
31
  if (Array.isArray(allow) && allow.some((rule) => String(rule).includes("mcp__pushary__"))) return true;
18
32
  }
@@ -32,6 +46,7 @@ var geminiWired = ({ settings }) => {
32
46
  };
33
47
 
34
48
  export {
49
+ pusharyHookExecutable,
35
50
  claudeWired,
36
51
  codexWired,
37
52
  geminiWired
@@ -4,11 +4,15 @@ import {
4
4
  isDefaultSession,
5
5
  isPolicyConfig,
6
6
  listPendingQuestions,
7
+ markPendingCommandDelivered,
8
+ readPendingCommand,
9
+ removePendingCommand,
7
10
  removePendingQuestion,
8
11
  removePendingSession,
12
+ savePendingCommand,
9
13
  sendNotification,
10
14
  waitForAnswer
11
- } from "./chunk-J3YDT4HM.js";
15
+ } from "./chunk-HUJAAWNL.js";
12
16
  import {
13
17
  AGENT_IDENTITIES,
14
18
  deriveReceiptMeta,
@@ -666,10 +670,31 @@ var consumesActivityContext = (agentType) => {
666
670
  return pendingCommandEvents(source).some((event) => event !== "Stop");
667
671
  };
668
672
  var detectInstallMode = () => (process.argv[1] ?? "").includes("_npx") ? "npx" : "cli";
673
+ var acknowledgePendingCommand = async (apiKey, baseUrl, commandId, sessionId, machineId, timeoutMs) => {
674
+ try {
675
+ const response = await fetch(`${baseUrl}/api/agent/command/ack`, {
676
+ method: "POST",
677
+ headers: {
678
+ "Content-Type": "application/json",
679
+ "Authorization": `Bearer ${apiKey}`
680
+ },
681
+ body: JSON.stringify({ commandId, sessionId: sessionId ?? null, machineId, state: "delivered" }),
682
+ signal: AbortSignal.timeout(timeoutMs)
683
+ });
684
+ return response.ok;
685
+ } catch {
686
+ return false;
687
+ }
688
+ };
669
689
  var reportEvent = async (event, options = {}) => {
670
690
  const apiKey = options.apiKey ?? getApiKey();
671
691
  const baseUrl = getBaseUrl();
692
+ const machineId = event.machineId ?? getMachineId();
693
+ const sessionKey = event.sessionId || DEFAULT_SESSION;
694
+ const receiptNamespace = `${apiKey}:${machineId}:${event.agentType}`;
695
+ const clientCanDrain = consumesActivityContext(event.agentType) && options.timeoutMs === void 0;
672
696
  return withRetry(async () => {
697
+ const storedCommand = clientCanDrain ? readPendingCommand(sessionKey, receiptNamespace) : null;
673
698
  const res = await fetch(`${baseUrl}/api/agent/event`, {
674
699
  method: "POST",
675
700
  headers: {
@@ -678,7 +703,7 @@ var reportEvent = async (event, options = {}) => {
678
703
  },
679
704
  body: JSON.stringify({
680
705
  ...event,
681
- machineId: event.machineId ?? getMachineId(),
706
+ machineId,
682
707
  // Lets the phone's Stop end this process instead of only denying its next
683
708
  // tool call. Ours alone: never another process, never a parent.
684
709
  pid: event.pid,
@@ -693,27 +718,54 @@ var reportEvent = async (event, options = {}) => {
693
718
  // Link-back for a phone-spawned session: forward the spawn id the daemon
694
719
  // set in the environment so the server can map spawnId -> sessionId.
695
720
  ...process.env.PUSHARY_SPAWN_ID ? { spawnId: process.env.PUSHARY_SPAWN_ID } : {},
696
- // Advertise that this client reads `pendingCommand` back off the
697
- // response, so the server may drain the phone-queued instruction on
698
- // activity events (tool_complete/tool_error/user_prompt), not only Stop.
699
- // Only Claude Code consumes additionalContext from these hooks; Codex and
700
- // Gemini keep their existing session_end-only drain, so they must NOT
701
- // advertise it or the server would pop-and-drop their queued command.
702
- //
703
- // And only when this caller kept the full budget. The pop is a destructive
704
- // LPOP that happens before the response is written, so a caller that
705
- // abandons the request destroys the message: `user_prompt` runs on a human
706
- // keystroke and gave it 800ms, which a cold function misses routinely.
707
- // Asking the server to consume something we may not wait to receive is the
708
- // one shape that loses it outright, and the instruction reaches the agent
709
- // on its next tool call instead.
710
- canDrainCommand: consumesActivityContext(event.agentType) && options.timeoutMs === void 0
721
+ canDrainCommand: clientCanDrain && !storedCommand,
722
+ ackMode: clientCanDrain && !storedCommand,
723
+ skipCommandDrain: storedCommand != null
711
724
  }),
712
725
  signal: AbortSignal.timeout(options.timeoutMs ?? 1e4)
713
726
  });
714
727
  if (options.requireOk && !res.ok) throw new Error(`/api/agent/event rejected (${res.status})`);
715
728
  try {
716
- return await res.json();
729
+ const result = await res.json();
730
+ const withoutPending = () => ({ ...result, pendingCommand: void 0, pendingCommandId: void 0 });
731
+ if (storedCommand) {
732
+ const acknowledged = await acknowledgePendingCommand(
733
+ apiKey,
734
+ baseUrl,
735
+ storedCommand.id,
736
+ event.sessionId,
737
+ machineId,
738
+ options.timeoutMs ?? 1e4
739
+ );
740
+ if (!acknowledged) return withoutPending();
741
+ if (storedCommand.delivered) {
742
+ removePendingCommand(sessionKey, storedCommand.id, receiptNamespace);
743
+ return withoutPending();
744
+ }
745
+ if (!markPendingCommandDelivered(sessionKey, storedCommand.id, receiptNamespace)) return withoutPending();
746
+ return { ...withoutPending(), pendingCommand: storedCommand.text, pendingCommandId: storedCommand.id };
747
+ }
748
+ if (typeof result.pendingCommandId === "string" && typeof result.pendingCommand === "string") {
749
+ const saved = savePendingCommand(sessionKey, {
750
+ id: result.pendingCommandId,
751
+ text: result.pendingCommand,
752
+ expiresAtMs: typeof result.pendingCommandExpiresAtMs === "number" ? result.pendingCommandExpiresAtMs : Date.now() + 36e5,
753
+ delivered: false
754
+ }, receiptNamespace);
755
+ if (!saved) return withoutPending();
756
+ const acknowledged = await acknowledgePendingCommand(
757
+ apiKey,
758
+ baseUrl,
759
+ result.pendingCommandId,
760
+ event.sessionId,
761
+ machineId,
762
+ options.timeoutMs ?? 1e4
763
+ );
764
+ if (!acknowledged || !markPendingCommandDelivered(sessionKey, result.pendingCommandId, receiptNamespace)) {
765
+ return withoutPending();
766
+ }
767
+ }
768
+ return result;
717
769
  } catch {
718
770
  return null;
719
771
  }
@@ -810,9 +862,6 @@ var handleStop = async (input, agent = CLAUDE_CODE_AGENT) => {
810
862
  const sessionKey = input.session_id || DEFAULT_SESSION;
811
863
  const late = await reconcilePendingQuestions(sessionKey);
812
864
  const tasks = [
813
- // maxAttempts:1 for the same reason as the activity-event drains: session_end
814
- // always pops the command server-side (single-use LPOP), so a retry after a
815
- // slow-but-successful response would pop a second command and drop the first.
816
865
  reportEvent({
817
866
  event: "session_end",
818
867
  agentType: agent.type,
@@ -68,13 +68,68 @@ var sendNotification = async (apiKey, params, timeoutMs = 15e3) => {
68
68
 
69
69
  // src/pending.ts
70
70
  import { join } from "path";
71
- import { tmpdir } from "os";
72
- import { existsSync, mkdirSync, writeFileSync, readdirSync, unlinkSync, rmSync, statSync } from "fs";
71
+ import { homedir, tmpdir } from "os";
72
+ import { createHash } from "crypto";
73
+ import { existsSync, mkdirSync, writeFileSync, readFileSync, readdirSync, renameSync, unlinkSync, rmSync, statSync } from "fs";
73
74
  var PENDING_DIR = join(tmpdir(), "pushary-pending");
75
+ var COMMAND_DIR = join(homedir(), ".pushary", "run", "commands");
74
76
  var DEFAULT_SESSION = "_no_session";
75
77
  var GRACE_MS = 10 * 60 * 1e3;
76
78
  var sanitize = (sessionId) => sessionId.replace(/[^A-Za-z0-9_-]/g, "_").slice(0, 128) || DEFAULT_SESSION;
77
79
  var dirFor = (sessionId) => join(PENDING_DIR, sanitize(sessionId));
80
+ var commandDirFor = (sessionId, namespace = "") => join(
81
+ COMMAND_DIR,
82
+ createHash("sha256").update(namespace).digest("hex").slice(0, 16),
83
+ sanitize(sessionId)
84
+ );
85
+ var commandFile = (sessionId, namespace = "") => join(commandDirFor(sessionId, namespace), "command.json");
86
+ var readPendingCommand = (sessionId, namespace = "") => {
87
+ try {
88
+ const value = JSON.parse(readFileSync(commandFile(sessionId, namespace), "utf-8"));
89
+ if (typeof value.id !== "string" || typeof value.text !== "string" || typeof value.expiresAtMs !== "number") return null;
90
+ if (value.expiresAtMs <= Date.now()) {
91
+ unlinkSync(commandFile(sessionId, namespace));
92
+ return null;
93
+ }
94
+ return { ...value, delivered: value.delivered === true };
95
+ } catch {
96
+ return null;
97
+ }
98
+ };
99
+ var savePendingCommand = (sessionId, command, namespace = "") => {
100
+ const dir = commandDirFor(sessionId, namespace);
101
+ try {
102
+ mkdirSync(dir, { recursive: true, mode: 448 });
103
+ writeFileSync(commandFile(sessionId, namespace), JSON.stringify(command), { encoding: "utf-8", mode: 384, flag: "wx" });
104
+ return true;
105
+ } catch {
106
+ return false;
107
+ }
108
+ };
109
+ var markPendingCommandDelivered = (sessionId, id, namespace = "") => {
110
+ const command = readPendingCommand(sessionId, namespace);
111
+ if (!command || command.id !== id) return false;
112
+ const file = commandFile(sessionId, namespace);
113
+ const temporary = `${file}.${process.pid}.tmp`;
114
+ try {
115
+ writeFileSync(temporary, JSON.stringify({ ...command, delivered: true }), { encoding: "utf-8", mode: 384 });
116
+ renameSync(temporary, file);
117
+ return true;
118
+ } catch {
119
+ try {
120
+ unlinkSync(temporary);
121
+ } catch {
122
+ }
123
+ return false;
124
+ }
125
+ };
126
+ var removePendingCommand = (sessionId, id, namespace = "") => {
127
+ if (readPendingCommand(sessionId, namespace)?.id !== id) return;
128
+ try {
129
+ unlinkSync(commandFile(sessionId, namespace));
130
+ } catch {
131
+ }
132
+ };
78
133
  var isDefaultSession = (sessionId) => sanitize(sessionId) === DEFAULT_SESSION;
79
134
  var savePendingQuestion = (sessionId, correlationId) => {
80
135
  const dir = dirFor(sessionId);
@@ -120,6 +175,10 @@ export {
120
175
  cancelQuestion,
121
176
  sendNotification,
122
177
  DEFAULT_SESSION,
178
+ readPendingCommand,
179
+ savePendingCommand,
180
+ markPendingCommandDelivered,
181
+ removePendingCommand,
123
182
  isDefaultSession,
124
183
  savePendingQuestion,
125
184
  listPendingQuestions,
@@ -17,7 +17,7 @@ import {
17
17
  readLastUserPrompt,
18
18
  repoKeyFor,
19
19
  throttlePass
20
- } from "./chunk-6XSC6Z2K.js";
20
+ } from "./chunk-3UBYTSCY.js";
21
21
  import {
22
22
  DEFAULT_SESSION,
23
23
  askUser,
@@ -25,7 +25,7 @@ import {
25
25
  pollForAnswer,
26
26
  savePendingQuestion,
27
27
  sendNotification
28
- } from "./chunk-J3YDT4HM.js";
28
+ } from "./chunk-HUJAAWNL.js";
29
29
  import {
30
30
  deriveAction,
31
31
  deriveActionBody,
@@ -68,6 +68,8 @@ interface ReportEventOptions {
68
68
  interface EventResponse {
69
69
  readonly ok?: boolean;
70
70
  readonly pendingCommand?: string;
71
+ readonly pendingCommandId?: string;
72
+ readonly pendingCommandExpiresAtMs?: number;
71
73
  }
72
74
  declare const reportEvent: (event: AgentEvent, options?: ReportEventOptions) => Promise<EventResponse | null>;
73
75
  declare const handlePostToolUse: (input: {
package/dist/src/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  handlePreToolUse
3
- } from "../chunk-HEQVIIBN.js";
3
+ } from "../chunk-JT6TIBYH.js";
4
4
  import "../chunk-ATBKJNWS.js";
5
5
  import "../chunk-VZKXBFUA.js";
6
6
  import "../chunk-YHG74UFF.js";
@@ -12,12 +12,12 @@ import {
12
12
  handlePostToolUse,
13
13
  handleStop,
14
14
  reportEvent
15
- } from "../chunk-6XSC6Z2K.js";
15
+ } from "../chunk-3UBYTSCY.js";
16
16
  import {
17
17
  askUser,
18
18
  cancelQuestion,
19
19
  waitForAnswer
20
- } from "../chunk-J3YDT4HM.js";
20
+ } from "../chunk-HUJAAWNL.js";
21
21
  import "../chunk-LGXTBUT4.js";
22
22
  import "../chunk-RN3NOEJF.js";
23
23
  import "../chunk-BSZYIAZL.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pushary/agent-hooks",
3
- "version": "0.89.2",
3
+ "version": "0.89.3",
4
4
  "description": "Permission hooks for AI coding agents: route tool approvals through Pushary push notifications",
5
5
  "keywords": [
6
6
  "pushary",