@pushary/agent-hooks 0.83.2 → 0.84.1

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 (64) hide show
  1. package/CHANGELOG.md +94 -0
  2. package/dist/bin/pushary-bell-hook.js +5 -5
  3. package/dist/bin/pushary-bell.js +15 -14
  4. package/dist/bin/pushary-claude.js +93 -143
  5. package/dist/bin/pushary-clean.js +13 -12
  6. package/dist/bin/pushary-codex-bridge.d.ts +1 -0
  7. package/dist/bin/pushary-codex-bridge.js +349 -0
  8. package/dist/bin/pushary-codex-hook.js +18 -14
  9. package/dist/bin/pushary-codex.js +7 -7
  10. package/dist/bin/pushary-connect.js +13 -12
  11. package/dist/bin/pushary-daemon.js +331 -41
  12. package/dist/bin/pushary-doctor.js +48 -25
  13. package/dist/bin/pushary-gemini-bridge.d.ts +1 -0
  14. package/dist/bin/pushary-gemini-bridge.js +299 -0
  15. package/dist/bin/pushary-gemini-hook.js +28 -21
  16. package/dist/bin/pushary-hook.js +14 -13
  17. package/dist/bin/pushary-login.js +15 -14
  18. package/dist/bin/pushary-logout.js +12 -11
  19. package/dist/bin/pushary-mode.js +10 -9
  20. package/dist/bin/pushary-notification-hook.js +5 -5
  21. package/dist/bin/pushary-permission-denied-hook.js +9 -9
  22. package/dist/bin/pushary-permission-hook.js +9 -9
  23. package/dist/bin/pushary-post-hook.js +5 -5
  24. package/dist/bin/pushary-prompt-hook.js +5 -5
  25. package/dist/bin/pushary-session-end-hook.js +5 -5
  26. package/dist/bin/pushary-session-start-hook.js +5 -5
  27. package/dist/bin/pushary-setup.js +42 -36
  28. package/dist/bin/pushary-stats.js +6 -5
  29. package/dist/bin/pushary-status.js +17 -16
  30. package/dist/bin/pushary-stop-hook.js +5 -5
  31. package/dist/bin/pushary-stopfailure-hook.js +5 -5
  32. package/dist/bin/pushary-suggestions.js +7 -6
  33. package/dist/bin/pushary-upgrade.js +18 -17
  34. package/dist/bin/pushary-wait.js +10 -9
  35. package/dist/bin/pushary.js +8 -6
  36. package/dist/{chunk-SAA3VBDK.js → chunk-437T77TY.js} +3 -3
  37. package/dist/{chunk-U27BZWFT.js → chunk-6LC4WSCL.js} +1 -1
  38. package/dist/{chunk-7WWZPII5.js → chunk-7PZPJSWI.js} +1 -1
  39. package/dist/{chunk-4QQOKEOS.js → chunk-BLDDXHHA.js} +6 -6
  40. package/dist/{chunk-7PO2VEA4.js → chunk-CCCJ6BPW.js} +40 -34
  41. package/dist/chunk-CCH775Y6.js +343 -0
  42. package/dist/chunk-DINDL2CF.js +45 -0
  43. package/dist/{chunk-TXF4QZSQ.js → chunk-DOXSWT4Q.js} +1 -1
  44. package/dist/chunk-ERYKGH4J.js +140 -0
  45. package/dist/{chunk-QR6MVVQ7.js → chunk-F52SIIDG.js} +4 -4
  46. package/dist/{chunk-26D2YMDU.js → chunk-FBGBJXVC.js} +1 -1
  47. package/dist/chunk-GCKAWHFP.js +124 -0
  48. package/dist/{chunk-QF7WTF2C.js → chunk-HZNOAJGB.js} +2 -2
  49. package/dist/chunk-ILB6UOAF.js +192 -0
  50. package/dist/{chunk-RRZZ7TFJ.js → chunk-J4KFVOCQ.js} +4 -2
  51. package/dist/{chunk-WDRRULBH.js → chunk-K7CSRGKZ.js} +4 -4
  52. package/dist/{chunk-ZUKH2NPJ.js → chunk-MGJBC6DD.js} +1 -44
  53. package/dist/{chunk-GQLB2GBJ.js → chunk-MHBC2XUP.js} +1 -1
  54. package/dist/{chunk-YWRJGYUH.js → chunk-MQMPG5X4.js} +28 -2
  55. package/dist/{chunk-3AGM3TDM.js → chunk-NJ7JT26G.js} +1 -1
  56. package/dist/chunk-NRGRUOAR.js +25 -0
  57. package/dist/{chunk-EHLTWCDZ.js → chunk-SEAHGHCE.js} +1 -1
  58. package/dist/{chunk-NDC3TBHI.js → chunk-UKSQWTBH.js} +1 -1
  59. package/dist/{chunk-UVAC2KHH.js → chunk-UXWEE3QI.js} +1 -1
  60. package/dist/{chunk-MUEW424A.js → chunk-VT4IVERX.js} +50 -3
  61. package/dist/{chunk-DHAZ6SOT.js → chunk-YR5TSPXB.js} +81 -25
  62. package/dist/src/index.d.ts +6 -3
  63. package/dist/src/index.js +9 -9
  64. package/package.json +3 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,99 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.84.1
4
+
5
+ ### Remote starts survive lost acknowledgements without double-launching
6
+
7
+ A phone-started agent now claims exclusive process ownership before the provider
8
+ starts. If the daemon restarts or the launch acknowledgement is lost, retrying the
9
+ same request reconnects to the existing process instead of starting a second one.
10
+
11
+ ### Repository approval scopes reach Codex and Gemini
12
+
13
+ Codex and Gemini sessions now carry their repository identity through approval
14
+ requests, so repository-scoped rules stay inside the repository that created them.
15
+
16
+ ### Encrypted transcripts retry safely
17
+
18
+ Transcript writers now retry while a session key is temporarily unavailable and
19
+ stop on a real key conflict, preventing undecryptable transcript entries from being
20
+ accepted or silently dropped.
21
+
22
+ ## 0.84.0
23
+
24
+ ### The phone can start an agent on a machine that is not running one
25
+
26
+ The phone could reach a session that already existed. Starting one meant being at
27
+ the keyboard. `pushary setup` and `pushary claude` now bring up a small background
28
+ process, `pushary-daemon`, which watches for a launch requested from the phone and
29
+ starts a real session with the prompt you sent.
30
+
31
+ It is a poller over a durable queue, not an open socket. Idle, it asks the server
32
+ for work every 30 seconds and sends one heartbeat a minute, and it carries a
33
+ prompt, never any code. One request is in flight at a time, each bounded by its
34
+ own timeout, with a spawn rate cap so a queue backlog cannot fill a machine with
35
+ agents. Exactly one daemon runs per machine: a second one asks the first to hand
36
+ over rather than racing it, and it declines to take over from a daemon that is
37
+ still healthy.
38
+
39
+ This is a background process that was not running before. `pushary doctor` reports
40
+ whether it is up, and `pushary daemon` in a terminal shows you what it is doing.
41
+
42
+ A launch is now acknowledged rather than assumed. The daemon claims a request,
43
+ confirms the session it started, and the phone shows what actually happened,
44
+ including the failure when a launch does not come up.
45
+
46
+ ### Always allowing a tool no longer has to mean everywhere
47
+
48
+ "Approve and always allow" wrote one kind of rule: this tool, this whole
49
+ workspace, until you delete it. That is the right scope for `Read` and far too
50
+ wide for a push.
51
+
52
+ The approval now offers the scope alongside it. Workspace is the previous
53
+ behaviour. Repository binds the rule to the repository the ask came from, so a
54
+ rule written in one checkout stops governing another. Session binds it to the run
55
+ you are watching and goes away with it.
56
+
57
+ An older Pushary never sees the narrower rules. A CLI that does not ask for them
58
+ is sent workspace-wide rules only, so a rule you scoped to one session cannot be
59
+ quietly applied everywhere by something released before the idea existed.
60
+
61
+ ### AskUserQuestion reaches the phone with its descriptions
62
+
63
+ A Claude Code question carries a header, a description per option, and sometimes
64
+ permission to pick more than one. The phone received the bare labels. It now
65
+ receives the question the way the agent wrote it.
66
+
67
+ One question goes out as one card carrying the whole shape. Every surface that
68
+ does not know that shape, the web decision page and Slack among them, still sees
69
+ an ordinary choice list and answers with a label, which the server reads back onto
70
+ the question. Two or more questions go out one at a time, which is what every
71
+ surface has always been able to answer: a bare label cannot say which of several
72
+ questions it belongs to, and a half-filled answer is worse for the agent than
73
+ being asked again in the terminal.
74
+
75
+ ### Codex and Gemini sessions started from the phone stay live
76
+
77
+ A phone-started Codex or Gemini run now goes through a bridge that survives the
78
+ connection dropping and resumes the same session instead of opening a new one.
79
+ The machine advertises only what it can actually do, so a Gemini CLI too old for
80
+ the bridge is not offered as one, and the phone stops showing controls that would
81
+ fail.
82
+
83
+ Stopping is acknowledged the same way. A stop request names the session and the
84
+ process it belongs to, and the machine proves it owns that process before acting
85
+ on it, so a recycled pid cannot be mistaken for the agent you asked to stop.
86
+
87
+
88
+ ## 0.83.0 to 0.83.2
89
+
90
+ Three releases went out without an entry here. In order: `ask_user` became
91
+ idempotent, so a create that gets retried after a timeout resolves to the question
92
+ already asked instead of buzzing a phone twice. The pairing QR stopped encoding a
93
+ code only a browser could resolve. And the gate's two shared pieces, the verdict a
94
+ policy produces and the "most specific rule wins" precedence, moved into the
95
+ package the CLI and the server both read, with no change to what either decides.
96
+
3
97
  ## 0.82.0
4
98
 
5
99
  ### Setup reads properly when an agent is the one running it
@@ -1,13 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  handleBell
4
- } from "../chunk-7WWZPII5.js";
5
- import "../chunk-WDRRULBH.js";
6
- import "../chunk-6MTNS63X.js";
4
+ } from "../chunk-7PZPJSWI.js";
5
+ import "../chunk-K7CSRGKZ.js";
7
6
  import "../chunk-BC3VCZ3E.js";
8
- import "../chunk-SAA3VBDK.js";
9
- import "../chunk-YWRJGYUH.js";
7
+ import "../chunk-6MTNS63X.js";
8
+ import "../chunk-437T77TY.js";
10
9
  import "../chunk-2UMNXADU.js";
10
+ import "../chunk-MQMPG5X4.js";
11
11
 
12
12
  // bin/pushary-bell-hook.ts
13
13
  var main = async () => {
@@ -5,33 +5,34 @@ import {
5
5
  liveAgentCount,
6
6
  ring,
7
7
  upgradeThreshold
8
- } from "../chunk-7WWZPII5.js";
8
+ } from "../chunk-7PZPJSWI.js";
9
9
  import {
10
10
  claudeSettings
11
- } from "../chunk-EHLTWCDZ.js";
11
+ } from "../chunk-SEAHGHCE.js";
12
12
  import {
13
13
  createIo
14
14
  } from "../chunk-5RUIFDTP.js";
15
- import "../chunk-WDRRULBH.js";
16
- import {
17
- readJsonSafe,
18
- writeJsonAtomic
19
- } from "../chunk-6MTNS63X.js";
15
+ import "../chunk-K7CSRGKZ.js";
20
16
  import {
21
17
  parseFlags
22
18
  } from "../chunk-GMXKITVA.js";
23
- import "../chunk-BC3VCZ3E.js";
24
- import "../chunk-SAA3VBDK.js";
25
- import "../chunk-7QLSKOSU.js";
26
- import "../chunk-YWRJGYUH.js";
27
19
  import {
28
20
  exitOnHelpFlag
29
- } from "../chunk-RRZZ7TFJ.js";
30
- import "../chunk-ZUKH2NPJ.js";
31
- import "../chunk-2UMNXADU.js";
21
+ } from "../chunk-J4KFVOCQ.js";
22
+ import "../chunk-MGJBC6DD.js";
23
+ import "../chunk-BC3VCZ3E.js";
24
+ import "../chunk-DINDL2CF.js";
25
+ import {
26
+ readJsonSafe,
27
+ writeJsonAtomic
28
+ } from "../chunk-6MTNS63X.js";
29
+ import "../chunk-437T77TY.js";
30
+ import "../chunk-7QLSKOSU.js";
32
31
  import {
33
32
  EXIT
34
33
  } from "../chunk-KZERVKTD.js";
34
+ import "../chunk-2UMNXADU.js";
35
+ import "../chunk-MQMPG5X4.js";
35
36
 
36
37
  // src/bell/install.ts
37
38
  import { join } from "path";
@@ -1,9 +1,30 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ ensureDaemonRunning
4
+ } from "../chunk-CCH775Y6.js";
5
+ import {
6
+ buildTranscriptRecords,
7
+ decodePublicKey,
8
+ fetchAccountPublicKey,
9
+ generateSessionKey,
10
+ uploadTranscriptRecords,
11
+ wrapSessionKey
12
+ } from "../chunk-GCKAWHFP.js";
13
+ import {
14
+ startProcessOwnership,
15
+ withSpawnProcessOwnership
16
+ } from "../chunk-ILB6UOAF.js";
17
+ import {
18
+ needsShell,
19
+ spawnClaude
20
+ } from "../chunk-XHKBHWLX.js";
21
+ import "../chunk-VT4IVERX.js";
22
+ import "../chunk-SEAHGHCE.js";
2
23
  import {
3
24
  KILL_REASON,
4
25
  isDeferAnswer,
5
26
  resolveGate
6
- } from "../chunk-4QQOKEOS.js";
27
+ } from "../chunk-BLDDXHHA.js";
7
28
  import {
8
29
  askUser,
9
30
  cancelQuestion,
@@ -15,26 +36,22 @@ import {
15
36
  reportEvent,
16
37
  scopePathFor,
17
38
  waitForAnswer
18
- } from "../chunk-7PO2VEA4.js";
19
- import "../chunk-BSZYIAZL.js";
20
- import "../chunk-SAF6HGAA.js";
21
- import "../chunk-SAA3VBDK.js";
22
- import "../chunk-7QLSKOSU.js";
23
- import {
24
- redactSecrets
25
- } from "../chunk-YWRJGYUH.js";
26
- import {
27
- needsShell,
28
- spawnClaude
29
- } from "../chunk-XHKBHWLX.js";
39
+ } from "../chunk-CCCJ6BPW.js";
30
40
  import {
31
41
  getMachineId
32
42
  } from "../chunk-RN3NOEJF.js";
43
+ import "../chunk-DINDL2CF.js";
44
+ import "../chunk-6MTNS63X.js";
45
+ import "../chunk-BSZYIAZL.js";
46
+ import "../chunk-SAF6HGAA.js";
47
+ import "../chunk-437T77TY.js";
48
+ import "../chunk-7QLSKOSU.js";
49
+ import "../chunk-KZERVKTD.js";
33
50
  import {
34
51
  getApiKey,
35
52
  getBaseUrl
36
53
  } from "../chunk-2UMNXADU.js";
37
- import "../chunk-KZERVKTD.js";
54
+ import "../chunk-MQMPG5X4.js";
38
55
 
39
56
  // src/wrapper/claudeBinary.ts
40
57
  import { statSync } from "fs";
@@ -97,9 +114,9 @@ var runLocalPassthrough = (binary, args2) => {
97
114
  cleanup();
98
115
  resolve2(127);
99
116
  });
100
- child.on("exit", (code, signal) => {
117
+ child.on("exit", (code2, signal) => {
101
118
  cleanup();
102
- if (typeof code === "number") resolve2(code);
119
+ if (typeof code2 === "number") resolve2(code2);
103
120
  else if (signal) resolve2(128 + (SIGNAL_NUMBERS[signal] ?? 0));
104
121
  else resolve2(0);
105
122
  });
@@ -113,112 +130,6 @@ import { basename } from "path";
113
130
  import { homedir } from "os";
114
131
  import { join as join2, resolve } from "path";
115
132
  import { existsSync, readFileSync } from "fs";
116
-
117
- // src/transcript-capture.ts
118
- import { createHash } from "crypto";
119
-
120
- // src/crypto.ts
121
- import nacl from "tweetnacl";
122
- var EPH_PUB_LEN = nacl.box.publicKeyLength;
123
- var BOX_NONCE_LEN = nacl.box.nonceLength;
124
- var SECRETBOX_NONCE_LEN = nacl.secretbox.nonceLength;
125
- var SESSION_KEY_LEN = nacl.secretbox.keyLength;
126
- var toBase64 = (bytes) => Buffer.from(bytes).toString("base64");
127
- var fromBase64 = (value) => new Uint8Array(Buffer.from(value, "base64"));
128
- var utf8Encode = (text) => new Uint8Array(Buffer.from(text, "utf-8"));
129
- var decodePublicKey = (value) => fromBase64(value);
130
- var generateSessionKey = () => nacl.randomBytes(SESSION_KEY_LEN);
131
- var wrapSessionKey = (sessionKey, accountPublicKey) => {
132
- const ephemeral = nacl.box.keyPair();
133
- const nonce = nacl.randomBytes(BOX_NONCE_LEN);
134
- const box = nacl.box(sessionKey, nonce, accountPublicKey, ephemeral.secretKey);
135
- const bundle = new Uint8Array(EPH_PUB_LEN + BOX_NONCE_LEN + box.length);
136
- bundle.set(ephemeral.publicKey, 0);
137
- bundle.set(nonce, EPH_PUB_LEN);
138
- bundle.set(box, EPH_PUB_LEN + BOX_NONCE_LEN);
139
- return toBase64(bundle);
140
- };
141
- var encryptRecord = (plaintext, sessionKey) => {
142
- const nonce = nacl.randomBytes(SECRETBOX_NONCE_LEN);
143
- const box = nacl.secretbox(utf8Encode(plaintext), nonce, sessionKey);
144
- const bundle = new Uint8Array(SECRETBOX_NONCE_LEN + box.length);
145
- bundle.set(nonce, 0);
146
- bundle.set(box, SECRETBOX_NONCE_LEN);
147
- return toBase64(bundle);
148
- };
149
-
150
- // src/transcript-capture.ts
151
- var INTERNAL_TYPES = /* @__PURE__ */ new Set([
152
- "file-history-snapshot",
153
- "change",
154
- "queue-operation",
155
- "summary"
156
- ]);
157
- var redactDeep = (value) => {
158
- if (typeof value === "string") return redactSecrets(value);
159
- if (Array.isArray(value)) return value.map(redactDeep);
160
- if (value && typeof value === "object") {
161
- const out = {};
162
- for (const [key, inner] of Object.entries(value)) {
163
- out[key] = redactDeep(inner);
164
- }
165
- return out;
166
- }
167
- return value;
168
- };
169
- var localIdFor = (parsed, rawLine) => typeof parsed.uuid === "string" && parsed.uuid.length > 0 ? parsed.uuid : createHash("sha256").update(rawLine).digest("hex").slice(0, 32);
170
- var buildTranscriptRecords = (lines, sessionKey, seen = /* @__PURE__ */ new Set()) => {
171
- const records = [];
172
- const nextSeen = new Set(seen);
173
- for (const line of lines) {
174
- const trimmed = line.trim();
175
- if (!trimmed) continue;
176
- let parsed;
177
- try {
178
- parsed = JSON.parse(trimmed);
179
- } catch {
180
- continue;
181
- }
182
- const type = typeof parsed.type === "string" ? parsed.type : "";
183
- if (INTERNAL_TYPES.has(type)) continue;
184
- const localId = localIdFor(parsed, trimmed);
185
- if (nextSeen.has(localId)) continue;
186
- nextSeen.add(localId);
187
- const content = encryptRecord(JSON.stringify(redactDeep(parsed)), sessionKey);
188
- records.push({ localId, content });
189
- }
190
- return { records, seen: nextSeen };
191
- };
192
-
193
- // src/transcript-api.ts
194
- var fetchAccountPublicKey = async (apiKey) => {
195
- try {
196
- const res = await fetch(`${getBaseUrl()}/api/agent/encryption-account`, {
197
- headers: { Authorization: `Bearer ${apiKey}` },
198
- signal: AbortSignal.timeout(5e3)
199
- });
200
- if (!res.ok) return null;
201
- const data = await res.json();
202
- return typeof data.publicKey === "string" && data.publicKey.length > 0 ? data.publicKey : null;
203
- } catch {
204
- return null;
205
- }
206
- };
207
- var uploadTranscriptRecords = async (apiKey, params) => {
208
- try {
209
- const res = await fetch(`${getBaseUrl()}/api/agent/transcript`, {
210
- method: "POST",
211
- headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
212
- body: JSON.stringify(params),
213
- signal: AbortSignal.timeout(15e3)
214
- });
215
- return res.ok;
216
- } catch {
217
- return false;
218
- }
219
- };
220
-
221
- // src/transcript-sync.ts
222
133
  var transcriptsEnabled = () => process.env.PUSHARY_TRANSCRIPTS === "1";
223
134
  var claudeTranscriptPath = (cwd, sessionId) => {
224
135
  const projectId = resolve(cwd).replace(/[^a-zA-Z0-9-]/g, "-");
@@ -229,9 +140,11 @@ var createSyncState = () => ({
229
140
  sessionKey: null,
230
141
  wrappedKey: null,
231
142
  seen: /* @__PURE__ */ new Set(),
232
- keyUploaded: false
143
+ keyUploaded: false,
144
+ keyRejected: false
233
145
  });
234
146
  var syncTick = async (lines, state, uploader, sessionId, machineId) => {
147
+ if (state.keyRejected) return state;
235
148
  let sessionKey = state.sessionKey;
236
149
  let wrappedKey = state.wrappedKey;
237
150
  if (!sessionKey) {
@@ -242,9 +155,9 @@ var syncTick = async (lines, state, uploader, sessionId, machineId) => {
242
155
  }
243
156
  const built = buildTranscriptRecords(lines, sessionKey, state.seen);
244
157
  if (built.records.length === 0) {
245
- return { sessionKey, wrappedKey, seen: built.seen, keyUploaded: state.keyUploaded };
158
+ return { ...state, sessionKey, wrappedKey, seen: built.seen };
246
159
  }
247
- const ok = await uploader.uploadRecords({
160
+ const result = await uploader.uploadRecords({
248
161
  sessionId,
249
162
  machineId,
250
163
  dataEncryptionKey: state.keyUploaded ? void 0 : wrappedKey ?? void 0,
@@ -253,8 +166,9 @@ var syncTick = async (lines, state, uploader, sessionId, machineId) => {
253
166
  return {
254
167
  sessionKey,
255
168
  wrappedKey,
256
- seen: ok ? built.seen : state.seen,
257
- keyUploaded: state.keyUploaded || ok
169
+ seen: result.ok ? built.seen : state.seen,
170
+ keyUploaded: state.keyUploaded || result.keyRegistered,
171
+ keyRejected: result.keyConflict
258
172
  };
259
173
  };
260
174
  var readLines = (path) => {
@@ -348,7 +262,7 @@ var ensureClaudeSdk = async (log) => {
348
262
  { cwd: dir, stdio: "ignore", shell: process.platform === "win32" }
349
263
  );
350
264
  child.on("error", () => resolve2(false));
351
- child.on("close", (code) => resolve2(code === 0));
265
+ child.on("close", (code2) => resolve2(code2 === 0));
352
266
  });
353
267
  if (!installed) {
354
268
  log?.(`[pushary] could not auto-install the SDK. Install it manually: npm i -g ${SDK_SPEC}`);
@@ -533,7 +447,7 @@ var createRemoteApprover = (deps) => {
533
447
  try {
534
448
  const sessionId = deps.getSessionId?.();
535
449
  const modeState = await fetchModeState2(deps.apiKey, sessionId);
536
- const policyConfig = await getPolicy2(deps.apiKey, modeState.policyVersion, "claude_code", true);
450
+ const policyConfig = await getPolicy2(deps.apiKey, modeState.policyVersion, "claude_code", true, sessionId);
537
451
  const scopePath = modeState.scope ? scopePathFor(toolName, input, deps.cwd) : void 0;
538
452
  const verdict = resolveGate({
539
453
  modeState,
@@ -542,7 +456,8 @@ var createRemoteApprover = (deps) => {
542
456
  toolInputs: [input],
543
457
  scopePaths: scopePath ? [scopePath] : [],
544
458
  cwd: deps.cwd,
545
- repoKey: repoKeyFor(deps.cwd)
459
+ repoKey: repoKeyFor(deps.cwd),
460
+ sessionId
546
461
  });
547
462
  if (verdict.kind === "kill") return deny(KILL_REASON);
548
463
  if (verdict.kind === "allow") return allow(input);
@@ -561,7 +476,8 @@ var createRemoteApprover = (deps) => {
561
476
  sessionId,
562
477
  machineId: deps.machineId,
563
478
  toolName,
564
- toolTarget
479
+ toolTarget,
480
+ repoKey: repoKeyFor(deps.cwd)
565
481
  });
566
482
  } catch {
567
483
  return applyTimeout(policy, input);
@@ -1127,13 +1043,13 @@ var runLocalLeg = (session, deps = {}) => {
1127
1043
  }
1128
1044
  finish();
1129
1045
  });
1130
- child.on("exit", (code, signal) => {
1046
+ child.on("exit", (code2, signal) => {
1131
1047
  if (signal === "SIGTERM" && legAbort.signal.aborted) {
1132
1048
  } else if (!exitReason) {
1133
1049
  if (session.input.bufferedCount > 0) {
1134
1050
  exitReason = "switch";
1135
- } else if (typeof code === "number") {
1136
- exitReason = { type: "exit", code };
1051
+ } else if (typeof code2 === "number") {
1052
+ exitReason = { type: "exit", code: code2 };
1137
1053
  } else if (signal) {
1138
1054
  exitReason = { type: "exit", code: SIGNAL_EXIT_BASE + (SIGNAL_NUMBERS2[signal] ?? 0) };
1139
1055
  } else {
@@ -1294,6 +1210,7 @@ var driveModeLoop = async (deps) => {
1294
1210
  var INPUT_QUEUE_CAP = 32;
1295
1211
  var MIN_MODE_DWELL_MS = 300;
1296
1212
  var RELAY_FALLBACK_POLL_MS = 6e4;
1213
+ var PRESENCE_HEARTBEAT_MS = 6e4;
1297
1214
  var SESSION_FLAG_RE = /^(-r|--resume|-c|--continue|--session-id)(=.*)?$/;
1298
1215
  var extractAssistantText = (message) => {
1299
1216
  const content = message.message?.content;
@@ -1378,28 +1295,53 @@ var runDualMode = async (binary, args2, startingMode) => {
1378
1295
  });
1379
1296
  relay.start();
1380
1297
  }
1381
- const announcePresence = (event, action) => {
1382
- if (!sessionId) return Promise.resolve();
1383
- return reportEvent(
1384
- { event, agentType: "claude_code", agentName, action, sessionId, machineId },
1385
- { maxAttempts: 2, timeoutMs: 4e3 }
1386
- ).then(() => {
1387
- }).catch(() => {
1298
+ let ownership;
1299
+ let ownershipSessionId;
1300
+ const ensureOwnership = async () => {
1301
+ if (!sessionId || ownershipSessionId === sessionId) return ownership?.ready;
1302
+ ownership?.stop();
1303
+ ownershipSessionId = sessionId;
1304
+ ownership = startProcessOwnership({
1305
+ sessionId,
1306
+ pid: process.pid,
1307
+ ...process.env.PUSHARY_SPAWN_ID ? { spawnId: process.env.PUSHARY_SPAWN_ID } : {}
1388
1308
  });
1309
+ await ownership.ready;
1310
+ };
1311
+ const announcePresence = async (event, action) => {
1312
+ if (!sessionId) return Promise.resolve();
1313
+ try {
1314
+ await ensureOwnership();
1315
+ await reportEvent(
1316
+ { event, agentType: "claude_code", agentName, action, sessionId, machineId, pid: process.pid },
1317
+ { maxAttempts: event === "session_start" ? 4 : 2, timeoutMs: 4e3, requireOk: event === "session_start" }
1318
+ );
1319
+ } catch {
1320
+ }
1389
1321
  };
1390
1322
  let stopTranscript;
1323
+ let presenceHeartbeat;
1391
1324
  let transcriptStarted = false;
1392
1325
  const ensureTranscriptSync = () => {
1393
1326
  if (transcriptStarted || !transcriptsEnabled() || !sessionId) return;
1394
1327
  transcriptStarted = true;
1395
1328
  stopTranscript = startTranscriptSync({ apiKey, sessionId, cwd: process.cwd(), machineId });
1396
1329
  };
1330
+ const ensurePresenceHeartbeat = () => {
1331
+ if (presenceHeartbeat || !sessionId) return;
1332
+ presenceHeartbeat = setInterval(
1333
+ () => void announcePresence("heartbeat", "Session online"),
1334
+ PRESENCE_HEARTBEAT_MS
1335
+ );
1336
+ presenceHeartbeat.unref();
1337
+ };
1397
1338
  const setSessionId = (id) => {
1398
1339
  const isNew = sessionId !== id;
1399
1340
  sessionId = id;
1400
1341
  sessionCreated = true;
1401
1342
  relay?.reannounce();
1402
1343
  if (isNew) void announcePresence("session_start", "Remote session ready \u2014 reachable from your phone");
1344
+ ensurePresenceHeartbeat();
1403
1345
  ensureTranscriptSync();
1404
1346
  };
1405
1347
  const onPhoneCommand = (command) => {
@@ -1483,6 +1425,7 @@ var runDualMode = async (binary, args2, startingMode) => {
1483
1425
  "[pushary] reachable from your phone \u2014 send an instruction from the app when this goes idle (Ctrl-] keeps the terminal).\n"
1484
1426
  );
1485
1427
  void announcePresence("session_start", "Session ready \u2014 reachable from your phone");
1428
+ ensurePresenceHeartbeat();
1486
1429
  ensureTranscriptSync();
1487
1430
  }
1488
1431
  if (startingMode === "remote" && initialPrompt) input.push(userMessage(initialPrompt));
@@ -1513,11 +1456,13 @@ var runDualMode = async (binary, args2, startingMode) => {
1513
1456
  process.removeListener("exit", restoreStdin);
1514
1457
  relay?.stop();
1515
1458
  poller.stop();
1459
+ if (presenceHeartbeat) clearInterval(presenceHeartbeat);
1516
1460
  stopTranscript?.();
1517
1461
  approver.teardown();
1518
1462
  input.close();
1519
1463
  stdin.dispose();
1520
1464
  await announcePresence("session_closed", "Session ended");
1465
+ ownership?.stop();
1521
1466
  if (totalCostUsd > 0) {
1522
1467
  process.stderr.write(`[pushary] session cost: $${totalCostUsd.toFixed(4)}
1523
1468
  `);
@@ -1551,10 +1496,15 @@ var runClaudeWrapper = async (args2) => {
1551
1496
 
1552
1497
  // bin/pushary-claude.ts
1553
1498
  var args = resolveClaudeArgs(process.argv);
1554
- runClaudeWrapper(args).then((code) => process.exit(code)).catch((err) => {
1499
+ var spawnId = process.env.PUSHARY_SPAWN_ID?.trim() || void 0;
1500
+ var code = await withSpawnProcessOwnership(spawnId, async () => {
1501
+ await ensureDaemonRunning();
1502
+ return runClaudeWrapper(args);
1503
+ }).catch((err) => {
1555
1504
  process.stderr.write(
1556
1505
  `[pushary] wrapper error: ${err instanceof Error ? err.message : String(err)}
1557
1506
  `
1558
1507
  );
1559
- process.exit(1);
1508
+ return 1;
1560
1509
  });
1510
+ process.exit(code);
@@ -4,14 +4,17 @@ import {
4
4
  shortenHome,
5
5
  unregisterPluginLocation,
6
6
  vscodeSettingsTargets
7
- } from "../chunk-UVAC2KHH.js";
7
+ } from "../chunk-UXWEE3QI.js";
8
8
  import {
9
9
  removeGeminiSettings
10
- } from "../chunk-TXF4QZSQ.js";
10
+ } from "../chunk-DOXSWT4Q.js";
11
11
  import {
12
12
  removeClaudeMcpServers,
13
13
  removePusharySettings
14
- } from "../chunk-NDC3TBHI.js";
14
+ } from "../chunk-UKSQWTBH.js";
15
+ import {
16
+ execNpm
17
+ } from "../chunk-VT4IVERX.js";
15
18
  import {
16
19
  claudeJson,
17
20
  claudeSettings,
@@ -29,25 +32,23 @@ import {
29
32
  pusharyDir,
30
33
  removeCodexHooks,
31
34
  vscodePluginDir
32
- } from "../chunk-EHLTWCDZ.js";
35
+ } from "../chunk-SEAHGHCE.js";
33
36
  import {
34
37
  rejectUnknownFlags
35
38
  } from "../chunk-GMXKITVA.js";
39
+ import {
40
+ exitOnHelpFlag
41
+ } from "../chunk-J4KFVOCQ.js";
42
+ import "../chunk-MGJBC6DD.js";
36
43
  import {
37
44
  removeClaudeAlias
38
45
  } from "../chunk-BC3VCZ3E.js";
46
+ import "../chunk-DINDL2CF.js";
39
47
  import "../chunk-7QLSKOSU.js";
40
- import "../chunk-YWRJGYUH.js";
41
- import {
42
- execNpm
43
- } from "../chunk-MUEW424A.js";
44
- import {
45
- exitOnHelpFlag
46
- } from "../chunk-RRZZ7TFJ.js";
47
- import "../chunk-ZUKH2NPJ.js";
48
48
  import {
49
49
  EXIT
50
50
  } from "../chunk-KZERVKTD.js";
51
+ import "../chunk-MQMPG5X4.js";
51
52
 
52
53
  // bin/pushary-clean.ts
53
54
  import { existsSync, readFileSync, copyFileSync, readdirSync } from "fs";
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node