@pushary/agent-hooks 0.89.2 → 0.89.4

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 (58) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/README.md +40 -0
  3. package/dist/bin/pushary-bell-hook.js +3 -3
  4. package/dist/bin/pushary-bell.js +9 -10
  5. package/dist/bin/pushary-claude.js +106 -24
  6. package/dist/bin/pushary-clean.js +28 -7
  7. package/dist/bin/pushary-codex-bridge.js +17 -7
  8. package/dist/bin/pushary-codex-hook.js +8 -7
  9. package/dist/bin/pushary-codex.js +6 -4
  10. package/dist/bin/pushary-connect.js +4 -4
  11. package/dist/bin/pushary-daemon.js +141 -21
  12. package/dist/bin/pushary-disconnect.js +3 -4
  13. package/dist/bin/pushary-doctor.js +45 -29
  14. package/dist/bin/pushary-elicitation-hook.js +4 -2
  15. package/dist/bin/pushary-gemini-bridge.js +16 -6
  16. package/dist/bin/pushary-gemini-hook.js +6 -4
  17. package/dist/bin/pushary-hook.js +9 -7
  18. package/dist/bin/pushary-login.js +4 -4
  19. package/dist/bin/pushary-logout.js +5 -6
  20. package/dist/bin/pushary-mode.js +2 -2
  21. package/dist/bin/pushary-notification-hook.js +6 -4
  22. package/dist/bin/pushary-opencode-hook.js +14 -6
  23. package/dist/bin/pushary-permission-denied-hook.js +7 -5
  24. package/dist/bin/pushary-permission-hook.js +7 -5
  25. package/dist/bin/pushary-post-hook.js +6 -4
  26. package/dist/bin/pushary-prompt-hook.js +6 -4
  27. package/dist/bin/pushary-session-end-hook.js +6 -4
  28. package/dist/bin/pushary-session-start-hook.js +6 -4
  29. package/dist/bin/pushary-setup.js +49 -28
  30. package/dist/bin/pushary-stats.js +2 -2
  31. package/dist/bin/pushary-status.js +7 -6
  32. package/dist/bin/pushary-stop-hook.js +6 -4
  33. package/dist/bin/pushary-stopfailure-hook.js +6 -4
  34. package/dist/bin/pushary-suggestions.js +2 -2
  35. package/dist/bin/pushary-upgrade.js +63 -28
  36. package/dist/bin/pushary-wait.js +2 -2
  37. package/dist/bin/pushary.js +1 -1
  38. package/dist/chunk-2HYPZLYY.js +45 -0
  39. package/dist/{chunk-ZYPS2XMT.js → chunk-4ZHQNBDP.js} +1 -1
  40. package/dist/{chunk-B7JE4XHD.js → chunk-7IJYOBHL.js} +1 -1
  41. package/dist/{chunk-NRGRUOAR.js → chunk-FWUEYXSD.js} +4 -0
  42. package/dist/{chunk-J3YDT4HM.js → chunk-GKAKAOKC.js} +68 -2
  43. package/dist/{chunk-GOEXZ5QJ.js → chunk-GPNOHKFO.js} +9 -2
  44. package/dist/{chunk-SQOFGPNX.js → chunk-KJ57OPCC.js} +1 -1
  45. package/dist/{chunk-LGXTBUT4.js → chunk-KOPUTJQC.js} +1 -0
  46. package/dist/{chunk-HEQVIIBN.js → chunk-MZBT2LXW.js} +4 -4
  47. package/dist/{chunk-6XSC6Z2K.js → chunk-NFI2K2ZQ.js} +80 -23
  48. package/dist/{chunk-K42SZKYF.js → chunk-NLMSSKES.js} +251 -6
  49. package/dist/{chunk-4VYUMYWD.js → chunk-OBI4YMCT.js} +17 -2
  50. package/dist/{chunk-LUCFN47K.js → chunk-QH4AA7QV.js} +1 -1
  51. package/dist/{chunk-VKVHY5HD.js → chunk-V37ZXMH3.js} +11 -3
  52. package/dist/{chunk-MHBC2XUP.js → chunk-VP7SDXYG.js} +1 -1
  53. package/dist/src/index.d.ts +2 -0
  54. package/dist/src/index.js +7 -5
  55. package/package.json +1 -1
  56. package/dist/chunk-GIRT7677.js +0 -11
  57. package/dist/chunk-RN3NOEJF.js +0 -9
  58. package/dist/{chunk-LALHWDWL.js → chunk-W6NPWHBJ.js} +3 -3
@@ -14,8 +14,8 @@ import {
14
14
  } from "../chunk-GMXKITVA.js";
15
15
  import {
16
16
  exitOnHelpFlag
17
- } from "../chunk-SQOFGPNX.js";
18
- import "../chunk-GOEXZ5QJ.js";
17
+ } from "../chunk-KJ57OPCC.js";
18
+ import "../chunk-GPNOHKFO.js";
19
19
  import "../chunk-DINDL2CF.js";
20
20
  import "../chunk-SAF6HGAA.js";
21
21
  import {
@@ -3,7 +3,7 @@ import {
3
3
  COMMANDS,
4
4
  isCommandName,
5
5
  renderHelp
6
- } from "../chunk-GOEXZ5QJ.js";
6
+ } from "../chunk-GPNOHKFO.js";
7
7
  import {
8
8
  getPackageVersion
9
9
  } from "../chunk-DINDL2CF.js";
@@ -0,0 +1,45 @@
1
+ import {
2
+ pusharyDir
3
+ } from "./chunk-V37ZXMH3.js";
4
+ import {
5
+ writeFileAtomic
6
+ } from "./chunk-6MTNS63X.js";
7
+
8
+ // src/identity.ts
9
+ import { randomUUID } from "crypto";
10
+ import { mkdirSync, readFileSync, writeFileSync } from "fs";
11
+ import { dirname, join } from "path";
12
+ var UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
13
+ var readMachineId = (filePath) => {
14
+ try {
15
+ const value = readFileSync(filePath, "utf8").trim();
16
+ return UUID.test(value) ? value : null;
17
+ } catch {
18
+ return null;
19
+ }
20
+ };
21
+ var getOrCreateMachineId = (options = {}) => {
22
+ const filePath = options.filePath ?? join(pusharyDir(), "machine-id");
23
+ const existing = readMachineId(filePath);
24
+ if (existing) return existing;
25
+ const id = (options.randomId ?? randomUUID)();
26
+ if (!UUID.test(id)) throw new Error("machine id generator returned an invalid UUID");
27
+ mkdirSync(dirname(filePath), { recursive: true });
28
+ try {
29
+ writeFileSync(filePath, `${id}
30
+ `, { flag: "wx", mode: 384 });
31
+ return id;
32
+ } catch (err) {
33
+ if (err.code !== "EEXIST") throw err;
34
+ }
35
+ const winner = readMachineId(filePath);
36
+ if (winner) return winner;
37
+ writeFileAtomic(filePath, `${id}
38
+ `, 384);
39
+ return id;
40
+ };
41
+ var getMachineId = (options = {}) => getOrCreateMachineId(options);
42
+
43
+ export {
44
+ getMachineId
45
+ };
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  pusharyDir
3
- } from "./chunk-VKVHY5HD.js";
3
+ } from "./chunk-V37ZXMH3.js";
4
4
  import {
5
5
  readJsonSafe
6
6
  } from "./chunk-6MTNS63X.js";
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  readConfigFileKey
3
- } from "./chunk-LALHWDWL.js";
3
+ } from "./chunk-W6NPWHBJ.js";
4
4
 
5
5
  // src/bell/fleet.ts
6
6
  import { closeSync, constants, futimesSync, lstatSync, mkdirSync, openSync, readdirSync, unlinkSync } from "fs";
@@ -1,4 +1,6 @@
1
1
  // src/live-capabilities.ts
2
+ var CLAUDE_BRIDGE_CAPABILITY = "claude-bridge";
3
+ var CLAUDE_RESUME_CAPABILITY = "claude-resume";
2
4
  var CODEX_BRIDGE_CAPABILITY = "codex-bridge";
3
5
  var CODEX_RESUME_CAPABILITY = "codex-resume";
4
6
  var GEMINI_BRIDGE_CAPABILITY = "gemini-bridge";
@@ -19,6 +21,8 @@ var geminiLiveCapabilities = (rawVersion) => {
19
21
  };
20
22
 
21
23
  export {
24
+ CLAUDE_BRIDGE_CAPABILITY,
25
+ CLAUDE_RESUME_CAPABILITY,
22
26
  CODEX_BRIDGE_CAPABILITY,
23
27
  CODEX_RESUME_CAPABILITY,
24
28
  geminiLiveCapabilities
@@ -68,13 +68,74 @@ 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 {
95
+ ...value,
96
+ delivered: value.delivered === true,
97
+ ...value.failed === true ? { failed: true } : {}
98
+ };
99
+ } catch {
100
+ return null;
101
+ }
102
+ };
103
+ var savePendingCommand = (sessionId, command, namespace = "") => {
104
+ const dir = commandDirFor(sessionId, namespace);
105
+ try {
106
+ mkdirSync(dir, { recursive: true, mode: 448 });
107
+ writeFileSync(commandFile(sessionId, namespace), JSON.stringify(command), { encoding: "utf-8", mode: 384, flag: "wx" });
108
+ return true;
109
+ } catch {
110
+ return false;
111
+ }
112
+ };
113
+ var updatePendingCommand = (sessionId, id, update, namespace = "") => {
114
+ const command = readPendingCommand(sessionId, namespace);
115
+ if (!command || command.id !== id) return false;
116
+ const file = commandFile(sessionId, namespace);
117
+ const temporary = `${file}.${process.pid}.tmp`;
118
+ try {
119
+ writeFileSync(temporary, JSON.stringify({ ...command, ...update }), { encoding: "utf-8", mode: 384 });
120
+ renameSync(temporary, file);
121
+ return true;
122
+ } catch {
123
+ try {
124
+ unlinkSync(temporary);
125
+ } catch {
126
+ }
127
+ return false;
128
+ }
129
+ };
130
+ var markPendingCommandDelivered = (sessionId, id, namespace = "") => updatePendingCommand(sessionId, id, { delivered: true, failed: void 0 }, namespace);
131
+ var markPendingCommandFailed = (sessionId, id, namespace = "") => updatePendingCommand(sessionId, id, { delivered: false, failed: true }, namespace);
132
+ var removePendingCommand = (sessionId, id, namespace = "") => {
133
+ if (readPendingCommand(sessionId, namespace)?.id !== id) return;
134
+ try {
135
+ unlinkSync(commandFile(sessionId, namespace));
136
+ } catch {
137
+ }
138
+ };
78
139
  var isDefaultSession = (sessionId) => sanitize(sessionId) === DEFAULT_SESSION;
79
140
  var savePendingQuestion = (sessionId, correlationId) => {
80
141
  const dir = dirFor(sessionId);
@@ -120,6 +181,11 @@ export {
120
181
  cancelQuestion,
121
182
  sendNotification,
122
183
  DEFAULT_SESSION,
184
+ readPendingCommand,
185
+ savePendingCommand,
186
+ markPendingCommandDelivered,
187
+ markPendingCommandFailed,
188
+ removePendingCommand,
123
189
  isDefaultSession,
124
190
  savePendingQuestion,
125
191
  listPendingQuestions,
@@ -19,8 +19,8 @@ var COMMAND_LIST = [
19
19
  module: "pushary-daemon",
20
20
  summary: "Keep this machine reachable so your phone can START a new session",
21
21
  detail: [
22
- "Leave it running in a project dir; it launches a headless",
23
- "'pushary claude --remote' when you send a prompt from the app."
22
+ "Install the native per-user service for restart-on-login and restart-on-failure.",
23
+ "A bare `pushary daemon` remains foreground mode for diagnostics."
24
24
  ]
25
25
  },
26
26
  {
@@ -114,6 +114,13 @@ var COMMAND_OPTIONS = {
114
114
  claude: [
115
115
  ["--remote", "Start headless and drive it entirely from your phone"],
116
116
  ["-p <prompt>", "Initial prompt"]
117
+ ],
118
+ daemon: [
119
+ ["install", "Install and start the native per-user service"],
120
+ ["start | stop | restart", "Control the installed service"],
121
+ ["status", "Show local daemon health and PID"],
122
+ ["uninstall", "Stop and remove the native service"],
123
+ ["foreground", "Run attached to this terminal for diagnostics"]
117
124
  ]
118
125
  };
119
126
  var SETUP_OPTIONS = [
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  renderCommandHelp
3
- } from "./chunk-GOEXZ5QJ.js";
3
+ } from "./chunk-GPNOHKFO.js";
4
4
  import {
5
5
  getPackageVersion
6
6
  } from "./chunk-DINDL2CF.js";
@@ -268,6 +268,7 @@ var sessionEndAction = (reason) => typeof reason === "string" && SESSION_END_ACT
268
268
  var taskTitleFrom = (prompt) => typeof prompt === "string" ? prompt.replace(/\s+/g, " ").trim().slice(0, TASK_TITLE_MAX_LENGTH) || void 0 : void 0;
269
269
 
270
270
  export {
271
+ HOOK_FALLBACK_MAX,
271
272
  describeToolCall,
272
273
  deriveToolTarget,
273
274
  parseApplyPatchOps,
@@ -17,7 +17,7 @@ import {
17
17
  readLastUserPrompt,
18
18
  repoKeyFor,
19
19
  throttlePass
20
- } from "./chunk-6XSC6Z2K.js";
20
+ } from "./chunk-NFI2K2ZQ.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-GKAKAOKC.js";
29
29
  import {
30
30
  deriveAction,
31
31
  deriveActionBody,
@@ -33,10 +33,10 @@ import {
33
33
  deriveToolTarget,
34
34
  describeToolCall,
35
35
  scopePathFor
36
- } from "./chunk-LGXTBUT4.js";
36
+ } from "./chunk-KOPUTJQC.js";
37
37
  import {
38
38
  getMachineId
39
- } from "./chunk-RN3NOEJF.js";
39
+ } from "./chunk-2HYPZLYY.js";
40
40
  import {
41
41
  getApiKey,
42
42
  getBaseUrl
@@ -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-GKAKAOKC.js";
12
16
  import {
13
17
  AGENT_IDENTITIES,
14
18
  deriveReceiptMeta,
@@ -22,10 +26,10 @@ import {
22
26
  summarizeStop,
23
27
  taskTitleFrom,
24
28
  toolResultErrorText
25
- } from "./chunk-LGXTBUT4.js";
29
+ } from "./chunk-KOPUTJQC.js";
26
30
  import {
27
31
  getMachineId
28
- } from "./chunk-RN3NOEJF.js";
32
+ } from "./chunk-2HYPZLYY.js";
29
33
  import {
30
34
  withRetry
31
35
  } from "./chunk-BSZYIAZL.js";
@@ -666,10 +670,37 @@ 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, state = "delivered") => {
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({
682
+ commandId,
683
+ sessionId: sessionId ?? null,
684
+ machineId,
685
+ state,
686
+ ...state === "failed" ? { failureCode: "consumer_rejected" } : {}
687
+ }),
688
+ signal: AbortSignal.timeout(timeoutMs)
689
+ });
690
+ return response.ok;
691
+ } catch {
692
+ return false;
693
+ }
694
+ };
669
695
  var reportEvent = async (event, options = {}) => {
670
696
  const apiKey = options.apiKey ?? getApiKey();
671
697
  const baseUrl = getBaseUrl();
698
+ const machineId = event.machineId ?? getMachineId();
699
+ const sessionKey = event.sessionId || DEFAULT_SESSION;
700
+ const receiptNamespace = `${apiKey}:${machineId}:${event.agentType}`;
701
+ const clientCanDrain = consumesActivityContext(event.agentType) && options.timeoutMs === void 0;
672
702
  return withRetry(async () => {
703
+ const storedCommand = clientCanDrain ? readPendingCommand(sessionKey, receiptNamespace) : null;
673
704
  const res = await fetch(`${baseUrl}/api/agent/event`, {
674
705
  method: "POST",
675
706
  headers: {
@@ -678,7 +709,7 @@ var reportEvent = async (event, options = {}) => {
678
709
  },
679
710
  body: JSON.stringify({
680
711
  ...event,
681
- machineId: event.machineId ?? getMachineId(),
712
+ machineId,
682
713
  // Lets the phone's Stop end this process instead of only denying its next
683
714
  // tool call. Ours alone: never another process, never a parent.
684
715
  pid: event.pid,
@@ -693,27 +724,56 @@ var reportEvent = async (event, options = {}) => {
693
724
  // Link-back for a phone-spawned session: forward the spawn id the daemon
694
725
  // set in the environment so the server can map spawnId -> sessionId.
695
726
  ...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
727
+ canDrainCommand: clientCanDrain && !storedCommand,
728
+ ackMode: clientCanDrain && !storedCommand,
729
+ skipCommandDrain: storedCommand != null
711
730
  }),
712
731
  signal: AbortSignal.timeout(options.timeoutMs ?? 1e4)
713
732
  });
714
733
  if (options.requireOk && !res.ok) throw new Error(`/api/agent/event rejected (${res.status})`);
715
734
  try {
716
- return await res.json();
735
+ const result = await res.json();
736
+ const withoutPending = () => ({ ...result, pendingCommand: void 0, pendingCommandId: void 0 });
737
+ if (storedCommand) {
738
+ const failed = storedCommand.failed === true;
739
+ const acknowledged = await acknowledgePendingCommand(
740
+ apiKey,
741
+ baseUrl,
742
+ storedCommand.id,
743
+ event.sessionId,
744
+ machineId,
745
+ options.timeoutMs ?? 1e4,
746
+ failed ? "failed" : "delivered"
747
+ );
748
+ if (!acknowledged) return withoutPending();
749
+ if (storedCommand.delivered || failed) {
750
+ removePendingCommand(sessionKey, storedCommand.id, receiptNamespace);
751
+ return withoutPending();
752
+ }
753
+ if (!markPendingCommandDelivered(sessionKey, storedCommand.id, receiptNamespace)) return withoutPending();
754
+ return { ...withoutPending(), pendingCommand: storedCommand.text, pendingCommandId: storedCommand.id };
755
+ }
756
+ if (typeof result.pendingCommandId === "string" && typeof result.pendingCommand === "string") {
757
+ const saved = savePendingCommand(sessionKey, {
758
+ id: result.pendingCommandId,
759
+ text: result.pendingCommand,
760
+ expiresAtMs: typeof result.pendingCommandExpiresAtMs === "number" ? result.pendingCommandExpiresAtMs : Date.now() + 36e5,
761
+ delivered: false
762
+ }, receiptNamespace);
763
+ if (!saved) return withoutPending();
764
+ const acknowledged = await acknowledgePendingCommand(
765
+ apiKey,
766
+ baseUrl,
767
+ result.pendingCommandId,
768
+ event.sessionId,
769
+ machineId,
770
+ options.timeoutMs ?? 1e4
771
+ );
772
+ if (!acknowledged || !markPendingCommandDelivered(sessionKey, result.pendingCommandId, receiptNamespace)) {
773
+ return withoutPending();
774
+ }
775
+ }
776
+ return result;
717
777
  } catch {
718
778
  return null;
719
779
  }
@@ -810,9 +870,6 @@ var handleStop = async (input, agent = CLAUDE_CODE_AGENT) => {
810
870
  const sessionKey = input.session_id || DEFAULT_SESSION;
811
871
  const late = await reconcilePendingQuestions(sessionKey);
812
872
  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
873
  reportEvent({
817
874
  event: "session_end",
818
875
  agentType: agent.type,