@pushary/agent-hooks 0.83.1 → 0.84.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/CHANGELOG.md +75 -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 +72 -131
  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 +348 -0
  8. package/dist/bin/pushary-codex-hook.js +15 -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 +313 -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 +298 -0
  15. package/dist/bin/pushary-gemini-hook.js +24 -20
  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-FFT7L2VE.js → chunk-37AD5K7W.js} +11 -11
  37. package/dist/{chunk-BT7NTTIW.js → chunk-437T77TY.js} +3 -3
  38. package/dist/{chunk-7GTDHV7O.js → chunk-6LC4WSCL.js} +1 -1
  39. package/dist/{chunk-UAWTJMXA.js → chunk-7PZPJSWI.js} +1 -1
  40. package/dist/chunk-CCH775Y6.js +343 -0
  41. package/dist/chunk-DINDL2CF.js +45 -0
  42. package/dist/{chunk-DMKSHZKF.js → chunk-DOXSWT4Q.js} +1 -1
  43. package/dist/chunk-EVJ6ULHN.js +153 -0
  44. package/dist/{chunk-CXGJTN74.js → chunk-F52SIIDG.js} +4 -4
  45. package/dist/{chunk-3JDIGKPV.js → chunk-FBGBJXVC.js} +1 -1
  46. package/dist/{chunk-V6MCBCUC.js → chunk-HZNOAJGB.js} +2 -2
  47. package/dist/{chunk-RRZZ7TFJ.js → chunk-J4KFVOCQ.js} +4 -2
  48. package/dist/{chunk-RZP3Z2WR.js → chunk-K7CSRGKZ.js} +4 -4
  49. package/dist/{chunk-ZUKH2NPJ.js → chunk-MGJBC6DD.js} +1 -44
  50. package/dist/{chunk-GQLB2GBJ.js → chunk-MHBC2XUP.js} +1 -1
  51. package/dist/{chunk-QA5HP7CR.js → chunk-MQMPG5X4.js} +59 -3
  52. package/dist/{chunk-2R63XKLG.js → chunk-NJ7JT26G.js} +1 -1
  53. package/dist/chunk-NRGRUOAR.js +25 -0
  54. package/dist/chunk-NWYGW7HY.js +117 -0
  55. package/dist/{chunk-C4SQY777.js → chunk-OPMSMF5R.js} +42 -63
  56. package/dist/{chunk-E7YX7CTM.js → chunk-QNBA5OXQ.js} +81 -25
  57. package/dist/{chunk-O55DONTG.js → chunk-SEAHGHCE.js} +1 -1
  58. package/dist/{chunk-U752S5RE.js → chunk-UKSQWTBH.js} +1 -1
  59. package/dist/{chunk-HKKERYZP.js → chunk-UXWEE3QI.js} +1 -1
  60. package/dist/{chunk-MUEW424A.js → chunk-VT4IVERX.js} +50 -3
  61. package/dist/chunk-YJCMI5GF.js +133 -0
  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,80 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.84.0
4
+
5
+ ### The phone can start an agent on a machine that is not running one
6
+
7
+ The phone could reach a session that already existed. Starting one meant being at
8
+ the keyboard. `pushary setup` and `pushary claude` now bring up a small background
9
+ process, `pushary-daemon`, which watches for a launch requested from the phone and
10
+ starts a real session with the prompt you sent.
11
+
12
+ It is a poller over a durable queue, not an open socket. Idle, it asks the server
13
+ for work every 30 seconds and sends one heartbeat a minute, and it carries a
14
+ prompt, never any code. One request is in flight at a time, each bounded by its
15
+ own timeout, with a spawn rate cap so a queue backlog cannot fill a machine with
16
+ agents. Exactly one daemon runs per machine: a second one asks the first to hand
17
+ over rather than racing it, and it declines to take over from a daemon that is
18
+ still healthy.
19
+
20
+ This is a background process that was not running before. `pushary doctor` reports
21
+ whether it is up, and `pushary daemon` in a terminal shows you what it is doing.
22
+
23
+ A launch is now acknowledged rather than assumed. The daemon claims a request,
24
+ confirms the session it started, and the phone shows what actually happened,
25
+ including the failure when a launch does not come up.
26
+
27
+ ### Always allowing a tool no longer has to mean everywhere
28
+
29
+ "Approve and always allow" wrote one kind of rule: this tool, this whole
30
+ workspace, until you delete it. That is the right scope for `Read` and far too
31
+ wide for a push.
32
+
33
+ The approval now offers the scope alongside it. Workspace is the previous
34
+ behaviour. Repository binds the rule to the repository the ask came from, so a
35
+ rule written in one checkout stops governing another. Session binds it to the run
36
+ you are watching and goes away with it.
37
+
38
+ An older Pushary never sees the narrower rules. A CLI that does not ask for them
39
+ is sent workspace-wide rules only, so a rule you scoped to one session cannot be
40
+ quietly applied everywhere by something released before the idea existed.
41
+
42
+ ### AskUserQuestion reaches the phone with its descriptions
43
+
44
+ A Claude Code question carries a header, a description per option, and sometimes
45
+ permission to pick more than one. The phone received the bare labels. It now
46
+ receives the question the way the agent wrote it.
47
+
48
+ One question goes out as one card carrying the whole shape. Every surface that
49
+ does not know that shape, the web decision page and Slack among them, still sees
50
+ an ordinary choice list and answers with a label, which the server reads back onto
51
+ the question. Two or more questions go out one at a time, which is what every
52
+ surface has always been able to answer: a bare label cannot say which of several
53
+ questions it belongs to, and a half-filled answer is worse for the agent than
54
+ being asked again in the terminal.
55
+
56
+ ### Codex and Gemini sessions started from the phone stay live
57
+
58
+ A phone-started Codex or Gemini run now goes through a bridge that survives the
59
+ connection dropping and resumes the same session instead of opening a new one.
60
+ The machine advertises only what it can actually do, so a Gemini CLI too old for
61
+ the bridge is not offered as one, and the phone stops showing controls that would
62
+ fail.
63
+
64
+ Stopping is acknowledged the same way. A stop request names the session and the
65
+ process it belongs to, and the machine proves it owns that process before acting
66
+ on it, so a recycled pid cannot be mistaken for the agent you asked to stop.
67
+
68
+
69
+ ## 0.83.0 to 0.83.2
70
+
71
+ Three releases went out without an entry here. In order: `ask_user` became
72
+ idempotent, so a create that gets retried after a timeout resolves to the question
73
+ already asked instead of buzzing a phone twice. The pairing QR stopped encoding a
74
+ code only a browser could resolve. And the gate's two shared pieces, the verdict a
75
+ policy produces and the "most specific rule wins" precedence, moved into the
76
+ package the CLI and the server both read, with no change to what either decides.
77
+
3
78
  ## 0.82.0
4
79
 
5
80
  ### 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-UAWTJMXA.js";
5
- import "../chunk-RZP3Z2WR.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-BT7NTTIW.js";
9
- import "../chunk-QA5HP7CR.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-UAWTJMXA.js";
8
+ } from "../chunk-7PZPJSWI.js";
9
9
  import {
10
10
  claudeSettings
11
- } from "../chunk-O55DONTG.js";
11
+ } from "../chunk-SEAHGHCE.js";
12
12
  import {
13
13
  createIo
14
14
  } from "../chunk-5RUIFDTP.js";
15
- import "../chunk-RZP3Z2WR.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-BT7NTTIW.js";
25
- import "../chunk-7QLSKOSU.js";
26
- import "../chunk-QA5HP7CR.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,29 @@
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-NWYGW7HY.js";
13
+ import {
14
+ startProcessOwnership
15
+ } from "../chunk-EVJ6ULHN.js";
16
+ import {
17
+ needsShell,
18
+ spawnClaude
19
+ } from "../chunk-XHKBHWLX.js";
20
+ import "../chunk-VT4IVERX.js";
21
+ import "../chunk-SEAHGHCE.js";
2
22
  import {
3
23
  KILL_REASON,
4
24
  isDeferAnswer,
5
25
  resolveGate
6
- } from "../chunk-FFT7L2VE.js";
26
+ } from "../chunk-37AD5K7W.js";
7
27
  import {
8
28
  askUser,
9
29
  cancelQuestion,
@@ -15,26 +35,22 @@ import {
15
35
  reportEvent,
16
36
  scopePathFor,
17
37
  waitForAnswer
18
- } from "../chunk-C4SQY777.js";
19
- import "../chunk-BSZYIAZL.js";
20
- import "../chunk-SAF6HGAA.js";
21
- import "../chunk-BT7NTTIW.js";
22
- import "../chunk-7QLSKOSU.js";
23
- import {
24
- redactSecrets
25
- } from "../chunk-QA5HP7CR.js";
26
- import {
27
- needsShell,
28
- spawnClaude
29
- } from "../chunk-XHKBHWLX.js";
38
+ } from "../chunk-OPMSMF5R.js";
30
39
  import {
31
40
  getMachineId
32
41
  } from "../chunk-RN3NOEJF.js";
42
+ import "../chunk-DINDL2CF.js";
43
+ import "../chunk-6MTNS63X.js";
44
+ import "../chunk-BSZYIAZL.js";
45
+ import "../chunk-SAF6HGAA.js";
46
+ import "../chunk-437T77TY.js";
47
+ import "../chunk-7QLSKOSU.js";
48
+ import "../chunk-KZERVKTD.js";
33
49
  import {
34
50
  getApiKey,
35
51
  getBaseUrl
36
52
  } from "../chunk-2UMNXADU.js";
37
- import "../chunk-KZERVKTD.js";
53
+ import "../chunk-MQMPG5X4.js";
38
54
 
39
55
  // src/wrapper/claudeBinary.ts
40
56
  import { statSync } from "fs";
@@ -113,112 +129,6 @@ import { basename } from "path";
113
129
  import { homedir } from "os";
114
130
  import { join as join2, resolve } from "path";
115
131
  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
132
  var transcriptsEnabled = () => process.env.PUSHARY_TRANSCRIPTS === "1";
223
133
  var claudeTranscriptPath = (cwd, sessionId) => {
224
134
  const projectId = resolve(cwd).replace(/[^a-zA-Z0-9-]/g, "-");
@@ -533,7 +443,7 @@ var createRemoteApprover = (deps) => {
533
443
  try {
534
444
  const sessionId = deps.getSessionId?.();
535
445
  const modeState = await fetchModeState2(deps.apiKey, sessionId);
536
- const policyConfig = await getPolicy2(deps.apiKey, modeState.policyVersion, "claude_code", true);
446
+ const policyConfig = await getPolicy2(deps.apiKey, modeState.policyVersion, "claude_code", true, sessionId);
537
447
  const scopePath = modeState.scope ? scopePathFor(toolName, input, deps.cwd) : void 0;
538
448
  const verdict = resolveGate({
539
449
  modeState,
@@ -542,7 +452,8 @@ var createRemoteApprover = (deps) => {
542
452
  toolInputs: [input],
543
453
  scopePaths: scopePath ? [scopePath] : [],
544
454
  cwd: deps.cwd,
545
- repoKey: repoKeyFor(deps.cwd)
455
+ repoKey: repoKeyFor(deps.cwd),
456
+ sessionId
546
457
  });
547
458
  if (verdict.kind === "kill") return deny(KILL_REASON);
548
459
  if (verdict.kind === "allow") return allow(input);
@@ -561,7 +472,8 @@ var createRemoteApprover = (deps) => {
561
472
  sessionId,
562
473
  machineId: deps.machineId,
563
474
  toolName,
564
- toolTarget
475
+ toolTarget,
476
+ repoKey: repoKeyFor(deps.cwd)
565
477
  });
566
478
  } catch {
567
479
  return applyTimeout(policy, input);
@@ -1294,6 +1206,7 @@ var driveModeLoop = async (deps) => {
1294
1206
  var INPUT_QUEUE_CAP = 32;
1295
1207
  var MIN_MODE_DWELL_MS = 300;
1296
1208
  var RELAY_FALLBACK_POLL_MS = 6e4;
1209
+ var PRESENCE_HEARTBEAT_MS = 6e4;
1297
1210
  var SESSION_FLAG_RE = /^(-r|--resume|-c|--continue|--session-id)(=.*)?$/;
1298
1211
  var extractAssistantText = (message) => {
1299
1212
  const content = message.message?.content;
@@ -1378,28 +1291,53 @@ var runDualMode = async (binary, args2, startingMode) => {
1378
1291
  });
1379
1292
  relay.start();
1380
1293
  }
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(() => {
1294
+ let ownership;
1295
+ let ownershipSessionId;
1296
+ const ensureOwnership = async () => {
1297
+ if (!sessionId || ownershipSessionId === sessionId) return ownership?.ready;
1298
+ ownership?.stop();
1299
+ ownershipSessionId = sessionId;
1300
+ ownership = startProcessOwnership({
1301
+ sessionId,
1302
+ pid: process.pid,
1303
+ ...process.env.PUSHARY_SPAWN_ID ? { spawnId: process.env.PUSHARY_SPAWN_ID } : {}
1388
1304
  });
1305
+ await ownership.ready;
1306
+ };
1307
+ const announcePresence = async (event, action) => {
1308
+ if (!sessionId) return Promise.resolve();
1309
+ try {
1310
+ await ensureOwnership();
1311
+ await reportEvent(
1312
+ { event, agentType: "claude_code", agentName, action, sessionId, machineId, pid: process.pid },
1313
+ { maxAttempts: event === "session_start" ? 4 : 2, timeoutMs: 4e3, requireOk: event === "session_start" }
1314
+ );
1315
+ } catch {
1316
+ }
1389
1317
  };
1390
1318
  let stopTranscript;
1319
+ let presenceHeartbeat;
1391
1320
  let transcriptStarted = false;
1392
1321
  const ensureTranscriptSync = () => {
1393
1322
  if (transcriptStarted || !transcriptsEnabled() || !sessionId) return;
1394
1323
  transcriptStarted = true;
1395
1324
  stopTranscript = startTranscriptSync({ apiKey, sessionId, cwd: process.cwd(), machineId });
1396
1325
  };
1326
+ const ensurePresenceHeartbeat = () => {
1327
+ if (presenceHeartbeat || !sessionId) return;
1328
+ presenceHeartbeat = setInterval(
1329
+ () => void announcePresence("heartbeat", "Session online"),
1330
+ PRESENCE_HEARTBEAT_MS
1331
+ );
1332
+ presenceHeartbeat.unref();
1333
+ };
1397
1334
  const setSessionId = (id) => {
1398
1335
  const isNew = sessionId !== id;
1399
1336
  sessionId = id;
1400
1337
  sessionCreated = true;
1401
1338
  relay?.reannounce();
1402
1339
  if (isNew) void announcePresence("session_start", "Remote session ready \u2014 reachable from your phone");
1340
+ ensurePresenceHeartbeat();
1403
1341
  ensureTranscriptSync();
1404
1342
  };
1405
1343
  const onPhoneCommand = (command) => {
@@ -1483,6 +1421,7 @@ var runDualMode = async (binary, args2, startingMode) => {
1483
1421
  "[pushary] reachable from your phone \u2014 send an instruction from the app when this goes idle (Ctrl-] keeps the terminal).\n"
1484
1422
  );
1485
1423
  void announcePresence("session_start", "Session ready \u2014 reachable from your phone");
1424
+ ensurePresenceHeartbeat();
1486
1425
  ensureTranscriptSync();
1487
1426
  }
1488
1427
  if (startingMode === "remote" && initialPrompt) input.push(userMessage(initialPrompt));
@@ -1513,11 +1452,13 @@ var runDualMode = async (binary, args2, startingMode) => {
1513
1452
  process.removeListener("exit", restoreStdin);
1514
1453
  relay?.stop();
1515
1454
  poller.stop();
1455
+ if (presenceHeartbeat) clearInterval(presenceHeartbeat);
1516
1456
  stopTranscript?.();
1517
1457
  approver.teardown();
1518
1458
  input.close();
1519
1459
  stdin.dispose();
1520
1460
  await announcePresence("session_closed", "Session ended");
1461
+ ownership?.stop();
1521
1462
  if (totalCostUsd > 0) {
1522
1463
  process.stderr.write(`[pushary] session cost: $${totalCostUsd.toFixed(4)}
1523
1464
  `);
@@ -1551,7 +1492,7 @@ var runClaudeWrapper = async (args2) => {
1551
1492
 
1552
1493
  // bin/pushary-claude.ts
1553
1494
  var args = resolveClaudeArgs(process.argv);
1554
- runClaudeWrapper(args).then((code) => process.exit(code)).catch((err) => {
1495
+ ensureDaemonRunning().then(() => runClaudeWrapper(args)).then((code) => process.exit(code)).catch((err) => {
1555
1496
  process.stderr.write(
1556
1497
  `[pushary] wrapper error: ${err instanceof Error ? err.message : String(err)}
1557
1498
  `
@@ -4,14 +4,17 @@ import {
4
4
  shortenHome,
5
5
  unregisterPluginLocation,
6
6
  vscodeSettingsTargets
7
- } from "../chunk-HKKERYZP.js";
7
+ } from "../chunk-UXWEE3QI.js";
8
8
  import {
9
9
  removeGeminiSettings
10
- } from "../chunk-DMKSHZKF.js";
10
+ } from "../chunk-DOXSWT4Q.js";
11
11
  import {
12
12
  removeClaudeMcpServers,
13
13
  removePusharySettings
14
- } from "../chunk-U752S5RE.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-O55DONTG.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-QA5HP7CR.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