@pushary/agent-hooks 0.89.13 → 0.90.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 (78) hide show
  1. package/CHANGELOG.md +73 -0
  2. package/data/SKILL.md +1 -1
  3. package/data/cursor-plugin/skills/pushary/SKILL.md +1 -1
  4. package/data/vscode-plugin/skills/pushary/SKILL.md +1 -1
  5. package/dist/bin/pushary-bell-hook.js +4 -4
  6. package/dist/bin/pushary-bell.js +8 -8
  7. package/dist/bin/pushary-claude.js +72 -21
  8. package/dist/bin/pushary-clean.js +16 -42
  9. package/dist/bin/pushary-codex-bridge.js +37 -12
  10. package/dist/bin/pushary-codex-hook.js +45 -12
  11. package/dist/bin/pushary-codex.js +7 -7
  12. package/dist/bin/pushary-connect.js +11 -10
  13. package/dist/bin/pushary-daemon.js +187 -63
  14. package/dist/bin/pushary-disconnect.js +8 -8
  15. package/dist/bin/pushary-doctor.js +108 -25
  16. package/dist/bin/pushary-elicitation-hook.js +4 -4
  17. package/dist/bin/pushary-gemini-bridge.js +33 -11
  18. package/dist/bin/pushary-gemini-hook.js +21 -10
  19. package/dist/bin/pushary-hook.js +13 -13
  20. package/dist/bin/pushary-login.js +18 -7
  21. package/dist/bin/pushary-logout.js +6 -6
  22. package/dist/bin/pushary-mode.js +28 -21
  23. package/dist/bin/pushary-notification-hook.js +7 -7
  24. package/dist/bin/pushary-opencode-hook.js +8 -8
  25. package/dist/bin/pushary-permission-denied-hook.js +11 -11
  26. package/dist/bin/pushary-permission-hook.js +11 -11
  27. package/dist/bin/pushary-post-hook.js +14 -8
  28. package/dist/bin/pushary-prompt-hook.js +7 -7
  29. package/dist/bin/pushary-session-end-hook.js +7 -7
  30. package/dist/bin/pushary-session-start-hook.js +7 -7
  31. package/dist/bin/pushary-setup.js +168 -45
  32. package/dist/bin/pushary-stats.js +4 -4
  33. package/dist/bin/pushary-status.js +11 -10
  34. package/dist/bin/pushary-stop-hook.js +14 -8
  35. package/dist/bin/pushary-stopfailure-hook.js +7 -7
  36. package/dist/bin/pushary-suggestions.js +2 -2
  37. package/dist/bin/pushary-transcripts.js +6 -6
  38. package/dist/bin/pushary-upgrade.js +15 -15
  39. package/dist/bin/pushary-wait.js +5 -5
  40. package/dist/bin/pushary.js +1 -1
  41. package/dist/{chunk-B2HND22S.js → chunk-2LAHXHBR.js} +1 -1
  42. package/dist/{chunk-GMJDQDXU.js → chunk-43GMYSS4.js} +1 -0
  43. package/dist/{chunk-OX35BSPV.js → chunk-4LOPSGLP.js} +70 -11
  44. package/dist/{chunk-JTBDBKHU.js → chunk-54ELC7D7.js} +1 -1
  45. package/dist/{chunk-C56VHSXU.js → chunk-5NAZ7SFS.js} +1 -1
  46. package/dist/{chunk-4IPSMEGT.js → chunk-7FE2KABY.js} +12 -2
  47. package/dist/{chunk-RAEPAUNJ.js → chunk-7TPQEWUZ.js} +1 -1
  48. package/dist/{chunk-ZO43JTT3.js → chunk-AJYV3VGU.js} +1 -1
  49. package/dist/{chunk-QFJZHVVL.js → chunk-AZPIQG4G.js} +35 -1
  50. package/dist/{chunk-5MNTWAAJ.js → chunk-B6H2KAQC.js} +29 -22
  51. package/dist/{chunk-MNTZKFZI.js → chunk-BQBEH6TV.js} +5 -5
  52. package/dist/{chunk-WHDSJ6PD.js → chunk-BVBEURWJ.js} +1 -1
  53. package/dist/{chunk-ZWEKONU6.js → chunk-CELX75MW.js} +1 -1
  54. package/dist/{chunk-ZP3SNUP7.js → chunk-CNPAT3HL.js} +3 -2
  55. package/dist/{chunk-YXT55PYF.js → chunk-DYP6SMOR.js} +1 -1
  56. package/dist/chunk-DZDI62LY.js +145 -0
  57. package/dist/{chunk-VF3RTVWY.js → chunk-EZIEXHYM.js} +15 -7
  58. package/dist/{chunk-7JZCADBR.js → chunk-F2ICEWZL.js} +101 -19
  59. package/dist/{chunk-JHJHCMAF.js → chunk-F7OURW3C.js} +24 -6
  60. package/dist/{chunk-VASZ6TWW.js → chunk-FQL5E6NG.js} +2 -2
  61. package/dist/{chunk-JVLB5CAJ.js → chunk-GL3FFM7A.js} +7 -4
  62. package/dist/{chunk-5YEF55AX.js → chunk-H2QZRQR2.js} +1 -1
  63. package/dist/{chunk-S4BLNILH.js → chunk-I463AEG3.js} +1 -1
  64. package/dist/{chunk-ANMHIZZY.js → chunk-JW4KO5AF.js} +1 -1
  65. package/dist/{chunk-3YGGEDJK.js → chunk-KUZWXPUS.js} +3 -2
  66. package/dist/{chunk-N4QJ3CLX.js → chunk-L4E2BIJ2.js} +80 -7
  67. package/dist/{chunk-NBDU3R3G.js → chunk-OSALSIUF.js} +1 -1
  68. package/dist/{chunk-KNLKHJZR.js → chunk-PYA4FLKI.js} +32 -21
  69. package/dist/chunk-QCGNGEGE.js +15 -0
  70. package/dist/chunk-S6LYP7QA.js +127 -0
  71. package/dist/{chunk-M3UABYRT.js → chunk-UZNKVRLE.js} +1 -1
  72. package/dist/{chunk-FOGW6O2Y.js → chunk-VHWCVD42.js} +1 -1
  73. package/dist/{chunk-QINYXLA5.js → chunk-VNMKIJZB.js} +15 -2
  74. package/dist/{chunk-CBGGUEKE.js → chunk-Y6BWPERV.js} +90 -7
  75. package/dist/src/index.d.ts +3 -0
  76. package/dist/src/index.js +11 -11
  77. package/package.json +1 -1
  78. package/dist/chunk-MSTHT2M7.js +0 -72
package/CHANGELOG.md CHANGED
@@ -1,5 +1,78 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.90.1
4
+
5
+ ### Daemon logs distinguish health from failure
6
+
7
+ Successful daemon startup and phone-requested launches now go to the normal log,
8
+ leaving the error log for failures that need attention.
9
+
10
+ ### Command-store tests never touch live agent state
11
+
12
+ The pending command store accepts an explicit directory override. Tests use a
13
+ fresh temporary store, so parallel and sandboxed runs cannot read, overwrite, or
14
+ prune commands belonging to a real agent session.
15
+
16
+ ## 0.90.0
17
+
18
+ ### A hook that needs an answer is allowed to wait for one
19
+
20
+ Claude Code hooks are now registered at the 600 second budget Claude itself
21
+ allows, so a policy that waits two minutes for a phone approval is no longer cut
22
+ off at 120 seconds and turned into a denial the user never saw. Anything a hook
23
+ does that is not a decision keeps its short timeout.
24
+
25
+ A phone card is now withdrawn when the tool it guarded completes or the agent's
26
+ own client denies it, so an approval cannot arrive for something that already
27
+ finished.
28
+
29
+ ### The prompts that never reached the phone
30
+
31
+ Codex permission requests are no longer filtered by a shell matcher, so a browser
32
+ use request, a managed network request and an MCP tool request all reach the
33
+ phone the same way a shell escalation does. Codex in bypass mode now decides
34
+ before the tool runs rather than after it, which is what push-first has always
35
+ meant everywhere else.
36
+
37
+ Gemini reports its turn boundary through AfterAgent and its teardown through
38
+ SessionEnd, so a finished Gemini turn is a finished turn rather than a session
39
+ that stayed open.
40
+
41
+ A finished subagent now ends its own session instead of ending its parent's, and
42
+ appears under the session that started it. Context compaction is reported as
43
+ itself rather than as a tool that ran.
44
+
45
+ Pushary's own tools are never gated by Pushary, on the server exactly as in the
46
+ hook, so asking the user a question can no longer wait on approval to ask it.
47
+
48
+ ### Sessions end, and a daemon that cannot log in says so
49
+
50
+ A session that goes quiet is closed rather than left standing forever, and the
51
+ daemon reaps the sessions it started. `pushary daemon status` lists what it is
52
+ holding, how old each one is, and how much memory it is using.
53
+
54
+ A daemon whose key is rejected now records that and stops, instead of restarting
55
+ into the same rejection until something notices. `pushary doctor` says the daemon
56
+ needs a login, and logging in clears it.
57
+
58
+ ### Setup and doctor tell the truth about this machine
59
+
60
+ `pushary doctor` names the other tools holding the same agent hooks and how long
61
+ each of them waits, checks that the bridge the Mac app installed can be found,
62
+ and reports which Node the hooks will actually run under.
63
+
64
+ `pushary setup` gained `--verify none` for an unattended install, prints what it
65
+ would also do during a dry run, offers a way out of pairing, and can take over
66
+ hooks another tool is holding with `--take-over-hooks`. `pushary mode` now sets a
67
+ site's mode permanently unless told how long to keep it.
68
+
69
+ ### Machine identity and clean
70
+
71
+ `pushary clean` leaves the machine identity shared with the Mac app intact, keeps
72
+ one entry rather than two, and treats the identity file as the CLI's to own and
73
+ the app's to read. Transcript sync seals its recovery ledger and admits a gap
74
+ rather than syncing bookkeeping around it.
75
+
3
76
  ## 0.89.13
4
77
 
5
78
  ### Synced transcripts survive large turns and restarts safely
package/data/SKILL.md CHANGED
@@ -348,7 +348,7 @@ else:
348
348
 
349
349
  If the user answers in chat before the push response arrives, call `cancel_question` before acting. If it returns `handoffAction: "stop"`, stop. Otherwise, if it returns false, poll once for 1 second and honor any phone answer that won the race.
350
350
 
351
- **A note on how long ask_user blocks:** the wait time and whether it blocks at all are governed by the site's delivery mode, which the user configures (you do not set it). In the default smart mode and push-only mode, ask_user blocks for the policy timeout; in notify-only mode it returns immediately with `answered: false` after sending the push. Always check `answered` rather than assuming the call blocked, and pass `timeoutMs` only when you need a shorter wait than the site policy.
351
+ **A note on how long ask_user blocks:** the wait time and whether it blocks at all are governed by the site's delivery mode, which the user configures (you do not set it). The four modes are "When I'm out" (`push_first`, the default), "Every time" (`push_only`), "Updates" (`notify_only`) and "Terminal" (`terminal_only`). In When I'm out, ask_user blocks for the push-first window (45 seconds by default) and the phone is only asked when the user is away from their terminal or their Mac; in Every time it blocks for the policy timeout and the phone is always asked; in Updates it returns immediately with `answered: false` after telling the phone, because the decision belongs in the current client; in Terminal nothing reaches the phone and it also returns immediately with `answered: false`. Always check `answered` rather than assuming the call blocked, and pass `timeoutMs` only when you need a shorter wait than the site policy.
352
352
 
353
353
  ## Identifying Your Agent
354
354
 
@@ -341,7 +341,7 @@ else:
341
341
 
342
342
  If the user answers in chat before the push response arrives, call `cancel_question` before acting. If it returns `handoffAction: "stop"`, stop. Otherwise, if it returns false, poll once for 1 second and honor any phone answer that won the race.
343
343
 
344
- **A note on how long ask_user blocks:** the wait time and whether it blocks at all are governed by the site's delivery mode, which the user configures (you do not set it). In the default smart mode and push-only mode, ask_user blocks for the policy timeout; in notify-only mode it returns immediately with `answered: false` after sending the push. Always check `answered` rather than assuming the call blocked, and pass `timeoutMs` only when you need a shorter wait than the site policy.
344
+ **A note on how long ask_user blocks:** the wait time and whether it blocks at all are governed by the site's delivery mode, which the user configures (you do not set it). The four modes are "When I'm out" (`push_first`, the default), "Every time" (`push_only`), "Updates" (`notify_only`) and "Terminal" (`terminal_only`). In When I'm out, ask_user blocks for the push-first window (45 seconds by default) and the phone is only asked when the user is away from their terminal or their Mac; in Every time it blocks for the policy timeout and the phone is always asked; in Updates it returns immediately with `answered: false` after telling the phone, because the decision belongs in the current client; in Terminal nothing reaches the phone and it also returns immediately with `answered: false`. Always check `answered` rather than assuming the call blocked, and pass `timeoutMs` only when you need a shorter wait than the site policy.
345
345
 
346
346
  ## Identifying Your Agent
347
347
 
@@ -341,7 +341,7 @@ else:
341
341
 
342
342
  If the user answers in chat before the push response arrives, call `cancel_question` before acting. If it returns `handoffAction: "stop"`, stop. Otherwise, if it returns false, poll once for 1 second and honor any phone answer that won the race.
343
343
 
344
- **A note on how long ask_user blocks:** the wait time and whether it blocks at all are governed by the site's delivery mode, which the user configures (you do not set it). In the default smart mode and push-only mode, ask_user blocks for the policy timeout; in notify-only mode it returns immediately with `answered: false` after sending the push. Always check `answered` rather than assuming the call blocked, and pass `timeoutMs` only when you need a shorter wait than the site policy.
344
+ **A note on how long ask_user blocks:** the wait time and whether it blocks at all are governed by the site's delivery mode, which the user configures (you do not set it). The four modes are "When I'm out" (`push_first`, the default), "Every time" (`push_only`), "Updates" (`notify_only`) and "Terminal" (`terminal_only`). In When I'm out, ask_user blocks for the push-first window (45 seconds by default) and the phone is only asked when the user is away from their terminal or their Mac; in Every time it blocks for the policy timeout and the phone is always asked; in Updates it returns immediately with `answered: false` after telling the phone, because the decision belongs in the current client; in Terminal nothing reaches the phone and it also returns immediately with `answered: false`. Always check `answered` rather than assuming the call blocked, and pass `timeoutMs` only when you need a shorter wait than the site policy.
345
345
 
346
346
  ## Identifying Your Agent
347
347
 
@@ -1,16 +1,16 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  handleBell
4
- } from "../chunk-C56VHSXU.js";
4
+ } from "../chunk-5NAZ7SFS.js";
5
5
  import {
6
6
  readHookInput
7
7
  } from "../chunk-6CUUA7HO.js";
8
- import "../chunk-NBDU3R3G.js";
8
+ import "../chunk-OSALSIUF.js";
9
9
  import "../chunk-BC3VCZ3E.js";
10
- import "../chunk-ANMHIZZY.js";
10
+ import "../chunk-JW4KO5AF.js";
11
11
  import "../chunk-4LL6LG5X.js";
12
12
  import "../chunk-2UMNXADU.js";
13
- import "../chunk-QINYXLA5.js";
13
+ import "../chunk-VNMKIJZB.js";
14
14
 
15
15
  // bin/pushary-bell-hook.ts
16
16
  var main = async () => {
@@ -5,14 +5,14 @@ import {
5
5
  liveAgentCount,
6
6
  ring,
7
7
  upgradeThreshold
8
- } from "../chunk-C56VHSXU.js";
8
+ } from "../chunk-5NAZ7SFS.js";
9
9
  import {
10
10
  guardBinary
11
- } from "../chunk-VF3RTVWY.js";
11
+ } from "../chunk-EZIEXHYM.js";
12
12
  import "../chunk-L4ZCFMWW.js";
13
13
  import {
14
14
  claudeSettings
15
- } from "../chunk-3YGGEDJK.js";
15
+ } from "../chunk-KUZWXPUS.js";
16
16
  import {
17
17
  createIo
18
18
  } from "../chunk-5RUIFDTP.js";
@@ -21,22 +21,22 @@ import {
21
21
  } from "../chunk-GMXKITVA.js";
22
22
  import {
23
23
  exitOnHelpFlag
24
- } from "../chunk-WHDSJ6PD.js";
25
- import "../chunk-GMJDQDXU.js";
24
+ } from "../chunk-BVBEURWJ.js";
25
+ import "../chunk-43GMYSS4.js";
26
26
  import "../chunk-DINDL2CF.js";
27
- import "../chunk-NBDU3R3G.js";
27
+ import "../chunk-OSALSIUF.js";
28
28
  import "../chunk-BC3VCZ3E.js";
29
29
  import "../chunk-7QLSKOSU.js";
30
30
  import {
31
31
  EXIT
32
32
  } from "../chunk-KZERVKTD.js";
33
- import "../chunk-ANMHIZZY.js";
33
+ import "../chunk-JW4KO5AF.js";
34
34
  import {
35
35
  readJsonSafe,
36
36
  writeJsonAtomic
37
37
  } from "../chunk-4LL6LG5X.js";
38
38
  import "../chunk-2UMNXADU.js";
39
- import "../chunk-QINYXLA5.js";
39
+ import "../chunk-VNMKIJZB.js";
40
40
 
41
41
  // src/bell/install.ts
42
42
  import { join } from "path";
@@ -7,24 +7,26 @@ import {
7
7
  } from "../chunk-YKLCWWEI.js";
8
8
  import {
9
9
  ensureDaemonRunning
10
- } from "../chunk-7JZCADBR.js";
10
+ } from "../chunk-F2ICEWZL.js";
11
11
  import {
12
12
  buildTranscriptRecords,
13
13
  decodePublicKey,
14
14
  generateSessionKey,
15
15
  wrapSessionKey
16
- } from "../chunk-4IPSMEGT.js";
16
+ } from "../chunk-7FE2KABY.js";
17
17
  import {
18
18
  fetchTranscriptRecipients,
19
19
  uploadTranscriptRecords
20
20
  } from "../chunk-QZCFVWA7.js";
21
21
  import {
22
22
  transcriptsEnabled
23
- } from "../chunk-M3UABYRT.js";
23
+ } from "../chunk-UZNKVRLE.js";
24
24
  import {
25
+ SESSION_IDLE_TIMEOUT_MS,
26
+ describeIdleWindow,
25
27
  startProcessOwnership,
26
28
  withSpawnProcessOwnership
27
- } from "../chunk-CBGGUEKE.js";
29
+ } from "../chunk-Y6BWPERV.js";
28
30
  import {
29
31
  needsShell,
30
32
  spawnClaude
@@ -36,9 +38,10 @@ import {
36
38
  import {
37
39
  fetchModeState,
38
40
  getPolicy,
41
+ phoneDrivenEnv,
39
42
  repoKeyFor,
40
43
  reportEvent
41
- } from "../chunk-OX35BSPV.js";
44
+ } from "../chunk-4LOPSGLP.js";
42
45
  import {
43
46
  DEFAULT_SESSION,
44
47
  askUser,
@@ -50,25 +53,25 @@ import {
50
53
  removePendingCommand,
51
54
  savePendingCommand,
52
55
  waitForAnswer
53
- } from "../chunk-JHJHCMAF.js";
56
+ } from "../chunk-F7OURW3C.js";
54
57
  import {
55
58
  UNNAMED_ACTION,
56
59
  deriveToolTarget,
57
60
  describeToolCall,
58
61
  scopePathFor
59
- } from "../chunk-N4QJ3CLX.js";
62
+ } from "../chunk-L4E2BIJ2.js";
60
63
  import {
61
64
  getMachineId
62
- } from "../chunk-JTBDBKHU.js";
63
- import "../chunk-3YGGEDJK.js";
65
+ } from "../chunk-54ELC7D7.js";
66
+ import "../chunk-KUZWXPUS.js";
64
67
  import "../chunk-DINDL2CF.js";
65
- import "../chunk-NBDU3R3G.js";
68
+ import "../chunk-OSALSIUF.js";
66
69
  import "../chunk-BC3VCZ3E.js";
67
70
  import "../chunk-BSZYIAZL.js";
68
71
  import "../chunk-SAF6HGAA.js";
69
72
  import "../chunk-7QLSKOSU.js";
70
73
  import "../chunk-KZERVKTD.js";
71
- import "../chunk-ANMHIZZY.js";
74
+ import "../chunk-JW4KO5AF.js";
72
75
  import "../chunk-4LL6LG5X.js";
73
76
  import {
74
77
  getApiKey,
@@ -77,7 +80,7 @@ import {
77
80
  import {
78
81
  KILL_REASON,
79
82
  resolveGate
80
- } from "../chunk-QINYXLA5.js";
83
+ } from "../chunk-VNMKIJZB.js";
81
84
 
82
85
  // src/wrapper/claudeBinary.ts
83
86
  import { statSync } from "fs";
@@ -1367,14 +1370,30 @@ var runLocalLeg = (session, deps = {}) => {
1367
1370
 
1368
1371
  // src/wrapper/remoteLoop.ts
1369
1372
  var errMsg = (err) => err instanceof Error ? err.message : String(err);
1373
+ var scheduleWithTimer = (fire, delayMs) => {
1374
+ const timer = setTimeout(fire, delayMs);
1375
+ timer.unref?.();
1376
+ return () => clearTimeout(timer);
1377
+ };
1378
+ var isTurnActivity = (message) => message.type !== "system" && message.type !== "result";
1379
+ var isInit = (message) => message.type === "system" && message.subtype === "init";
1370
1380
  var DEFAULT_MAX_RESTARTS = 5;
1371
1381
  var runRemoteLoop = async (deps) => {
1372
1382
  const maxRestarts = deps.maxRestarts ?? DEFAULT_MAX_RESTARTS;
1373
1383
  const sleep = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
1374
1384
  const backoff = deps.backoffMs ?? ((attempt) => Math.min(1e3 * 2 ** attempt, 15e3));
1385
+ const idleTimeoutMs = deps.idleTimeoutMs ?? SESSION_IDLE_TIMEOUT_MS;
1386
+ const scheduleIdle = deps.scheduleIdle ?? scheduleWithTimer;
1375
1387
  let resumeId = deps.initialResume;
1376
1388
  let totalCost = 0;
1377
1389
  let restarts = 0;
1390
+ let idleExpired = false;
1391
+ let cancelIdle;
1392
+ const disarmIdle = () => {
1393
+ cancelIdle?.();
1394
+ cancelIdle = void 0;
1395
+ };
1396
+ const ended = (exitCode, endedBy) => ({ exitCode, totalCostUsd: totalCost, endedBy });
1378
1397
  while (!deps.signal.aborted) {
1379
1398
  deps.input.detachConsumer();
1380
1399
  let query;
@@ -1392,7 +1411,7 @@ var runRemoteLoop = async (deps) => {
1392
1411
  });
1393
1412
  } catch (err) {
1394
1413
  deps.log?.(`[pushary] remote mode: query failed to start: ${errMsg(err)}`);
1395
- return { exitCode: 1, totalCostUsd: totalCost };
1414
+ return ended(1, "failed");
1396
1415
  }
1397
1416
  const onAbort = () => {
1398
1417
  deps.input.close();
@@ -1401,10 +1420,24 @@ var runRemoteLoop = async (deps) => {
1401
1420
  };
1402
1421
  if (deps.signal.aborted) onAbort();
1403
1422
  else deps.signal.addEventListener("abort", onAbort, { once: true });
1423
+ let turnOpen = false;
1424
+ const stillResting = (deliveredAtRest) => !turnOpen && deps.input.bufferedCount === 0 && deps.input.stats.delivered === deliveredAtRest;
1425
+ const armIdle = () => {
1426
+ disarmIdle();
1427
+ const deliveredAtRest = deps.input.stats.delivered;
1428
+ cancelIdle = scheduleIdle(() => {
1429
+ cancelIdle = void 0;
1430
+ if (!stillResting(deliveredAtRest)) return;
1431
+ idleExpired = true;
1432
+ deps.log?.(`[pushary] remote mode: no phone instruction for ${describeIdleWindow(idleTimeoutMs)}, ending the session`);
1433
+ onAbort();
1434
+ }, idleTimeoutMs);
1435
+ };
1436
+ const restsAfter = (message) => deps.input.bufferedCount === 0 && (message.type === "result" || isInit(message) && !turnOpen);
1404
1437
  try {
1405
1438
  for await (const message of query) {
1406
1439
  deps.onMessage?.(message);
1407
- if (message.type === "system" && message.subtype === "init" && typeof message.session_id === "string") {
1440
+ if (isInit(message) && typeof message.session_id === "string") {
1408
1441
  resumeId = message.session_id;
1409
1442
  deps.onSessionId?.(resumeId);
1410
1443
  }
@@ -1412,16 +1445,23 @@ var runRemoteLoop = async (deps) => {
1412
1445
  totalCost = message.total_cost_usd;
1413
1446
  deps.onCost?.(totalCost);
1414
1447
  }
1448
+ if (message.type === "result") turnOpen = false;
1449
+ else if (isTurnActivity(message)) turnOpen = true;
1450
+ if (restsAfter(message)) armIdle();
1415
1451
  }
1452
+ disarmIdle();
1416
1453
  deps.signal.removeEventListener("abort", onAbort);
1417
- return { exitCode: deps.signal.aborted ? 130 : 0, totalCostUsd: totalCost };
1454
+ if (idleExpired) return ended(0, "idle");
1455
+ return deps.signal.aborted ? ended(130, "aborted") : ended(0, "input-closed");
1418
1456
  } catch (err) {
1457
+ disarmIdle();
1419
1458
  deps.signal.removeEventListener("abort", onAbort);
1420
- if (deps.signal.aborted) return { exitCode: 130, totalCostUsd: totalCost };
1459
+ if (idleExpired) return ended(0, "idle");
1460
+ if (deps.signal.aborted) return ended(130, "aborted");
1421
1461
  restarts++;
1422
1462
  if (restarts > maxRestarts) {
1423
1463
  deps.log?.(`[pushary] remote mode: too many restarts, giving up: ${errMsg(err)}`);
1424
- return { exitCode: 1, totalCostUsd: totalCost };
1464
+ return ended(1, "failed");
1425
1465
  }
1426
1466
  deps.log?.(
1427
1467
  `[pushary] remote mode: transient error, resuming session (attempt ${restarts}/${maxRestarts}): ${errMsg(err)}`
@@ -1429,15 +1469,18 @@ var runRemoteLoop = async (deps) => {
1429
1469
  await sleep(backoff(restarts));
1430
1470
  }
1431
1471
  }
1432
- return { exitCode: 130, totalCostUsd: totalCost };
1472
+ return ended(130, "aborted");
1433
1473
  };
1434
1474
 
1435
1475
  // src/wrapper/remoteLeg.ts
1436
1476
  var CTRL_RIGHT_BRACKET = "";
1437
1477
  var CTRL_C = "";
1438
- var runRemoteLeg = async (session, sdk) => {
1478
+ var terminalAttended = () => Boolean(process.stdin.isTTY);
1479
+ var runRemoteLeg = async (session, sdk, deps = {}) => {
1439
1480
  const legAbort = new AbortController();
1481
+ const attended = deps.attended ?? terminalAttended();
1440
1482
  let switchBack = false;
1483
+ let idleEnded = false;
1441
1484
  const control = {
1442
1485
  // The running query already drains the shared input queue, so a freshly pushed
1443
1486
  // phone command is delivered without any action here.
@@ -1472,7 +1515,7 @@ var runRemoteLeg = async (session, sdk) => {
1472
1515
  permissionMode: session.permissionMode,
1473
1516
  // The SDK REPLACES the subprocess env; spread process.env and mark the
1474
1517
  // recursion guard so a wrapper-spawned claude cannot re-enter the wrapper.
1475
- env: { ...process.env, [WRAPPER_ACTIVE_ENV]: "1" },
1518
+ env: phoneDrivenEnv({ ...process.env, [WRAPPER_ACTIVE_ENV]: "1" }),
1476
1519
  cwd: process.cwd(),
1477
1520
  // Run the user's OWN claude (auth + version parity); Windows .cmd shims keep
1478
1521
  // the SDK's bundled binary since the SDK cannot spawn a shim directly.
@@ -1480,13 +1523,20 @@ var runRemoteLeg = async (session, sdk) => {
1480
1523
  onSessionId: (id) => session.setSessionId(id),
1481
1524
  onMessage: (message) => session.streamMessage(message),
1482
1525
  log: (message) => process.stderr.write(`${message}
1483
- `)
1526
+ `),
1527
+ idleTimeoutMs: deps.idleTimeoutMs,
1528
+ scheduleIdle: deps.scheduleIdle
1484
1529
  });
1485
1530
  exitCode = result.exitCode;
1531
+ idleEnded = result.endedBy === "idle";
1486
1532
  } finally {
1487
1533
  session.stdin.releaseToChild();
1488
1534
  session.setActiveLeg(null);
1489
1535
  }
1536
+ if (idleEnded && attended) {
1537
+ process.stderr.write("[pushary] handing the terminal back to local mode\n");
1538
+ return "switch";
1539
+ }
1490
1540
  return switchBack ? "switch" : { type: "exit", code: exitCode };
1491
1541
  };
1492
1542
 
@@ -1616,6 +1666,7 @@ var runDualMode = async (binary, args2, startingMode) => {
1616
1666
  if (!sessionId) return Promise.resolve();
1617
1667
  try {
1618
1668
  await ensureOwnership();
1669
+ ownership?.touch();
1619
1670
  await reportEvent(
1620
1671
  { event, agentType: "claude_code", agentName, action, sessionId, machineId, pid: process.pid },
1621
1672
  { maxAttempts: event === "session_start" ? 4 : 2, timeoutMs: 4e3, requireOk: event === "session_start" }
@@ -1,18 +1,19 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  HERMES_CONFIG_SCRIPT,
4
+ createGuards,
4
5
  resolveHermesPython,
5
6
  runHermesPythonScript
6
- } from "../chunk-QFJZHVVL.js";
7
+ } from "../chunk-AZPIQG4G.js";
7
8
  import {
8
9
  removeInstructionBlock,
9
10
  shortenHome,
10
11
  unregisterPluginLocation,
11
12
  vscodeSettingsTargets
12
- } from "../chunk-VASZ6TWW.js";
13
+ } from "../chunk-FQL5E6NG.js";
13
14
  import {
14
15
  pusharyHookExecutable
15
- } from "../chunk-MSTHT2M7.js";
16
+ } from "../chunk-DZDI62LY.js";
16
17
  import {
17
18
  OPENCODE_PLUGIN_SIBLING,
18
19
  OPENCODE_PLUGIN_SIBLING_CONTENTS,
@@ -22,25 +23,25 @@ import {
22
23
  openCodePluginDir,
23
24
  openCodePluginFile,
24
25
  removeOpenCodeMcpServer
25
- } from "../chunk-ZWEKONU6.js";
26
+ } from "../chunk-CELX75MW.js";
26
27
  import {
27
28
  isClaudeMcpSnapshot,
28
29
  removeClaudeMcpServers,
29
30
  removeGeminiSettings,
30
31
  removePusharySettings,
31
32
  restoreClaudeMcpServer
32
- } from "../chunk-JVLB5CAJ.js";
33
- import "../chunk-B2HND22S.js";
34
- import "../chunk-VF3RTVWY.js";
33
+ } from "../chunk-GL3FFM7A.js";
34
+ import "../chunk-2LAHXHBR.js";
35
+ import "../chunk-EZIEXHYM.js";
35
36
  import {
36
37
  disableDaemonService
37
- } from "../chunk-7JZCADBR.js";
38
+ } from "../chunk-F2ICEWZL.js";
38
39
  import "../chunk-XHKBHWLX.js";
39
40
  import {
40
41
  execNpm
41
42
  } from "../chunk-L4ZCFMWW.js";
42
- import "../chunk-N4QJ3CLX.js";
43
- import "../chunk-JTBDBKHU.js";
43
+ import "../chunk-L4E2BIJ2.js";
44
+ import "../chunk-54ELC7D7.js";
44
45
  import {
45
46
  claudeJson,
46
47
  claudeMcpReceipt,
@@ -63,14 +64,14 @@ import {
63
64
  removeCodexHookTrust,
64
65
  removeCodexHooks,
65
66
  vscodePluginDir
66
- } from "../chunk-3YGGEDJK.js";
67
+ } from "../chunk-KUZWXPUS.js";
67
68
  import {
68
69
  rejectUnknownFlags
69
70
  } from "../chunk-GMXKITVA.js";
70
71
  import {
71
72
  exitOnHelpFlag
72
- } from "../chunk-WHDSJ6PD.js";
73
- import "../chunk-GMJDQDXU.js";
73
+ } from "../chunk-BVBEURWJ.js";
74
+ import "../chunk-43GMYSS4.js";
74
75
  import "../chunk-DINDL2CF.js";
75
76
  import {
76
77
  removeClaudeAlias
@@ -84,7 +85,7 @@ import "../chunk-2UMNXADU.js";
84
85
  import {
85
86
  CURSOR_GATE_EVENTS,
86
87
  HOOK_LOCATOR_BINARY
87
- } from "../chunk-QINYXLA5.js";
88
+ } from "../chunk-VNMKIJZB.js";
88
89
 
89
90
  // bin/pushary-clean.ts
90
91
  import { existsSync, readFileSync, copyFileSync, readdirSync } from "fs";
@@ -93,24 +94,6 @@ import { homedir, tmpdir } from "os";
93
94
  import { confirm } from "@inquirer/prompts";
94
95
  import { parse as parseTOML, stringify as stringifyTOML } from "smol-toml";
95
96
 
96
- // src/cli/dry-run.ts
97
- import { execSync } from "child_process";
98
- import { rmSync, writeFileSync } from "fs";
99
- var createGuards = (isDryRun, note) => ({
100
- write: (path, contents) => {
101
- if (isDryRun()) return note("rewrite", path);
102
- writeFileSync(path, contents, "utf-8");
103
- },
104
- remove: (path, options = {}) => {
105
- if (isDryRun()) return note("remove ", path);
106
- rmSync(path, { recursive: true, force: options.force ?? false });
107
- },
108
- exec: (command, options, describe) => {
109
- if (isDryRun()) return note("run ", describe);
110
- execSync(command, options);
111
- }
112
- });
113
-
114
97
  // src/app-owned-paths.ts
115
98
  var APP_OWNED_ENTRIES = [
116
99
  // PusharyPaths.bin: bridgeStub, bridgeCache
@@ -122,16 +105,7 @@ var APP_OWNED_ENTRIES = [
122
105
  // Shared: AgentInstaller.vscodePlugin writes .pushary/plugins/vscode, and so
123
106
  // does the CLI. Not attributable, so it is kept whenever the app is installed
124
107
  // rather than removed and left registered against a path that is gone.
125
- "plugins",
126
- // Shared, and the one whose loss is silent. `identity.ts` writes this Mac's id
127
- // here and `DeviceIdentity.current()` reads it in preference to its own
128
- // UserDefaults copy, precisely so one Mac has one id. Deleting it while the app
129
- // is installed does not stop the app: it falls back to the copy it minted
130
- // before that reconciliation existed, and the Mac starts reporting two ids that
131
- // can never compare equal. A session tagged with the other one is then a
132
- // session the app refuses to fetch messages for, and one `TerminalJump` calls
133
- // `.onAnotherMachine`.
134
- "machine-id"
108
+ "plugins"
135
109
  ];
136
110
  var isAppOwned = (name) => APP_OWNED_ENTRIES.includes(name);
137
111
 
@@ -6,16 +6,19 @@ import {
6
6
  postLiveSession,
7
7
  postLiveSessionUntilAccepted,
8
8
  startLiveSessionHeartbeat
9
- } from "../chunk-ZP3SNUP7.js";
10
- import "../chunk-4IPSMEGT.js";
9
+ } from "../chunk-CNPAT3HL.js";
10
+ import "../chunk-7FE2KABY.js";
11
11
  import "../chunk-QZCFVWA7.js";
12
12
  import {
13
13
  transcriptsEnabled
14
- } from "../chunk-M3UABYRT.js";
14
+ } from "../chunk-UZNKVRLE.js";
15
15
  import {
16
+ SESSION_IDLE_TIMEOUT_MS,
17
+ describeIdleWindow,
18
+ idleDeadlinePassed,
16
19
  startProcessOwnership,
17
20
  withSpawnProcessOwnership
18
- } from "../chunk-CBGGUEKE.js";
21
+ } from "../chunk-Y6BWPERV.js";
19
22
  import {
20
23
  spawnClaude
21
24
  } from "../chunk-XHKBHWLX.js";
@@ -24,17 +27,17 @@ import {
24
27
  } from "../chunk-L4ZCFMWW.js";
25
28
  import {
26
29
  getMachineId
27
- } from "../chunk-JTBDBKHU.js";
28
- import "../chunk-3YGGEDJK.js";
29
- import "../chunk-NBDU3R3G.js";
30
+ } from "../chunk-54ELC7D7.js";
31
+ import "../chunk-KUZWXPUS.js";
32
+ import "../chunk-OSALSIUF.js";
30
33
  import "../chunk-BC3VCZ3E.js";
31
- import "../chunk-ANMHIZZY.js";
34
+ import "../chunk-JW4KO5AF.js";
32
35
  import "../chunk-4LL6LG5X.js";
33
36
  import {
34
37
  getApiKey,
35
38
  getBaseUrl
36
39
  } from "../chunk-2UMNXADU.js";
37
- import "../chunk-QINYXLA5.js";
40
+ import "../chunk-VNMKIJZB.js";
38
41
 
39
42
  // bin/pushary-codex-bridge.ts
40
43
  import { existsSync } from "fs";
@@ -187,15 +190,21 @@ var rpcTurnId = (result) => {
187
190
  };
188
191
  var runCodexBridge = async (options) => {
189
192
  const timeoutMs = options.requestTimeoutMs ?? 1e4;
193
+ const now = options.now ?? Date.now;
194
+ const idleTimeoutMs = options.idleTimeoutMs ?? SESSION_IDLE_TIMEOUT_MS;
190
195
  const rpc = new JsonRpcClient(options.appServer, options.cwd, timeoutMs);
191
196
  let threadId = options.resumeThreadId ?? null;
192
197
  let activeTurnId = null;
198
+ let lastCompletedTurnId = null;
199
+ let idleSince = now();
200
+ let idledOut = false;
193
201
  let relay = null;
194
202
  let linked = false;
195
203
  let stopHeartbeat = () => {
196
204
  };
197
205
  let ownership = null;
198
206
  const messages = /* @__PURE__ */ new Map();
207
+ const idleDeadlineReached = () => activeTurnId === null && idleDeadlinePassed(idleSince, now(), idleTimeoutMs);
199
208
  const reportBody = (event, action) => ({
200
209
  event,
201
210
  action,
@@ -213,7 +222,9 @@ var runCodexBridge = async (options) => {
213
222
  const params = message.params ?? {};
214
223
  if (message.method === "turn/started") activeTurnId = string(object(params.turn)?.id);
215
224
  if (message.method === "turn/completed") {
225
+ lastCompletedTurnId = string(object(params.turn)?.id) ?? activeTurnId;
216
226
  activeTurnId = null;
227
+ idleSince = now();
217
228
  void report("stop", "Turn completed").catch(() => {
218
229
  });
219
230
  }
@@ -253,7 +264,12 @@ var runCodexBridge = async (options) => {
253
264
  reportBody("session_start", options.resumeThreadId ? "Session resumed" : "Session started")
254
265
  );
255
266
  linked = true;
256
- stopHeartbeat = startLiveSessionHeartbeat(options, reportBody("heartbeat", "Session online"));
267
+ stopHeartbeat = startLiveSessionHeartbeat(
268
+ options,
269
+ reportBody("heartbeat", "Session online"),
270
+ void 0,
271
+ () => ownership?.touch()
272
+ );
257
273
  const startTurn = async (text, commandId) => {
258
274
  relay?.add({ type: "user", uuid: commandId, message: { role: "user", content: text } });
259
275
  const result = activeTurnId ? await rpc.request("turn/steer", {
@@ -268,11 +284,16 @@ var runCodexBridge = async (options) => {
268
284
  model: options.model ?? null,
269
285
  clientUserMessageId: commandId
270
286
  });
271
- activeTurnId = rpcTurnId(result) ?? activeTurnId;
287
+ const startedTurnId = rpcTurnId(result);
288
+ if (startedTurnId && startedTurnId !== lastCompletedTurnId) activeTurnId = startedTurnId;
272
289
  };
273
290
  if (options.prompt) await startTurn(options.prompt, `spawn-${options.spawnId}`);
274
291
  let pendingAck = null;
275
292
  while (!options.signal.aborted && !rpc.isClosed) {
293
+ if (idleDeadlineReached()) {
294
+ idledOut = true;
295
+ break;
296
+ }
276
297
  try {
277
298
  if (pendingAck) {
278
299
  const acknowledged = await postLiveSession(options, "/api/agent/command/ack", pendingAck);
@@ -292,6 +313,7 @@ var runCodexBridge = async (options) => {
292
313
  const commandId = string(command?.id);
293
314
  const text = string(command?.text);
294
315
  if (commandId && text) {
316
+ idleSince = now();
295
317
  try {
296
318
  await startTurn(text, commandId);
297
319
  pendingAck = {
@@ -321,7 +343,10 @@ var runCodexBridge = async (options) => {
321
343
  await rpc.request("turn/interrupt", { threadId, turnId: activeTurnId }).catch(() => null);
322
344
  }
323
345
  await relay?.flush();
324
- await report("session_closed", "Session stopped").catch(() => null);
346
+ await report(
347
+ "session_closed",
348
+ idledOut ? `Session ended after ${describeIdleWindow(idleTimeoutMs)} idle` : "Session stopped"
349
+ ).catch(() => null);
325
350
  return "stopped";
326
351
  } catch (error) {
327
352
  const message = error instanceof Error ? error.message : String(error);