@pushary/agent-hooks 0.48.0 → 0.49.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.
- package/dist/bin/pushary-claude.js +24 -23
- package/dist/bin/pushary-clean.js +16 -0
- package/dist/bin/pushary-codex-hook.js +4 -2
- package/dist/bin/pushary-codex.js +4 -2
- package/dist/bin/pushary-daemon.d.ts +1 -0
- package/dist/bin/pushary-daemon.js +143 -0
- package/dist/bin/pushary-doctor.js +39 -0
- package/dist/bin/pushary-gemini-hook.js +4 -2
- package/dist/bin/pushary-hook.js +3 -2
- package/dist/bin/pushary-notification-hook.js +2 -1
- package/dist/bin/pushary-permission-denied-hook.js +3 -2
- package/dist/bin/pushary-permission-hook.js +3 -2
- package/dist/bin/pushary-post-hook.js +2 -1
- package/dist/bin/pushary-prompt-hook.js +2 -1
- package/dist/bin/pushary-session-end-hook.js +2 -1
- package/dist/bin/pushary-session-start-hook.js +2 -1
- package/dist/bin/pushary-setup.js +30 -1
- package/dist/bin/pushary-stop-hook.js +2 -1
- package/dist/bin/pushary-stopfailure-hook.js +2 -1
- package/dist/bin/pushary.js +6 -0
- package/dist/chunk-BC3VCZ3E.js +86 -0
- package/dist/{chunk-VZNP4R5I.js → chunk-O5MFSRWV.js} +9 -13
- package/dist/{chunk-E3Q3KYZF.js → chunk-OQMEGREX.js} +4 -2
- package/dist/chunk-RN3NOEJF.js +9 -0
- package/dist/chunk-XHKBHWLX.js +14 -0
- package/dist/src/index.js +3 -2
- package/package.json +2 -1
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
needsShell,
|
|
4
|
+
spawnClaude
|
|
5
|
+
} from "../chunk-XHKBHWLX.js";
|
|
2
6
|
import {
|
|
3
7
|
isDeferAnswer
|
|
4
8
|
} from "../chunk-KQYIHZ5E.js";
|
|
@@ -8,12 +12,14 @@ import {
|
|
|
8
12
|
deriveToolTarget,
|
|
9
13
|
describeToolCall,
|
|
10
14
|
fetchModeState,
|
|
11
|
-
getMachineId,
|
|
12
15
|
getPolicy,
|
|
13
16
|
reportEvent,
|
|
14
17
|
resolvePolicy,
|
|
15
18
|
waitForAnswer
|
|
16
|
-
} from "../chunk-
|
|
19
|
+
} from "../chunk-O5MFSRWV.js";
|
|
20
|
+
import {
|
|
21
|
+
getMachineId
|
|
22
|
+
} from "../chunk-RN3NOEJF.js";
|
|
17
23
|
import "../chunk-DWED7BS3.js";
|
|
18
24
|
import "../chunk-Z5PL3K7C.js";
|
|
19
25
|
import {
|
|
@@ -47,16 +53,6 @@ var findClaudeBinary = () => {
|
|
|
47
53
|
return null;
|
|
48
54
|
};
|
|
49
55
|
|
|
50
|
-
// src/wrapper/spawnClaude.ts
|
|
51
|
-
import { spawn } from "child_process";
|
|
52
|
-
var needsShell = (binary, platform = process.platform) => platform === "win32" && /\.(cmd|bat)$/i.test(binary);
|
|
53
|
-
var spawnClaude = (binary, args2, options) => {
|
|
54
|
-
if (needsShell(binary)) {
|
|
55
|
-
return spawn(`"${binary}"`, args2, { ...options, shell: true });
|
|
56
|
-
}
|
|
57
|
-
return spawn(binary, args2, options);
|
|
58
|
-
};
|
|
59
|
-
|
|
60
56
|
// src/wrapper/localPassthrough.ts
|
|
61
57
|
var SIGNAL_NUMBERS = {
|
|
62
58
|
SIGHUP: 1,
|
|
@@ -105,7 +101,7 @@ var runLocalPassthrough = (binary, args2) => {
|
|
|
105
101
|
import { basename } from "path";
|
|
106
102
|
|
|
107
103
|
// src/wrapper/sdkLoader.ts
|
|
108
|
-
import { spawn
|
|
104
|
+
import { spawn } from "child_process";
|
|
109
105
|
import { existsSync, mkdirSync, writeFileSync } from "fs";
|
|
110
106
|
import { homedir } from "os";
|
|
111
107
|
import { join as join2 } from "path";
|
|
@@ -154,7 +150,7 @@ var ensureClaudeSdk = async (log) => {
|
|
|
154
150
|
log?.("[pushary] setting up phone control (one-time, fetching the Claude Agent SDK)...");
|
|
155
151
|
const npmCmd = process.platform === "win32" ? "npm.cmd" : "npm";
|
|
156
152
|
const installed = await new Promise((resolve) => {
|
|
157
|
-
const child =
|
|
153
|
+
const child = spawn(
|
|
158
154
|
npmCmd,
|
|
159
155
|
["install", "--no-optional", "--no-audit", "--no-fund", "--loglevel=error", SDK_SPEC],
|
|
160
156
|
{ cwd: dir, stdio: "ignore", shell: process.platform === "win32" }
|
|
@@ -513,7 +509,7 @@ var startCommandPoller = (opts) => {
|
|
|
513
509
|
schedule(nextDelay());
|
|
514
510
|
};
|
|
515
511
|
schedule(fast);
|
|
516
|
-
opts.log?.("[pushary]
|
|
512
|
+
opts.log?.("[pushary] command poller active");
|
|
517
513
|
return {
|
|
518
514
|
stop() {
|
|
519
515
|
stopped = true;
|
|
@@ -881,7 +877,7 @@ var nativeArgs = (session) => {
|
|
|
881
877
|
return args2;
|
|
882
878
|
};
|
|
883
879
|
var runLocalLeg = (session, deps = {}) => {
|
|
884
|
-
const
|
|
880
|
+
const spawn2 = deps.spawn ?? spawnClaude;
|
|
885
881
|
return new Promise((resolve) => {
|
|
886
882
|
const legAbort = new AbortController();
|
|
887
883
|
let exitReason = null;
|
|
@@ -911,7 +907,7 @@ var runLocalLeg = (session, deps = {}) => {
|
|
|
911
907
|
session.stdin.releaseToChild();
|
|
912
908
|
let child;
|
|
913
909
|
try {
|
|
914
|
-
child =
|
|
910
|
+
child = spawn2(session.binary, nativeArgs(session), {
|
|
915
911
|
stdio: "inherit",
|
|
916
912
|
env: { ...process.env, [WRAPPER_ACTIVE_ENV]: "1" },
|
|
917
913
|
signal: legAbort.signal
|
|
@@ -1134,6 +1130,11 @@ var runDualMode = async (binary, args2, startingMode) => {
|
|
|
1134
1130
|
const agentName = `Claude Code - ${projectName}`;
|
|
1135
1131
|
const machineId = getMachineId();
|
|
1136
1132
|
const stdin = createStdinHandoff();
|
|
1133
|
+
const verbose = process.env.PUSHARY_VERBOSE === "1";
|
|
1134
|
+
const verboseLog = (message) => {
|
|
1135
|
+
if (verbose) process.stderr.write(`${message}
|
|
1136
|
+
`);
|
|
1137
|
+
};
|
|
1137
1138
|
const { initialPrompt, resume, permissionMode } = parseRemoteArgs(args2);
|
|
1138
1139
|
const userManagesSession = args2.some((a) => SESSION_FLAG_RE.test(a));
|
|
1139
1140
|
let sessionId = resume;
|
|
@@ -1168,10 +1169,8 @@ var runDualMode = async (binary, args2, startingMode) => {
|
|
|
1168
1169
|
agentType: "claude_code",
|
|
1169
1170
|
getSessionId: () => sessionId,
|
|
1170
1171
|
onCommand: (command) => onPhoneCommand(command),
|
|
1171
|
-
onFatal: (message) =>
|
|
1172
|
-
|
|
1173
|
-
log: (message) => process.stderr.write(`${message}
|
|
1174
|
-
`)
|
|
1172
|
+
onFatal: (message) => verboseLog(`[pushary] relay unavailable (${message}); using polling`),
|
|
1173
|
+
log: verboseLog
|
|
1175
1174
|
});
|
|
1176
1175
|
relay.start();
|
|
1177
1176
|
}
|
|
@@ -1232,8 +1231,7 @@ var runDualMode = async (binary, args2, startingMode) => {
|
|
|
1232
1231
|
activeLeg?.stop("kill");
|
|
1233
1232
|
}
|
|
1234
1233
|
},
|
|
1235
|
-
log:
|
|
1236
|
-
`)
|
|
1234
|
+
log: verboseLog
|
|
1237
1235
|
});
|
|
1238
1236
|
const signals = ["SIGINT", "SIGTERM", "SIGHUP"];
|
|
1239
1237
|
const onSignal = () => {
|
|
@@ -1269,6 +1267,9 @@ var runDualMode = async (binary, args2, startingMode) => {
|
|
|
1269
1267
|
};
|
|
1270
1268
|
try {
|
|
1271
1269
|
if (sessionId && startingMode === "local") {
|
|
1270
|
+
process.stderr.write(
|
|
1271
|
+
"[pushary] reachable from your phone \u2014 send an instruction from the app when this goes idle (Ctrl-] keeps the terminal).\n"
|
|
1272
|
+
);
|
|
1272
1273
|
void announcePresence("session_start", "Session ready \u2014 reachable from your phone");
|
|
1273
1274
|
}
|
|
1274
1275
|
if (startingMode === "remote" && initialPrompt) input.push(userMessage(initialPrompt));
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
removeClaudeAlias
|
|
4
|
+
} from "../chunk-BC3VCZ3E.js";
|
|
2
5
|
import {
|
|
3
6
|
removeCodexHooks,
|
|
4
7
|
removeGeminiSettings,
|
|
@@ -240,6 +243,12 @@ var main = async () => {
|
|
|
240
243
|
} catch {
|
|
241
244
|
}
|
|
242
245
|
}
|
|
246
|
+
const aliasRemovedFrom = removeClaudeAlias();
|
|
247
|
+
if (aliasRemovedFrom.length > 0) {
|
|
248
|
+
console.log(` ${check} claude alias ${dim(`(removed from ${aliasRemovedFrom.map((p) => p.split("/").pop()).join(", ")})`)}`);
|
|
249
|
+
} else {
|
|
250
|
+
console.log(` ${skip} claude alias ${dim("(not set)")}`);
|
|
251
|
+
}
|
|
243
252
|
if (existsSync(PUSHARY_DIR)) {
|
|
244
253
|
rmSync(PUSHARY_DIR, { recursive: true });
|
|
245
254
|
console.log(` ${check} Local state ${dim("(~/.pushary removed: stored API key, ledger)")}`);
|
|
@@ -268,6 +277,13 @@ var main = async () => {
|
|
|
268
277
|
console.log();
|
|
269
278
|
console.log(` ${green(bold("Clean complete."))}`);
|
|
270
279
|
console.log(` ${dim("Run")} npx @pushary/agent-hooks@latest setup ${dim("to reinstall.")}`);
|
|
280
|
+
if (aliasRemovedFrom.length > 0) {
|
|
281
|
+
console.log();
|
|
282
|
+
console.log(` ${yellow("!")} ${bold("One thing left in your current terminal.")}`);
|
|
283
|
+
console.log(` ${dim("It still has the")} ${bold("claude")} ${dim("alias loaded from before. Run")} ${bold("unalias claude")}`);
|
|
284
|
+
console.log(` ${dim("(or open a new terminal) so")} ${bold("claude")} ${dim("runs Claude Code again.")}`);
|
|
285
|
+
}
|
|
286
|
+
console.log(` ${dim("If a")} ${bold("pushary claude")} ${dim("or")} ${bold("pushary daemon")} ${dim("is still running anywhere, stop it (Ctrl-C).")}`);
|
|
271
287
|
console.log();
|
|
272
288
|
};
|
|
273
289
|
main();
|
|
@@ -17,7 +17,6 @@ import {
|
|
|
17
17
|
describeApplyPatch,
|
|
18
18
|
describeToolCall,
|
|
19
19
|
fetchModeState,
|
|
20
|
-
getMachineId,
|
|
21
20
|
getPolicy,
|
|
22
21
|
handlePostToolUse,
|
|
23
22
|
handleStop,
|
|
@@ -32,7 +31,10 @@ import {
|
|
|
32
31
|
toCodexWire,
|
|
33
32
|
toPolicyLookup,
|
|
34
33
|
waitForAnswer
|
|
35
|
-
} from "../chunk-
|
|
34
|
+
} from "../chunk-O5MFSRWV.js";
|
|
35
|
+
import {
|
|
36
|
+
getMachineId
|
|
37
|
+
} from "../chunk-RN3NOEJF.js";
|
|
36
38
|
import {
|
|
37
39
|
isGatingMoment,
|
|
38
40
|
recordKeylessMoment
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
askUser,
|
|
4
|
-
getMachineId,
|
|
5
4
|
reportEvent,
|
|
6
5
|
waitForAnswer
|
|
7
|
-
} from "../chunk-
|
|
6
|
+
} from "../chunk-O5MFSRWV.js";
|
|
7
|
+
import {
|
|
8
|
+
getMachineId
|
|
9
|
+
} from "../chunk-RN3NOEJF.js";
|
|
8
10
|
import "../chunk-DWED7BS3.js";
|
|
9
11
|
import "../chunk-Z5PL3K7C.js";
|
|
10
12
|
import {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
spawnClaude
|
|
4
|
+
} from "../chunk-XHKBHWLX.js";
|
|
5
|
+
import {
|
|
6
|
+
resolveGlobalBinary
|
|
7
|
+
} from "../chunk-J7JWI3KU.js";
|
|
8
|
+
import {
|
|
9
|
+
getMachineId
|
|
10
|
+
} from "../chunk-RN3NOEJF.js";
|
|
11
|
+
import {
|
|
12
|
+
getApiKey,
|
|
13
|
+
getBaseUrl
|
|
14
|
+
} from "../chunk-NKXSILEW.js";
|
|
15
|
+
|
|
16
|
+
// src/spawn-daemon.ts
|
|
17
|
+
import { spawn } from "child_process";
|
|
18
|
+
import { existsSync } from "fs";
|
|
19
|
+
import { basename, dirname, join } from "path";
|
|
20
|
+
var DRAIN_PATH = "/api/agent/spawn/drain";
|
|
21
|
+
var FAST_POLL_MS = 3e3;
|
|
22
|
+
var SLOW_POLL_MS = 3e4;
|
|
23
|
+
var FAST_DECAY_TICKS = 3;
|
|
24
|
+
var MAX_ERROR_BACKOFF_MS = 12e4;
|
|
25
|
+
var MAX_ERROR_STREAK = 6;
|
|
26
|
+
var REQUEST_TIMEOUT_MS = 1e4;
|
|
27
|
+
var MAX_SPAWNS_PER_WINDOW = 5;
|
|
28
|
+
var SPAWN_WINDOW_MS = 6e4;
|
|
29
|
+
var spawnRateAllows = (timestamps, now, maxPerWindow = MAX_SPAWNS_PER_WINDOW, windowMs = SPAWN_WINDOW_MS) => {
|
|
30
|
+
while (timestamps.length > 0 && now - timestamps[0] > windowMs) timestamps.shift();
|
|
31
|
+
return timestamps.length < maxPerWindow;
|
|
32
|
+
};
|
|
33
|
+
var stderr = (msg) => {
|
|
34
|
+
process.stderr.write(msg);
|
|
35
|
+
};
|
|
36
|
+
var runSpawnDaemon = async () => {
|
|
37
|
+
let apiKey;
|
|
38
|
+
try {
|
|
39
|
+
apiKey = getApiKey();
|
|
40
|
+
} catch {
|
|
41
|
+
stderr("[pushary] daemon needs an API key. Run `npx @pushary/agent-hooks setup`.\n");
|
|
42
|
+
return 1;
|
|
43
|
+
}
|
|
44
|
+
const machineId = getMachineId();
|
|
45
|
+
const baseUrl = getBaseUrl();
|
|
46
|
+
const pusharyBin = resolveGlobalBinary("pushary") ?? "pushary";
|
|
47
|
+
const siblingEntry = process.argv[1] ? join(dirname(process.argv[1]), "pushary.js") : "";
|
|
48
|
+
const spawnDirect = siblingEntry.length > 0 && existsSync(siblingEntry);
|
|
49
|
+
let stopped = false;
|
|
50
|
+
let idleTicks = 0;
|
|
51
|
+
let errorStreak = 0;
|
|
52
|
+
const spawnTimestamps = [];
|
|
53
|
+
let timer;
|
|
54
|
+
let inflight;
|
|
55
|
+
const launch = (req) => {
|
|
56
|
+
spawnTimestamps.push(Date.now());
|
|
57
|
+
const cwd = req.cwd && existsSync(req.cwd) ? req.cwd : process.cwd();
|
|
58
|
+
const args = ["claude", "--remote", "-p", req.prompt];
|
|
59
|
+
try {
|
|
60
|
+
const child = spawnDirect ? spawn(process.execPath, [siblingEntry, ...args], { cwd, detached: true, stdio: "ignore", env: process.env }) : spawnClaude(pusharyBin, args, { cwd, detached: true, stdio: "ignore", env: process.env });
|
|
61
|
+
child.on("error", (err) => stderr(`[pushary] could not launch session: ${err.message}
|
|
62
|
+
`));
|
|
63
|
+
child.unref();
|
|
64
|
+
stderr(`[pushary] launched a phone-requested session in ${basename(cwd)}
|
|
65
|
+
`);
|
|
66
|
+
} catch (err) {
|
|
67
|
+
stderr(`[pushary] could not launch session: ${err instanceof Error ? err.message : String(err)}
|
|
68
|
+
`);
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
const drain = async (signal) => {
|
|
72
|
+
const res = await fetch(`${baseUrl}${DRAIN_PATH}`, {
|
|
73
|
+
method: "POST",
|
|
74
|
+
headers: { "Content-Type": "application/json", Authorization: `Bearer ${apiKey}` },
|
|
75
|
+
body: JSON.stringify({ machineId }),
|
|
76
|
+
signal
|
|
77
|
+
});
|
|
78
|
+
if (!res.ok) throw new Error(`drain ${res.status}`);
|
|
79
|
+
const data = await res.json();
|
|
80
|
+
const spawn2 = data.spawn;
|
|
81
|
+
return spawn2 && typeof spawn2.prompt === "string" && typeof spawn2.id === "string" ? spawn2 : null;
|
|
82
|
+
};
|
|
83
|
+
const nextDelay = () => {
|
|
84
|
+
if (errorStreak > 0) return Math.min(SLOW_POLL_MS * 2 ** (errorStreak - 1), MAX_ERROR_BACKOFF_MS);
|
|
85
|
+
return idleTicks >= FAST_DECAY_TICKS ? SLOW_POLL_MS : FAST_POLL_MS;
|
|
86
|
+
};
|
|
87
|
+
const jitter = (ms) => Math.max(1, Math.round(ms * (0.85 + Math.random() * 0.3)));
|
|
88
|
+
const tick = async () => {
|
|
89
|
+
if (stopped) return;
|
|
90
|
+
if (!spawnRateAllows(spawnTimestamps, Date.now())) {
|
|
91
|
+
timer = setTimeout(tick, jitter(nextDelay()));
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
const controller = new AbortController();
|
|
95
|
+
inflight = controller;
|
|
96
|
+
const timeout = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
|
|
97
|
+
try {
|
|
98
|
+
const req = await drain(controller.signal);
|
|
99
|
+
errorStreak = 0;
|
|
100
|
+
if (req) {
|
|
101
|
+
idleTicks = 0;
|
|
102
|
+
launch(req);
|
|
103
|
+
} else {
|
|
104
|
+
idleTicks++;
|
|
105
|
+
}
|
|
106
|
+
} catch {
|
|
107
|
+
errorStreak = Math.min(errorStreak + 1, MAX_ERROR_STREAK);
|
|
108
|
+
} finally {
|
|
109
|
+
clearTimeout(timeout);
|
|
110
|
+
inflight = void 0;
|
|
111
|
+
}
|
|
112
|
+
if (!stopped) timer = setTimeout(tick, jitter(nextDelay()));
|
|
113
|
+
};
|
|
114
|
+
const teardown = () => {
|
|
115
|
+
if (stopped) return;
|
|
116
|
+
stopped = true;
|
|
117
|
+
if (timer) clearTimeout(timer);
|
|
118
|
+
inflight?.abort();
|
|
119
|
+
};
|
|
120
|
+
stderr(
|
|
121
|
+
`[pushary] daemon online \u2014 this machine (${machineId}) can now be sent a new session from your phone. Ctrl-C to stop.
|
|
122
|
+
`
|
|
123
|
+
);
|
|
124
|
+
timer = setTimeout(tick, FAST_POLL_MS);
|
|
125
|
+
const signals = ["SIGINT", "SIGTERM", "SIGHUP"];
|
|
126
|
+
await new Promise((resolve) => {
|
|
127
|
+
const onSignal = () => {
|
|
128
|
+
teardown();
|
|
129
|
+
resolve();
|
|
130
|
+
};
|
|
131
|
+
for (const signal of signals) process.on(signal, onSignal);
|
|
132
|
+
});
|
|
133
|
+
return 0;
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
// bin/pushary-daemon.ts
|
|
137
|
+
runSpawnDaemon().then((code) => process.exit(code)).catch((err) => {
|
|
138
|
+
process.stderr.write(
|
|
139
|
+
`[pushary] daemon error: ${err instanceof Error ? err.message : String(err)}
|
|
140
|
+
`
|
|
141
|
+
);
|
|
142
|
+
process.exit(1);
|
|
143
|
+
});
|
|
@@ -342,6 +342,45 @@ var main = async () => {
|
|
|
342
342
|
}
|
|
343
343
|
}
|
|
344
344
|
console.log();
|
|
345
|
+
console.log(` ${dim("Remote control")}`);
|
|
346
|
+
const aliasRc = SHELL_FILES.find((f) => {
|
|
347
|
+
try {
|
|
348
|
+
return /alias\s+claude=['"]?pushary claude/.test(readFileSync(f, "utf-8"));
|
|
349
|
+
} catch {
|
|
350
|
+
return false;
|
|
351
|
+
}
|
|
352
|
+
});
|
|
353
|
+
if (aliasRc) {
|
|
354
|
+
if (globalVersion) {
|
|
355
|
+
check(true, "claude alias \u2192 pushary claude", `set in ${aliasRc.split("/").pop()}`);
|
|
356
|
+
} else {
|
|
357
|
+
check(false, "claude alias \u2192 pushary claude", "alias is set but pushary is not installed \u2014 run `unalias claude` (this shell) or reinstall");
|
|
358
|
+
}
|
|
359
|
+
} else {
|
|
360
|
+
console.log(` ${dim("\u2013")} claude alias not set ${dim("(optional \u2014 run `pushary claude` directly, or re-run setup to add it)")}`);
|
|
361
|
+
}
|
|
362
|
+
if (apiKey) {
|
|
363
|
+
try {
|
|
364
|
+
const modeRes = await fetch(`${getBaseUrl()}/api/mcp/mode`, {
|
|
365
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
366
|
+
signal: AbortSignal.timeout(8e3)
|
|
367
|
+
});
|
|
368
|
+
const relayUrl = modeRes.ok ? (await modeRes.json()).relayUrl : void 0;
|
|
369
|
+
if (typeof relayUrl === "string" && relayUrl.length > 0) {
|
|
370
|
+
const healthUrl = relayUrl.replace(/^wss:/, "https:").replace(/\/relay$/, "/health/ready");
|
|
371
|
+
try {
|
|
372
|
+
const h = await fetch(healthUrl, { signal: AbortSignal.timeout(6e3) });
|
|
373
|
+
console.log(h.ok ? ` ${pass} Relay reachable ${dim("(sub-second phone commands)")}` : ` ${dim("\u2013")} Relay advertised but returned ${h.status} ${dim("(commands still delivered via polling)")}`);
|
|
374
|
+
} catch {
|
|
375
|
+
console.log(` ${dim("\u2013")} Relay advertised but unreachable ${dim("(commands still delivered via polling)")}`);
|
|
376
|
+
}
|
|
377
|
+
} else {
|
|
378
|
+
console.log(` ${dim("\u2013")} Relay not enabled ${dim("(phone commands delivered via polling)")}`);
|
|
379
|
+
}
|
|
380
|
+
} catch {
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
console.log();
|
|
345
384
|
console.log(` ${dim("Connectivity")}`);
|
|
346
385
|
if (!apiKey) {
|
|
347
386
|
check(false, "MCP server reachable", "skipped \u2014 no API key");
|
|
@@ -12,7 +12,6 @@ import {
|
|
|
12
12
|
deriveToolTarget,
|
|
13
13
|
describeToolCall,
|
|
14
14
|
fetchModeState,
|
|
15
|
-
getMachineId,
|
|
16
15
|
getPolicy,
|
|
17
16
|
handlePostToolUse,
|
|
18
17
|
handleStop,
|
|
@@ -24,7 +23,10 @@ import {
|
|
|
24
23
|
savePendingQuestion,
|
|
25
24
|
sendNotification,
|
|
26
25
|
waitForAnswer
|
|
27
|
-
} from "../chunk-
|
|
26
|
+
} from "../chunk-O5MFSRWV.js";
|
|
27
|
+
import {
|
|
28
|
+
getMachineId
|
|
29
|
+
} from "../chunk-RN3NOEJF.js";
|
|
28
30
|
import {
|
|
29
31
|
isGatingMoment,
|
|
30
32
|
recordKeylessMoment
|
package/dist/bin/pushary-hook.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
handlePreToolUse
|
|
4
|
-
} from "../chunk-
|
|
4
|
+
} from "../chunk-OQMEGREX.js";
|
|
5
5
|
import "../chunk-7EW3USQF.js";
|
|
6
6
|
import "../chunk-KQYIHZ5E.js";
|
|
7
|
-
import "../chunk-
|
|
7
|
+
import "../chunk-O5MFSRWV.js";
|
|
8
|
+
import "../chunk-RN3NOEJF.js";
|
|
8
9
|
import "../chunk-R5AJNXZS.js";
|
|
9
10
|
import "../chunk-DWED7BS3.js";
|
|
10
11
|
import "../chunk-Z5PL3K7C.js";
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
handlePermissionDenied
|
|
4
|
-
} from "../chunk-
|
|
4
|
+
} from "../chunk-OQMEGREX.js";
|
|
5
5
|
import "../chunk-7EW3USQF.js";
|
|
6
6
|
import "../chunk-KQYIHZ5E.js";
|
|
7
|
-
import "../chunk-
|
|
7
|
+
import "../chunk-O5MFSRWV.js";
|
|
8
|
+
import "../chunk-RN3NOEJF.js";
|
|
8
9
|
import "../chunk-R5AJNXZS.js";
|
|
9
10
|
import "../chunk-DWED7BS3.js";
|
|
10
11
|
import "../chunk-Z5PL3K7C.js";
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
handlePermissionRequest
|
|
4
|
-
} from "../chunk-
|
|
4
|
+
} from "../chunk-OQMEGREX.js";
|
|
5
5
|
import "../chunk-7EW3USQF.js";
|
|
6
6
|
import "../chunk-KQYIHZ5E.js";
|
|
7
|
-
import "../chunk-
|
|
7
|
+
import "../chunk-O5MFSRWV.js";
|
|
8
|
+
import "../chunk-RN3NOEJF.js";
|
|
8
9
|
import "../chunk-R5AJNXZS.js";
|
|
9
10
|
import "../chunk-DWED7BS3.js";
|
|
10
11
|
import "../chunk-Z5PL3K7C.js";
|
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
installClaudeAlias,
|
|
4
|
+
resolveShellRc
|
|
5
|
+
} from "../chunk-BC3VCZ3E.js";
|
|
2
6
|
import {
|
|
3
7
|
GEMINI_HOOK_BINARY,
|
|
4
8
|
addCodexHookTrust,
|
|
@@ -24,7 +28,8 @@ import {
|
|
|
24
28
|
} from "../chunk-7EW3USQF.js";
|
|
25
29
|
import {
|
|
26
30
|
reportEvent
|
|
27
|
-
} from "../chunk-
|
|
31
|
+
} from "../chunk-O5MFSRWV.js";
|
|
32
|
+
import "../chunk-RN3NOEJF.js";
|
|
28
33
|
import "../chunk-DWED7BS3.js";
|
|
29
34
|
import {
|
|
30
35
|
isValidApiKey
|
|
@@ -876,6 +881,27 @@ var offerProjectInstructions = async (agents) => {
|
|
|
876
881
|
}
|
|
877
882
|
console.log(` ${dim2("Commit the file to share it. Teammates without a key fall back to the terminal.")}`);
|
|
878
883
|
};
|
|
884
|
+
var offerClaudeAlias = async () => {
|
|
885
|
+
if (!process.stdout.isTTY) return;
|
|
886
|
+
const rc = resolveShellRc();
|
|
887
|
+
console.log();
|
|
888
|
+
if (!rc) {
|
|
889
|
+
console.log(` ${dim2("Tip: alias")} ${cyan2("claude='pushary claude'")} ${dim2("in your shell to make plain")} ${cyan2("claude")} ${dim2("reachable from your phone.")}`);
|
|
890
|
+
return;
|
|
891
|
+
}
|
|
892
|
+
const wanted = await confirm({
|
|
893
|
+
message: `Make plain ${bold2("claude")} reachable from your phone by default? (adds an alias to ${basename(rc.path)}, reversible)`,
|
|
894
|
+
default: false
|
|
895
|
+
});
|
|
896
|
+
if (!wanted) return;
|
|
897
|
+
const result = installClaudeAlias();
|
|
898
|
+
if (result.installed) {
|
|
899
|
+
console.log(` ${check2} Aliased ${cyan2("claude")} \u2192 ${cyan2("pushary claude")} in ${dim2(result.path ?? rc.path)}`);
|
|
900
|
+
console.log(` ${dim2("Open a new terminal (or")} ${cyan2(`source ${rc.path}`)}${dim2(") to use it. Undo any time with")} ${cyan2("pushary clean")}${dim2(".")}`);
|
|
901
|
+
} else if (result.reason === "exists") {
|
|
902
|
+
console.log(` ${check2} ${dim2("The")} ${cyan2("claude")} ${dim2("alias is already set.")}`);
|
|
903
|
+
}
|
|
904
|
+
};
|
|
879
905
|
var main = async () => {
|
|
880
906
|
const version = getPackageVersion();
|
|
881
907
|
console.log();
|
|
@@ -974,6 +1000,9 @@ var main = async () => {
|
|
|
974
1000
|
console.log(` ${yellow2("!")} Failed: ${failed.join(", ")} ${dim2("(others completed successfully)")}`);
|
|
975
1001
|
}
|
|
976
1002
|
await offerProjectInstructions(agents);
|
|
1003
|
+
if (completed.includes("claude_code")) {
|
|
1004
|
+
await offerClaudeAlias();
|
|
1005
|
+
}
|
|
977
1006
|
if (completed.length > 0) {
|
|
978
1007
|
try {
|
|
979
1008
|
await reportEvent(
|
package/dist/bin/pushary.js
CHANGED
|
@@ -20,6 +20,8 @@ if (command === "setup") {
|
|
|
20
20
|
await import("./pushary-upgrade.js");
|
|
21
21
|
} else if (command === "claude") {
|
|
22
22
|
await import("./pushary-claude.js");
|
|
23
|
+
} else if (command === "daemon") {
|
|
24
|
+
await import("./pushary-daemon.js");
|
|
23
25
|
} else {
|
|
24
26
|
console.log(`
|
|
25
27
|
Pushary Agent Hooks
|
|
@@ -29,6 +31,9 @@ Commands:
|
|
|
29
31
|
claude Run Claude Code through Pushary \u2014 the native terminal, and reachable from
|
|
30
32
|
your phone: send an instruction and it drives even a fully idle agent
|
|
31
33
|
(press Ctrl-] to take the terminal back). Add --remote to start headless.
|
|
34
|
+
daemon Keep this machine reachable to START a new session from your phone, even
|
|
35
|
+
with nothing running. Leave it in a project dir; it launches a headless
|
|
36
|
+
'pushary claude --remote' when you send a prompt from the app.
|
|
32
37
|
doctor Verify your Pushary installation is working
|
|
33
38
|
clean Remove all Pushary configuration (--yes for non-interactive)
|
|
34
39
|
mode Switch approval mode (push_only, push_first, terminal_only)
|
|
@@ -51,5 +56,6 @@ Usage:
|
|
|
51
56
|
npx @pushary/agent-hooks@latest wait 45
|
|
52
57
|
pushary claude # native terminal, reachable from your phone when idle
|
|
53
58
|
pushary claude --remote -p "start the refactor" # start headless, drive entirely from your phone
|
|
59
|
+
pushary daemon # keep this machine ready to spawn a session from your phone
|
|
54
60
|
`);
|
|
55
61
|
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// src/shell-alias.ts
|
|
2
|
+
import { homedir } from "os";
|
|
3
|
+
import { existsSync, readFileSync, appendFileSync, writeFileSync } from "fs";
|
|
4
|
+
import { join } from "path";
|
|
5
|
+
var MARKER_START = "# >>> pushary >>>";
|
|
6
|
+
var MARKER_END = "# <<< pushary <<<";
|
|
7
|
+
var CANDIDATE_RCS = [".zshrc", ".zprofile", ".bashrc", ".bash_profile"];
|
|
8
|
+
var aliasBlock = () => `${MARKER_START}
|
|
9
|
+
# Make \`claude\` reachable from your phone when idle (runs it through Pushary).
|
|
10
|
+
# Remove this block, or run \`pushary clean\`, to undo.
|
|
11
|
+
alias claude='pushary claude'
|
|
12
|
+
${MARKER_END}
|
|
13
|
+
`;
|
|
14
|
+
var hasClaudeAlias = (content) => content.includes(MARKER_START) || /alias\s+claude=['"]?pushary claude/.test(content);
|
|
15
|
+
var stripPusharyBlock = (content) => {
|
|
16
|
+
const lines = content.split("\n");
|
|
17
|
+
const out = [];
|
|
18
|
+
let inBlock = false;
|
|
19
|
+
for (const line of lines) {
|
|
20
|
+
const trimmed = line.trim();
|
|
21
|
+
if (trimmed === MARKER_START) {
|
|
22
|
+
inBlock = true;
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
if (trimmed === MARKER_END) {
|
|
26
|
+
inBlock = false;
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
if (!inBlock) out.push(line);
|
|
30
|
+
}
|
|
31
|
+
return out.join("\n").replace(/\n{3,}/g, "\n\n");
|
|
32
|
+
};
|
|
33
|
+
var resolveShellRc = () => {
|
|
34
|
+
if (process.platform === "win32") return null;
|
|
35
|
+
const shell = (process.env.SHELL ?? "").split("/").pop() ?? "";
|
|
36
|
+
const home = homedir();
|
|
37
|
+
if (shell === "zsh") return { path: join(home, ".zshrc"), shell };
|
|
38
|
+
if (shell === "bash") {
|
|
39
|
+
const bashrc = join(home, ".bashrc");
|
|
40
|
+
const profile = join(home, ".bash_profile");
|
|
41
|
+
if (process.platform === "darwin") return { path: profile, shell };
|
|
42
|
+
return { path: existsSync(bashrc) ? bashrc : profile, shell };
|
|
43
|
+
}
|
|
44
|
+
return null;
|
|
45
|
+
};
|
|
46
|
+
var installClaudeAlias = () => {
|
|
47
|
+
const rc = resolveShellRc();
|
|
48
|
+
if (!rc) return { installed: false, reason: "unsupported-shell" };
|
|
49
|
+
let content = "";
|
|
50
|
+
try {
|
|
51
|
+
content = readFileSync(rc.path, "utf-8");
|
|
52
|
+
} catch {
|
|
53
|
+
}
|
|
54
|
+
if (hasClaudeAlias(content)) return { installed: false, path: rc.path, reason: "exists" };
|
|
55
|
+
const gap = content.length > 0 && !content.endsWith("\n") ? "\n\n" : content.length > 0 ? "\n" : "";
|
|
56
|
+
appendFileSync(rc.path, `${gap}${aliasBlock()}`, "utf-8");
|
|
57
|
+
return { installed: true, path: rc.path };
|
|
58
|
+
};
|
|
59
|
+
var removeClaudeAlias = () => {
|
|
60
|
+
const removed = [];
|
|
61
|
+
for (const name of CANDIDATE_RCS) {
|
|
62
|
+
const path = join(homedir(), name);
|
|
63
|
+
if (!existsSync(path)) continue;
|
|
64
|
+
let content;
|
|
65
|
+
try {
|
|
66
|
+
content = readFileSync(path, "utf-8");
|
|
67
|
+
} catch {
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
const stripped = stripPusharyBlock(content);
|
|
71
|
+
if (stripped !== content) {
|
|
72
|
+
try {
|
|
73
|
+
writeFileSync(path, stripped, "utf-8");
|
|
74
|
+
removed.push(path);
|
|
75
|
+
} catch {
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return removed;
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
export {
|
|
83
|
+
resolveShellRc,
|
|
84
|
+
installClaudeAlias,
|
|
85
|
+
removeClaudeAlias
|
|
86
|
+
};
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getMachineId
|
|
3
|
+
} from "./chunk-RN3NOEJF.js";
|
|
1
4
|
import {
|
|
2
5
|
callMcpTool,
|
|
3
6
|
withRetry
|
|
@@ -18,14 +21,8 @@ import {
|
|
|
18
21
|
getBaseUrl
|
|
19
22
|
} from "./chunk-NKXSILEW.js";
|
|
20
23
|
|
|
21
|
-
// src/identity.ts
|
|
22
|
-
import { createHash } from "crypto";
|
|
23
|
-
import { hostname } from "os";
|
|
24
|
-
var deriveMachineId = (host) => createHash("sha256").update(host).digest("hex").slice(0, 8);
|
|
25
|
-
var getMachineId = () => deriveMachineId(hostname());
|
|
26
|
-
|
|
27
24
|
// src/policy.ts
|
|
28
|
-
import { createHash
|
|
25
|
+
import { createHash } from "crypto";
|
|
29
26
|
import { existsSync, readFileSync, writeFileSync } from "fs";
|
|
30
27
|
import { join } from "path";
|
|
31
28
|
import { tmpdir } from "os";
|
|
@@ -50,7 +47,7 @@ var isWaitForAnswerResponse = (data) => {
|
|
|
50
47
|
// src/policy.ts
|
|
51
48
|
var CACHE_TTL_MS = 60 * 1e3;
|
|
52
49
|
var cacheFile = (apiKey) => {
|
|
53
|
-
const hash =
|
|
50
|
+
const hash = createHash("sha256").update(apiKey).digest("hex").slice(0, 12);
|
|
54
51
|
return join(tmpdir(), `pushary-policy-${hash}.json`);
|
|
55
52
|
};
|
|
56
53
|
var fetchPolicy = async (apiKey) => {
|
|
@@ -632,10 +629,10 @@ var preToolUseTimeoutDecision = (timeoutAction, denyReason = "No response within
|
|
|
632
629
|
// src/throttle.ts
|
|
633
630
|
import { join as join4 } from "path";
|
|
634
631
|
import { tmpdir as tmpdir4 } from "os";
|
|
635
|
-
import { createHash as
|
|
632
|
+
import { createHash as createHash2 } from "crypto";
|
|
636
633
|
import { existsSync as existsSync3, mkdirSync as mkdirSync3, statSync as statSync3, writeFileSync as writeFileSync4 } from "fs";
|
|
637
634
|
var THROTTLE_DIR = join4(tmpdir4(), "pushary-throttle");
|
|
638
|
-
var markerPath = (key) => join4(THROTTLE_DIR,
|
|
635
|
+
var markerPath = (key) => join4(THROTTLE_DIR, createHash2("sha256").update(key).digest("hex").slice(0, 16));
|
|
639
636
|
var throttlePass = (key, windowMs) => {
|
|
640
637
|
try {
|
|
641
638
|
const path = markerPath(key);
|
|
@@ -650,7 +647,7 @@ var throttlePass = (key, windowMs) => {
|
|
|
650
647
|
|
|
651
648
|
// src/events.ts
|
|
652
649
|
import { basename, join as join5 } from "path";
|
|
653
|
-
import { createHash as
|
|
650
|
+
import { createHash as createHash3 } from "crypto";
|
|
654
651
|
import { existsSync as existsSync4, mkdirSync as mkdirSync4, readFileSync as readFileSync3, statSync as statSync4, writeFileSync as writeFileSync5 } from "fs";
|
|
655
652
|
import { tmpdir as tmpdir5 } from "os";
|
|
656
653
|
var INTENT_DIR = join5(tmpdir5(), "pushary-intent");
|
|
@@ -727,7 +724,7 @@ var notifyLateAnswers = async (apiKey, late, agentName, sessionId) => {
|
|
|
727
724
|
var CLAUDE_CODE_AGENT = { type: "claude_code", label: "Claude Code" };
|
|
728
725
|
var POLICY_CACHE_TTL_MS = 5 * 60 * 1e3;
|
|
729
726
|
var readFreshCachedPolicy = (apiKey) => {
|
|
730
|
-
const hash =
|
|
727
|
+
const hash = createHash3("sha256").update(apiKey).digest("hex").slice(0, 12);
|
|
731
728
|
const path = join5(tmpdir5(), `pushary-policy-${hash}.json`);
|
|
732
729
|
if (!existsSync4(path)) return null;
|
|
733
730
|
const cached = JSON.parse(readFileSync3(path, "utf-8"));
|
|
@@ -1087,7 +1084,6 @@ var handleStopFailure = async (input, agent = CLAUDE_CODE_AGENT) => {
|
|
|
1087
1084
|
};
|
|
1088
1085
|
|
|
1089
1086
|
export {
|
|
1090
|
-
getMachineId,
|
|
1091
1087
|
getPolicy,
|
|
1092
1088
|
resolvePolicy,
|
|
1093
1089
|
fetchModeState,
|
|
@@ -15,7 +15,6 @@ import {
|
|
|
15
15
|
deriveToolTarget,
|
|
16
16
|
describeToolCall,
|
|
17
17
|
fetchModeState,
|
|
18
|
-
getMachineId,
|
|
19
18
|
getPolicy,
|
|
20
19
|
readLastPrompt,
|
|
21
20
|
readLastUserPrompt,
|
|
@@ -24,7 +23,10 @@ import {
|
|
|
24
23
|
sendNotification,
|
|
25
24
|
throttlePass,
|
|
26
25
|
waitForAnswer
|
|
27
|
-
} from "./chunk-
|
|
26
|
+
} from "./chunk-O5MFSRWV.js";
|
|
27
|
+
import {
|
|
28
|
+
getMachineId
|
|
29
|
+
} from "./chunk-RN3NOEJF.js";
|
|
28
30
|
import {
|
|
29
31
|
isGatingMoment,
|
|
30
32
|
recordKeylessMoment
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// src/identity.ts
|
|
2
|
+
import { createHash } from "crypto";
|
|
3
|
+
import { hostname } from "os";
|
|
4
|
+
var deriveMachineId = (host) => createHash("sha256").update(host).digest("hex").slice(0, 8);
|
|
5
|
+
var getMachineId = () => deriveMachineId(hostname());
|
|
6
|
+
|
|
7
|
+
export {
|
|
8
|
+
getMachineId
|
|
9
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// src/wrapper/spawnClaude.ts
|
|
2
|
+
import { spawn } from "child_process";
|
|
3
|
+
var needsShell = (binary, platform = process.platform) => platform === "win32" && /\.(cmd|bat)$/i.test(binary);
|
|
4
|
+
var spawnClaude = (binary, args, options) => {
|
|
5
|
+
if (needsShell(binary)) {
|
|
6
|
+
return spawn(`"${binary}"`, args, { ...options, shell: true });
|
|
7
|
+
}
|
|
8
|
+
return spawn(binary, args, options);
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export {
|
|
12
|
+
needsShell,
|
|
13
|
+
spawnClaude
|
|
14
|
+
};
|
package/dist/src/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
handlePreToolUse
|
|
3
|
-
} from "../chunk-
|
|
3
|
+
} from "../chunk-OQMEGREX.js";
|
|
4
4
|
import "../chunk-7EW3USQF.js";
|
|
5
5
|
import "../chunk-KQYIHZ5E.js";
|
|
6
6
|
import {
|
|
@@ -15,7 +15,8 @@ import {
|
|
|
15
15
|
reportEvent,
|
|
16
16
|
resolvePolicy,
|
|
17
17
|
waitForAnswer
|
|
18
|
-
} from "../chunk-
|
|
18
|
+
} from "../chunk-O5MFSRWV.js";
|
|
19
|
+
import "../chunk-RN3NOEJF.js";
|
|
19
20
|
import "../chunk-R5AJNXZS.js";
|
|
20
21
|
import "../chunk-DWED7BS3.js";
|
|
21
22
|
import "../chunk-Z5PL3K7C.js";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pushary/agent-hooks",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.49.1",
|
|
4
4
|
"description": "Permission hooks for AI coding agents: route tool approvals through Pushary push notifications",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pushary",
|
|
@@ -54,6 +54,7 @@
|
|
|
54
54
|
"pushary-permission-hook": "./dist/bin/pushary-permission-hook.js",
|
|
55
55
|
"pushary-permission-denied-hook": "./dist/bin/pushary-permission-denied-hook.js",
|
|
56
56
|
"pushary-claude": "./dist/bin/pushary-claude.js",
|
|
57
|
+
"pushary-daemon": "./dist/bin/pushary-daemon.js",
|
|
57
58
|
"pushary-codex": "./dist/bin/pushary-codex.js",
|
|
58
59
|
"pushary-codex-hook": "./dist/bin/pushary-codex-hook.js",
|
|
59
60
|
"pushary-gemini-hook": "./dist/bin/pushary-gemini-hook.js",
|