@pushary/agent-hooks 0.89.12 → 0.90.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.
- package/CHANGELOG.md +77 -0
- package/README.md +1 -0
- package/data/SKILL.md +1 -1
- package/data/cursor-plugin/skills/pushary/SKILL.md +1 -1
- package/data/vscode-plugin/skills/pushary/SKILL.md +1 -1
- package/dist/bin/pushary-bell-hook.js +4 -4
- package/dist/bin/pushary-bell.js +8 -8
- package/dist/bin/pushary-claude.js +194 -38
- package/dist/bin/pushary-clean.js +15 -32
- package/dist/bin/pushary-codex-bridge.js +37 -12
- package/dist/bin/pushary-codex-hook.js +45 -12
- package/dist/bin/pushary-codex.js +7 -7
- package/dist/bin/pushary-connect.js +11 -10
- package/dist/bin/pushary-daemon.js +186 -62
- package/dist/bin/pushary-disconnect.js +8 -8
- package/dist/bin/pushary-doctor.js +108 -25
- package/dist/bin/pushary-elicitation-hook.js +4 -4
- package/dist/bin/pushary-gemini-bridge.js +33 -11
- package/dist/bin/pushary-gemini-hook.js +21 -10
- package/dist/bin/pushary-hook.js +13 -13
- package/dist/bin/pushary-login.js +18 -7
- package/dist/bin/pushary-logout.js +6 -6
- package/dist/bin/pushary-mode.js +28 -21
- package/dist/bin/pushary-notification-hook.js +7 -7
- package/dist/bin/pushary-opencode-hook.js +8 -8
- package/dist/bin/pushary-permission-denied-hook.js +11 -11
- package/dist/bin/pushary-permission-hook.js +11 -11
- package/dist/bin/pushary-post-hook.js +14 -8
- package/dist/bin/pushary-prompt-hook.js +7 -7
- package/dist/bin/pushary-session-end-hook.js +7 -7
- package/dist/bin/pushary-session-start-hook.js +7 -7
- package/dist/bin/pushary-setup.js +168 -45
- package/dist/bin/pushary-stats.js +4 -4
- package/dist/bin/pushary-status.js +11 -10
- package/dist/bin/pushary-stop-hook.js +14 -8
- package/dist/bin/pushary-stopfailure-hook.js +7 -7
- package/dist/bin/pushary-suggestions.js +2 -2
- package/dist/bin/pushary-transcripts.d.ts +1 -0
- package/dist/bin/pushary-transcripts.js +79 -0
- package/dist/bin/pushary-upgrade.js +15 -15
- package/dist/bin/pushary-wait.js +5 -5
- package/dist/bin/pushary.js +3 -2
- package/dist/{chunk-KVHP5ZME.js → chunk-2LAHXHBR.js} +1 -1
- package/dist/{chunk-RLQWNEPM.js → chunk-43GMYSS4.js} +15 -0
- package/dist/{chunk-5NDB3YOE.js → chunk-54ELC7D7.js} +1 -1
- package/dist/{chunk-VPLQFXCH.js → chunk-5NAZ7SFS.js} +1 -1
- package/dist/chunk-7FE2KABY.js +156 -0
- package/dist/{chunk-DHR4F2CR.js → chunk-7TPQEWUZ.js} +1 -1
- package/dist/{chunk-Y4WVZIEI.js → chunk-AJYV3VGU.js} +1 -1
- package/dist/{chunk-QFJZHVVL.js → chunk-AZPIQG4G.js} +35 -1
- package/dist/{chunk-OCMOTJTW.js → chunk-B7UZHEBL.js} +29 -22
- package/dist/{chunk-7HYJPOGG.js → chunk-BQBEH6TV.js} +5 -5
- package/dist/{chunk-OTPJ4FUR.js → chunk-BVBEURWJ.js} +1 -1
- package/dist/{chunk-7DK3ZH3N.js → chunk-CELX75MW.js} +1 -1
- package/dist/{chunk-6VXNA5JB.js → chunk-CNPAT3HL.js} +3 -2
- package/dist/{chunk-327YMKP5.js → chunk-DYP6SMOR.js} +1 -1
- package/dist/chunk-DZDI62LY.js +145 -0
- package/dist/{chunk-VF3RTVWY.js → chunk-EZIEXHYM.js} +15 -7
- package/dist/{chunk-PS7DQ2SU.js → chunk-F2ICEWZL.js} +101 -19
- package/dist/{chunk-3XSP5SML.js → chunk-FQL5E6NG.js} +2 -2
- package/dist/{chunk-JHJHCMAF.js → chunk-FXEVRLHC.js} +20 -2
- package/dist/{chunk-6V7ZDH3A.js → chunk-GL3FFM7A.js} +7 -4
- package/dist/{chunk-NBRM3FMD.js → chunk-H2QZRQR2.js} +1 -1
- package/dist/{chunk-GKPURU7P.js → chunk-I463AEG3.js} +1 -1
- package/dist/{chunk-XREW7RQ3.js → chunk-JW4KO5AF.js} +1 -1
- package/dist/{chunk-OVEGEQSB.js → chunk-KUZWXPUS.js} +3 -2
- package/dist/{chunk-JVZX5YRJ.js → chunk-L4E2BIJ2.js} +80 -7
- package/dist/{chunk-RNKPLUWC.js → chunk-OSALSIUF.js} +1 -1
- package/dist/{chunk-MFNQ5IMW.js → chunk-PYA4FLKI.js} +32 -21
- package/dist/chunk-QCGNGEGE.js +15 -0
- package/dist/chunk-S6LYP7QA.js +127 -0
- package/dist/{chunk-OADGBOP7.js → chunk-TDZFPMSZ.js} +70 -11
- package/dist/{chunk-JJV5Q4KG.js → chunk-UZNKVRLE.js} +1 -1
- package/dist/{chunk-FCUUVHTS.js → chunk-VHWCVD42.js} +1 -1
- package/dist/{chunk-3MCEES6Z.js → chunk-VNMKIJZB.js} +15 -2
- package/dist/{chunk-6GU55AQQ.js → chunk-Y6BWPERV.js} +90 -7
- package/dist/chunk-YKLCWWEI.js +76 -0
- package/dist/src/index.d.ts +3 -0
- package/dist/src/index.js +11 -11
- package/package.json +3 -2
- package/dist/chunk-NESKLV6R.js +0 -72
- package/dist/chunk-WDVQ6O4C.js +0 -84
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,82 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.90.0
|
|
4
|
+
|
|
5
|
+
### A hook that needs an answer is allowed to wait for one
|
|
6
|
+
|
|
7
|
+
Claude Code hooks are now registered at the 600 second budget Claude itself
|
|
8
|
+
allows, so a policy that waits two minutes for a phone approval is no longer cut
|
|
9
|
+
off at 120 seconds and turned into a denial the user never saw. Anything a hook
|
|
10
|
+
does that is not a decision keeps its short timeout.
|
|
11
|
+
|
|
12
|
+
A phone card is now withdrawn when the tool it guarded completes or the agent's
|
|
13
|
+
own client denies it, so an approval cannot arrive for something that already
|
|
14
|
+
finished.
|
|
15
|
+
|
|
16
|
+
### The prompts that never reached the phone
|
|
17
|
+
|
|
18
|
+
Codex permission requests are no longer filtered by a shell matcher, so a browser
|
|
19
|
+
use request, a managed network request and an MCP tool request all reach the
|
|
20
|
+
phone the same way a shell escalation does. Codex in bypass mode now decides
|
|
21
|
+
before the tool runs rather than after it, which is what push-first has always
|
|
22
|
+
meant everywhere else.
|
|
23
|
+
|
|
24
|
+
Gemini reports its turn boundary through AfterAgent and its teardown through
|
|
25
|
+
SessionEnd, so a finished Gemini turn is a finished turn rather than a session
|
|
26
|
+
that stayed open.
|
|
27
|
+
|
|
28
|
+
A finished subagent now ends its own session instead of ending its parent's, and
|
|
29
|
+
appears under the session that started it. Context compaction is reported as
|
|
30
|
+
itself rather than as a tool that ran.
|
|
31
|
+
|
|
32
|
+
Pushary's own tools are never gated by Pushary, on the server exactly as in the
|
|
33
|
+
hook, so asking the user a question can no longer wait on approval to ask it.
|
|
34
|
+
|
|
35
|
+
### Sessions end, and a daemon that cannot log in says so
|
|
36
|
+
|
|
37
|
+
A session that goes quiet is closed rather than left standing forever, and the
|
|
38
|
+
daemon reaps the sessions it started. `pushary daemon status` lists what it is
|
|
39
|
+
holding, how old each one is, and how much memory it is using.
|
|
40
|
+
|
|
41
|
+
A daemon whose key is rejected now records that and stops, instead of restarting
|
|
42
|
+
into the same rejection until something notices. `pushary doctor` says the daemon
|
|
43
|
+
needs a login, and logging in clears it.
|
|
44
|
+
|
|
45
|
+
### Setup and doctor tell the truth about this machine
|
|
46
|
+
|
|
47
|
+
`pushary doctor` names the other tools holding the same agent hooks and how long
|
|
48
|
+
each of them waits, checks that the bridge the Mac app installed can be found,
|
|
49
|
+
and reports which Node the hooks will actually run under.
|
|
50
|
+
|
|
51
|
+
`pushary setup` gained `--verify none` for an unattended install, prints what it
|
|
52
|
+
would also do during a dry run, offers a way out of pairing, and can take over
|
|
53
|
+
hooks another tool is holding with `--take-over-hooks`. `pushary mode` now sets a
|
|
54
|
+
site's mode permanently unless told how long to keep it.
|
|
55
|
+
|
|
56
|
+
### Machine identity and clean
|
|
57
|
+
|
|
58
|
+
`pushary clean` leaves the machine identity shared with the Mac app intact, keeps
|
|
59
|
+
one entry rather than two, and treats the identity file as the CLI's to own and
|
|
60
|
+
the app's to read. Transcript sync seals its recovery ledger and admits a gap
|
|
61
|
+
rather than syncing bookkeeping around it.
|
|
62
|
+
|
|
63
|
+
## 0.89.13
|
|
64
|
+
|
|
65
|
+
### Synced transcripts survive large turns and restarts safely
|
|
66
|
+
|
|
67
|
+
Oversized file reads and diffs are shortened before encryption instead of being
|
|
68
|
+
rejected by the server, with the removed UTF-8 byte count recorded in the turn.
|
|
69
|
+
Stable HMAC identifiers prevent transcript contents leaking through record ids,
|
|
70
|
+
and a resumed session reuses its persisted encryption key rather than conflicting
|
|
71
|
+
with the key already registered for that session.
|
|
72
|
+
|
|
73
|
+
Transcript recipients are now pinned on first use and later key changes stop sync
|
|
74
|
+
until the user explicitly runs `pushary transcripts trust --rotate`. First use
|
|
75
|
+
also fails closed when that pin cannot be stored, so no session key is wrapped to
|
|
76
|
+
an unrecorded recipient. New sessions can independently wrap their key for the
|
|
77
|
+
owner's phone and an optional audited compliance-recovery recipient; installations
|
|
78
|
+
without one remain compatible.
|
|
79
|
+
|
|
3
80
|
## 0.89.12
|
|
4
81
|
|
|
5
82
|
### Phone control installs as a native service, without a VM
|
package/README.md
CHANGED
|
@@ -200,6 +200,7 @@ stats Show the approval moments your agents hit while not connected
|
|
|
200
200
|
suggestions List rules mined from your own approvals
|
|
201
201
|
upgrade Update the globally installed hooks to the latest version
|
|
202
202
|
hook Run as a PreToolUse hook (reads stdin, writes stdout)
|
|
203
|
+
transcripts Show or re-trust the keys this machine seals transcripts to
|
|
203
204
|
```
|
|
204
205
|
|
|
205
206
|
Run `pushary <command> --help` for one command's options.
|
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).
|
|
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).
|
|
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).
|
|
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-
|
|
4
|
+
} from "../chunk-5NAZ7SFS.js";
|
|
5
5
|
import {
|
|
6
6
|
readHookInput
|
|
7
7
|
} from "../chunk-6CUUA7HO.js";
|
|
8
|
-
import "../chunk-
|
|
8
|
+
import "../chunk-OSALSIUF.js";
|
|
9
9
|
import "../chunk-BC3VCZ3E.js";
|
|
10
|
-
import "../chunk-
|
|
10
|
+
import "../chunk-JW4KO5AF.js";
|
|
11
11
|
import "../chunk-4LL6LG5X.js";
|
|
12
12
|
import "../chunk-2UMNXADU.js";
|
|
13
|
-
import "../chunk-
|
|
13
|
+
import "../chunk-VNMKIJZB.js";
|
|
14
14
|
|
|
15
15
|
// bin/pushary-bell-hook.ts
|
|
16
16
|
var main = async () => {
|
package/dist/bin/pushary-bell.js
CHANGED
|
@@ -5,14 +5,14 @@ import {
|
|
|
5
5
|
liveAgentCount,
|
|
6
6
|
ring,
|
|
7
7
|
upgradeThreshold
|
|
8
|
-
} from "../chunk-
|
|
8
|
+
} from "../chunk-5NAZ7SFS.js";
|
|
9
9
|
import {
|
|
10
10
|
guardBinary
|
|
11
|
-
} from "../chunk-
|
|
11
|
+
} from "../chunk-EZIEXHYM.js";
|
|
12
12
|
import "../chunk-L4ZCFMWW.js";
|
|
13
13
|
import {
|
|
14
14
|
claudeSettings
|
|
15
|
-
} from "../chunk-
|
|
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-
|
|
25
|
-
import "../chunk-
|
|
24
|
+
} from "../chunk-BVBEURWJ.js";
|
|
25
|
+
import "../chunk-43GMYSS4.js";
|
|
26
26
|
import "../chunk-DINDL2CF.js";
|
|
27
|
-
import "../chunk-
|
|
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-
|
|
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-
|
|
39
|
+
import "../chunk-VNMKIJZB.js";
|
|
40
40
|
|
|
41
41
|
// src/bell/install.ts
|
|
42
42
|
import { join } from "path";
|
|
@@ -1,24 +1,32 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
changedRecipientsMessage,
|
|
4
|
+
loadPin,
|
|
5
|
+
savePin,
|
|
6
|
+
verifyRecipients
|
|
7
|
+
} from "../chunk-YKLCWWEI.js";
|
|
2
8
|
import {
|
|
3
9
|
ensureDaemonRunning
|
|
4
|
-
} from "../chunk-
|
|
10
|
+
} from "../chunk-F2ICEWZL.js";
|
|
5
11
|
import {
|
|
6
12
|
buildTranscriptRecords,
|
|
7
13
|
decodePublicKey,
|
|
8
14
|
generateSessionKey,
|
|
9
15
|
wrapSessionKey
|
|
10
|
-
} from "../chunk-
|
|
16
|
+
} from "../chunk-7FE2KABY.js";
|
|
11
17
|
import {
|
|
12
18
|
fetchTranscriptRecipients,
|
|
13
19
|
uploadTranscriptRecords
|
|
14
20
|
} from "../chunk-QZCFVWA7.js";
|
|
15
21
|
import {
|
|
16
22
|
transcriptsEnabled
|
|
17
|
-
} from "../chunk-
|
|
23
|
+
} from "../chunk-UZNKVRLE.js";
|
|
18
24
|
import {
|
|
25
|
+
SESSION_IDLE_TIMEOUT_MS,
|
|
26
|
+
describeIdleWindow,
|
|
19
27
|
startProcessOwnership,
|
|
20
28
|
withSpawnProcessOwnership
|
|
21
|
-
} from "../chunk-
|
|
29
|
+
} from "../chunk-Y6BWPERV.js";
|
|
22
30
|
import {
|
|
23
31
|
needsShell,
|
|
24
32
|
spawnClaude
|
|
@@ -30,9 +38,10 @@ import {
|
|
|
30
38
|
import {
|
|
31
39
|
fetchModeState,
|
|
32
40
|
getPolicy,
|
|
41
|
+
phoneDrivenEnv,
|
|
33
42
|
repoKeyFor,
|
|
34
43
|
reportEvent
|
|
35
|
-
} from "../chunk-
|
|
44
|
+
} from "../chunk-TDZFPMSZ.js";
|
|
36
45
|
import {
|
|
37
46
|
DEFAULT_SESSION,
|
|
38
47
|
askUser,
|
|
@@ -44,25 +53,25 @@ import {
|
|
|
44
53
|
removePendingCommand,
|
|
45
54
|
savePendingCommand,
|
|
46
55
|
waitForAnswer
|
|
47
|
-
} from "../chunk-
|
|
56
|
+
} from "../chunk-FXEVRLHC.js";
|
|
48
57
|
import {
|
|
49
58
|
UNNAMED_ACTION,
|
|
50
59
|
deriveToolTarget,
|
|
51
60
|
describeToolCall,
|
|
52
61
|
scopePathFor
|
|
53
|
-
} from "../chunk-
|
|
62
|
+
} from "../chunk-L4E2BIJ2.js";
|
|
54
63
|
import {
|
|
55
64
|
getMachineId
|
|
56
|
-
} from "../chunk-
|
|
57
|
-
import "../chunk-
|
|
65
|
+
} from "../chunk-54ELC7D7.js";
|
|
66
|
+
import "../chunk-KUZWXPUS.js";
|
|
58
67
|
import "../chunk-DINDL2CF.js";
|
|
59
|
-
import "../chunk-
|
|
68
|
+
import "../chunk-OSALSIUF.js";
|
|
60
69
|
import "../chunk-BC3VCZ3E.js";
|
|
61
70
|
import "../chunk-BSZYIAZL.js";
|
|
62
71
|
import "../chunk-SAF6HGAA.js";
|
|
63
72
|
import "../chunk-7QLSKOSU.js";
|
|
64
73
|
import "../chunk-KZERVKTD.js";
|
|
65
|
-
import "../chunk-
|
|
74
|
+
import "../chunk-JW4KO5AF.js";
|
|
66
75
|
import "../chunk-4LL6LG5X.js";
|
|
67
76
|
import {
|
|
68
77
|
getApiKey,
|
|
@@ -71,7 +80,7 @@ import {
|
|
|
71
80
|
import {
|
|
72
81
|
KILL_REASON,
|
|
73
82
|
resolveGate
|
|
74
|
-
} from "../chunk-
|
|
83
|
+
} from "../chunk-VNMKIJZB.js";
|
|
75
84
|
|
|
76
85
|
// src/wrapper/claudeBinary.ts
|
|
77
86
|
import { statSync } from "fs";
|
|
@@ -147,13 +156,82 @@ var runLocalPassthrough = (binary, args2) => {
|
|
|
147
156
|
import { basename } from "path";
|
|
148
157
|
|
|
149
158
|
// src/transcript-sync.ts
|
|
159
|
+
import { homedir as homedir2 } from "os";
|
|
160
|
+
import { join as join3, resolve } from "path";
|
|
161
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
|
|
162
|
+
|
|
163
|
+
// src/transcript-session-key.ts
|
|
164
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync as statSync2, writeFileSync } from "fs";
|
|
150
165
|
import { homedir } from "os";
|
|
151
|
-
import { join as join2
|
|
152
|
-
|
|
166
|
+
import { join as join2 } from "path";
|
|
167
|
+
var sessionDir = () => process.env.PUSHARY_SESSION_KEY_DIR?.trim() || join2(homedir(), ".pushary", "sessions");
|
|
168
|
+
var SESSION_KEY_TTL_MS = 48 * 60 * 60 * 1e3;
|
|
169
|
+
var SAFE_SESSION_ID = /^[A-Za-z0-9._-]{1,128}$/;
|
|
170
|
+
var fileFor = (sessionId) => SAFE_SESSION_ID.test(sessionId) ? join2(sessionDir(), `${sessionId}.json`) : null;
|
|
171
|
+
var loadSessionKey = (sessionId) => {
|
|
172
|
+
const path = fileFor(sessionId);
|
|
173
|
+
if (!path || !existsSync(path)) return null;
|
|
174
|
+
try {
|
|
175
|
+
if (Date.now() - statSync2(path).mtimeMs > SESSION_KEY_TTL_MS) return null;
|
|
176
|
+
const raw = JSON.parse(readFileSync(path, "utf-8"));
|
|
177
|
+
const key = typeof raw.sessionKey === "string" ? Buffer.from(raw.sessionKey, "base64") : null;
|
|
178
|
+
if (!key || key.length !== 32) return null;
|
|
179
|
+
return {
|
|
180
|
+
sessionKey: new Uint8Array(key),
|
|
181
|
+
wrappedKey: typeof raw.wrappedKey === "string" ? raw.wrappedKey : null,
|
|
182
|
+
providerWrappedKey: typeof raw.providerWrappedKey === "string" ? raw.providerWrappedKey : null,
|
|
183
|
+
providerKeyId: typeof raw.providerKeyId === "string" ? raw.providerKeyId : null,
|
|
184
|
+
keyUploaded: raw.keyUploaded === true
|
|
185
|
+
};
|
|
186
|
+
} catch {
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
var saveSessionKey = (sessionId, value) => {
|
|
191
|
+
const path = fileFor(sessionId);
|
|
192
|
+
if (!path) return;
|
|
193
|
+
try {
|
|
194
|
+
mkdirSync(sessionDir(), { recursive: true, mode: 448 });
|
|
195
|
+
writeFileSync(
|
|
196
|
+
path,
|
|
197
|
+
JSON.stringify({
|
|
198
|
+
sessionKey: Buffer.from(value.sessionKey).toString("base64"),
|
|
199
|
+
wrappedKey: value.wrappedKey,
|
|
200
|
+
providerWrappedKey: value.providerWrappedKey,
|
|
201
|
+
providerKeyId: value.providerKeyId,
|
|
202
|
+
keyUploaded: value.keyUploaded
|
|
203
|
+
}),
|
|
204
|
+
{ mode: 384 }
|
|
205
|
+
);
|
|
206
|
+
} catch {
|
|
207
|
+
}
|
|
208
|
+
};
|
|
209
|
+
var sweepStaleSessionKeys = (now = Date.now()) => {
|
|
210
|
+
const dir = sessionDir();
|
|
211
|
+
if (!existsSync(dir)) return 0;
|
|
212
|
+
let removed = 0;
|
|
213
|
+
try {
|
|
214
|
+
for (const name of readdirSync(dir)) {
|
|
215
|
+
if (!name.endsWith(".json")) continue;
|
|
216
|
+
const path = join2(dir, name);
|
|
217
|
+
try {
|
|
218
|
+
if (now - statSync2(path).mtimeMs <= SESSION_KEY_TTL_MS) continue;
|
|
219
|
+
rmSync(path, { force: true });
|
|
220
|
+
removed += 1;
|
|
221
|
+
} catch {
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
} catch {
|
|
225
|
+
return removed;
|
|
226
|
+
}
|
|
227
|
+
return removed;
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
// src/transcript-sync.ts
|
|
153
231
|
var claudeTranscriptPath = (cwd, sessionId) => {
|
|
154
232
|
const projectId = resolve(cwd).replace(/[^a-zA-Z0-9-]/g, "-");
|
|
155
|
-
const configDir = process.env.CLAUDE_CONFIG_DIR ||
|
|
156
|
-
return
|
|
233
|
+
const configDir = process.env.CLAUDE_CONFIG_DIR || join3(homedir2(), ".claude");
|
|
234
|
+
return join3(configDir, "projects", projectId, `${sessionId}.jsonl`);
|
|
157
235
|
};
|
|
158
236
|
var createSyncState = () => ({
|
|
159
237
|
sessionKey: null,
|
|
@@ -241,7 +319,7 @@ var chunkRecords = (records) => {
|
|
|
241
319
|
};
|
|
242
320
|
var readLines = (path) => {
|
|
243
321
|
try {
|
|
244
|
-
return
|
|
322
|
+
return existsSync2(path) ? readFileSync2(path, "utf-8").split("\n") : [];
|
|
245
323
|
} catch {
|
|
246
324
|
return [];
|
|
247
325
|
}
|
|
@@ -249,19 +327,49 @@ var readLines = (path) => {
|
|
|
249
327
|
var IDLE_CEILING_MS = 6e4;
|
|
250
328
|
var nextInterval = (previousMs, idle, baseMs, maxMs) => idle ? Math.min(previousMs * 2, maxMs) : baseMs;
|
|
251
329
|
var startTranscriptSync = (opts) => {
|
|
330
|
+
let warnedAboutRecipients = false;
|
|
331
|
+
const fetchPinnedRecipients = async () => {
|
|
332
|
+
const offer = await fetchTranscriptRecipients(opts.apiKey);
|
|
333
|
+
if (!offer) return null;
|
|
334
|
+
const verdict = verifyRecipients(offer, loadPin());
|
|
335
|
+
if (verdict.kind === "first-use" && !savePin(verdict.pin)) return null;
|
|
336
|
+
if (verdict.kind === "changed") {
|
|
337
|
+
if (!warnedAboutRecipients) {
|
|
338
|
+
warnedAboutRecipients = true;
|
|
339
|
+
process.stderr.write(`${changedRecipientsMessage(verdict, offer)}
|
|
340
|
+
`);
|
|
341
|
+
}
|
|
342
|
+
return null;
|
|
343
|
+
}
|
|
344
|
+
return offer;
|
|
345
|
+
};
|
|
252
346
|
const uploader = {
|
|
253
|
-
fetchTranscriptRecipients:
|
|
347
|
+
fetchTranscriptRecipients: fetchPinnedRecipients,
|
|
254
348
|
uploadRecords: (params) => uploadTranscriptRecords(opts.apiKey, params)
|
|
255
349
|
};
|
|
256
350
|
const path = claudeTranscriptPath(opts.cwd, opts.sessionId);
|
|
257
|
-
|
|
351
|
+
const persisted = loadSessionKey(opts.sessionId);
|
|
352
|
+
let state = persisted ? { ...createSyncState(), ...persisted } : createSyncState();
|
|
353
|
+
sweepStaleSessionKeys();
|
|
354
|
+
let persistedUploaded = persisted?.keyUploaded ?? false;
|
|
258
355
|
let running = true;
|
|
259
356
|
let inFlight = false;
|
|
260
357
|
const tick = async () => {
|
|
261
358
|
if (!running || inFlight) return;
|
|
262
359
|
inFlight = true;
|
|
263
360
|
try {
|
|
361
|
+
const before = state.sessionKey;
|
|
264
362
|
state = await syncTick(readLines(path), state, uploader, opts.sessionId, opts.machineId);
|
|
363
|
+
if (state.sessionKey && (before !== state.sessionKey || state.keyUploaded !== persistedUploaded)) {
|
|
364
|
+
persistedUploaded = state.keyUploaded;
|
|
365
|
+
saveSessionKey(opts.sessionId, {
|
|
366
|
+
sessionKey: state.sessionKey,
|
|
367
|
+
wrappedKey: state.wrappedKey,
|
|
368
|
+
providerWrappedKey: state.providerWrappedKey,
|
|
369
|
+
providerKeyId: state.providerKeyId,
|
|
370
|
+
keyUploaded: state.keyUploaded
|
|
371
|
+
});
|
|
372
|
+
}
|
|
265
373
|
} catch {
|
|
266
374
|
} finally {
|
|
267
375
|
inFlight = false;
|
|
@@ -292,9 +400,9 @@ var startTranscriptSync = (opts) => {
|
|
|
292
400
|
|
|
293
401
|
// src/wrapper/sdkLoader.ts
|
|
294
402
|
import { spawn } from "child_process";
|
|
295
|
-
import { existsSync as
|
|
296
|
-
import { homedir as
|
|
297
|
-
import { join as
|
|
403
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
404
|
+
import { homedir as homedir3 } from "os";
|
|
405
|
+
import { join as join4 } from "path";
|
|
298
406
|
import { pathToFileURL } from "url";
|
|
299
407
|
var userMessage = (text) => ({
|
|
300
408
|
type: "user",
|
|
@@ -304,8 +412,8 @@ var userMessage = (text) => ({
|
|
|
304
412
|
var SDK_VERSION = "0.3.207";
|
|
305
413
|
var SDK_PACKAGE = "@anthropic-ai/claude-agent-sdk";
|
|
306
414
|
var SDK_SPEC = `${SDK_PACKAGE}@${SDK_VERSION}`;
|
|
307
|
-
var sdkCacheDir = () => process.env.PUSHARY_REMOTE_SDK_DIR?.trim() ||
|
|
308
|
-
var cachedSdkEntry = () =>
|
|
415
|
+
var sdkCacheDir = () => process.env.PUSHARY_REMOTE_SDK_DIR?.trim() || join4(homedir3(), ".pushary", "remote-sdk");
|
|
416
|
+
var cachedSdkEntry = () => join4(sdkCacheDir(), "node_modules", "@anthropic-ai", "claude-agent-sdk", "sdk.mjs");
|
|
309
417
|
var importSdk = async (specifierOrUrl) => {
|
|
310
418
|
try {
|
|
311
419
|
const mod = await import(specifierOrUrl);
|
|
@@ -320,7 +428,7 @@ var loadClaudeSdk = async () => {
|
|
|
320
428
|
const fromResolution = await importSdk(SDK_PACKAGE);
|
|
321
429
|
if (fromResolution) return fromResolution;
|
|
322
430
|
const entry = cachedSdkEntry();
|
|
323
|
-
if (
|
|
431
|
+
if (existsSync3(entry)) return importSdk(pathToFileURL(entry).href);
|
|
324
432
|
return null;
|
|
325
433
|
};
|
|
326
434
|
var ensureClaudeSdk = async (log) => {
|
|
@@ -328,10 +436,10 @@ var ensureClaudeSdk = async (log) => {
|
|
|
328
436
|
if (present) return present;
|
|
329
437
|
const dir = sdkCacheDir();
|
|
330
438
|
try {
|
|
331
|
-
|
|
332
|
-
const pkgJson =
|
|
333
|
-
if (!
|
|
334
|
-
|
|
439
|
+
mkdirSync2(dir, { recursive: true });
|
|
440
|
+
const pkgJson = join4(dir, "package.json");
|
|
441
|
+
if (!existsSync3(pkgJson)) {
|
|
442
|
+
writeFileSync2(pkgJson, `${JSON.stringify({ name: "pushary-remote-sdk", private: true })}
|
|
335
443
|
`);
|
|
336
444
|
}
|
|
337
445
|
} catch {
|
|
@@ -1262,14 +1370,30 @@ var runLocalLeg = (session, deps = {}) => {
|
|
|
1262
1370
|
|
|
1263
1371
|
// src/wrapper/remoteLoop.ts
|
|
1264
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";
|
|
1265
1380
|
var DEFAULT_MAX_RESTARTS = 5;
|
|
1266
1381
|
var runRemoteLoop = async (deps) => {
|
|
1267
1382
|
const maxRestarts = deps.maxRestarts ?? DEFAULT_MAX_RESTARTS;
|
|
1268
1383
|
const sleep = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
1269
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;
|
|
1270
1387
|
let resumeId = deps.initialResume;
|
|
1271
1388
|
let totalCost = 0;
|
|
1272
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 });
|
|
1273
1397
|
while (!deps.signal.aborted) {
|
|
1274
1398
|
deps.input.detachConsumer();
|
|
1275
1399
|
let query;
|
|
@@ -1287,7 +1411,7 @@ var runRemoteLoop = async (deps) => {
|
|
|
1287
1411
|
});
|
|
1288
1412
|
} catch (err) {
|
|
1289
1413
|
deps.log?.(`[pushary] remote mode: query failed to start: ${errMsg(err)}`);
|
|
1290
|
-
return
|
|
1414
|
+
return ended(1, "failed");
|
|
1291
1415
|
}
|
|
1292
1416
|
const onAbort = () => {
|
|
1293
1417
|
deps.input.close();
|
|
@@ -1296,10 +1420,24 @@ var runRemoteLoop = async (deps) => {
|
|
|
1296
1420
|
};
|
|
1297
1421
|
if (deps.signal.aborted) onAbort();
|
|
1298
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);
|
|
1299
1437
|
try {
|
|
1300
1438
|
for await (const message of query) {
|
|
1301
1439
|
deps.onMessage?.(message);
|
|
1302
|
-
if (message
|
|
1440
|
+
if (isInit(message) && typeof message.session_id === "string") {
|
|
1303
1441
|
resumeId = message.session_id;
|
|
1304
1442
|
deps.onSessionId?.(resumeId);
|
|
1305
1443
|
}
|
|
@@ -1307,16 +1445,23 @@ var runRemoteLoop = async (deps) => {
|
|
|
1307
1445
|
totalCost = message.total_cost_usd;
|
|
1308
1446
|
deps.onCost?.(totalCost);
|
|
1309
1447
|
}
|
|
1448
|
+
if (message.type === "result") turnOpen = false;
|
|
1449
|
+
else if (isTurnActivity(message)) turnOpen = true;
|
|
1450
|
+
if (restsAfter(message)) armIdle();
|
|
1310
1451
|
}
|
|
1452
|
+
disarmIdle();
|
|
1311
1453
|
deps.signal.removeEventListener("abort", onAbort);
|
|
1312
|
-
|
|
1454
|
+
if (idleExpired) return ended(0, "idle");
|
|
1455
|
+
return deps.signal.aborted ? ended(130, "aborted") : ended(0, "input-closed");
|
|
1313
1456
|
} catch (err) {
|
|
1457
|
+
disarmIdle();
|
|
1314
1458
|
deps.signal.removeEventListener("abort", onAbort);
|
|
1315
|
-
if (
|
|
1459
|
+
if (idleExpired) return ended(0, "idle");
|
|
1460
|
+
if (deps.signal.aborted) return ended(130, "aborted");
|
|
1316
1461
|
restarts++;
|
|
1317
1462
|
if (restarts > maxRestarts) {
|
|
1318
1463
|
deps.log?.(`[pushary] remote mode: too many restarts, giving up: ${errMsg(err)}`);
|
|
1319
|
-
return
|
|
1464
|
+
return ended(1, "failed");
|
|
1320
1465
|
}
|
|
1321
1466
|
deps.log?.(
|
|
1322
1467
|
`[pushary] remote mode: transient error, resuming session (attempt ${restarts}/${maxRestarts}): ${errMsg(err)}`
|
|
@@ -1324,15 +1469,18 @@ var runRemoteLoop = async (deps) => {
|
|
|
1324
1469
|
await sleep(backoff(restarts));
|
|
1325
1470
|
}
|
|
1326
1471
|
}
|
|
1327
|
-
return
|
|
1472
|
+
return ended(130, "aborted");
|
|
1328
1473
|
};
|
|
1329
1474
|
|
|
1330
1475
|
// src/wrapper/remoteLeg.ts
|
|
1331
1476
|
var CTRL_RIGHT_BRACKET = "";
|
|
1332
1477
|
var CTRL_C = "";
|
|
1333
|
-
var
|
|
1478
|
+
var terminalAttended = () => Boolean(process.stdin.isTTY);
|
|
1479
|
+
var runRemoteLeg = async (session, sdk, deps = {}) => {
|
|
1334
1480
|
const legAbort = new AbortController();
|
|
1481
|
+
const attended = deps.attended ?? terminalAttended();
|
|
1335
1482
|
let switchBack = false;
|
|
1483
|
+
let idleEnded = false;
|
|
1336
1484
|
const control = {
|
|
1337
1485
|
// The running query already drains the shared input queue, so a freshly pushed
|
|
1338
1486
|
// phone command is delivered without any action here.
|
|
@@ -1367,7 +1515,7 @@ var runRemoteLeg = async (session, sdk) => {
|
|
|
1367
1515
|
permissionMode: session.permissionMode,
|
|
1368
1516
|
// The SDK REPLACES the subprocess env; spread process.env and mark the
|
|
1369
1517
|
// recursion guard so a wrapper-spawned claude cannot re-enter the wrapper.
|
|
1370
|
-
env: { ...process.env, [WRAPPER_ACTIVE_ENV]: "1" },
|
|
1518
|
+
env: phoneDrivenEnv({ ...process.env, [WRAPPER_ACTIVE_ENV]: "1" }),
|
|
1371
1519
|
cwd: process.cwd(),
|
|
1372
1520
|
// Run the user's OWN claude (auth + version parity); Windows .cmd shims keep
|
|
1373
1521
|
// the SDK's bundled binary since the SDK cannot spawn a shim directly.
|
|
@@ -1375,13 +1523,20 @@ var runRemoteLeg = async (session, sdk) => {
|
|
|
1375
1523
|
onSessionId: (id) => session.setSessionId(id),
|
|
1376
1524
|
onMessage: (message) => session.streamMessage(message),
|
|
1377
1525
|
log: (message) => process.stderr.write(`${message}
|
|
1378
|
-
`)
|
|
1526
|
+
`),
|
|
1527
|
+
idleTimeoutMs: deps.idleTimeoutMs,
|
|
1528
|
+
scheduleIdle: deps.scheduleIdle
|
|
1379
1529
|
});
|
|
1380
1530
|
exitCode = result.exitCode;
|
|
1531
|
+
idleEnded = result.endedBy === "idle";
|
|
1381
1532
|
} finally {
|
|
1382
1533
|
session.stdin.releaseToChild();
|
|
1383
1534
|
session.setActiveLeg(null);
|
|
1384
1535
|
}
|
|
1536
|
+
if (idleEnded && attended) {
|
|
1537
|
+
process.stderr.write("[pushary] handing the terminal back to local mode\n");
|
|
1538
|
+
return "switch";
|
|
1539
|
+
}
|
|
1385
1540
|
return switchBack ? "switch" : { type: "exit", code: exitCode };
|
|
1386
1541
|
};
|
|
1387
1542
|
|
|
@@ -1511,6 +1666,7 @@ var runDualMode = async (binary, args2, startingMode) => {
|
|
|
1511
1666
|
if (!sessionId) return Promise.resolve();
|
|
1512
1667
|
try {
|
|
1513
1668
|
await ensureOwnership();
|
|
1669
|
+
ownership?.touch();
|
|
1514
1670
|
await reportEvent(
|
|
1515
1671
|
{ event, agentType: "claude_code", agentName, action, sessionId, machineId, pid: process.pid },
|
|
1516
1672
|
{ maxAttempts: event === "session_start" ? 4 : 2, timeoutMs: 4e3, requireOk: event === "session_start" }
|