@pushary/agent-hooks 0.49.0 → 0.50.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.
@@ -16,12 +16,12 @@ import {
16
16
  reportEvent,
17
17
  resolvePolicy,
18
18
  waitForAnswer
19
- } from "../chunk-O5MFSRWV.js";
19
+ } from "../chunk-SCXPHMAS.js";
20
20
  import {
21
21
  getMachineId
22
22
  } from "../chunk-RN3NOEJF.js";
23
23
  import "../chunk-DWED7BS3.js";
24
- import "../chunk-Z5PL3K7C.js";
24
+ import "../chunk-WI3F6XNZ.js";
25
25
  import {
26
26
  getApiKey,
27
27
  getBaseUrl
@@ -343,7 +343,7 @@ var createRemoteApprover = (deps) => {
343
343
  const sessionId = deps.getSessionId?.();
344
344
  const modeState = await fetchModeState2(deps.apiKey, sessionId);
345
345
  if (modeState.kill) return deny("Stopped by user \u2014 this agent was halted from Pushary");
346
- const policyConfig = await getPolicy2(deps.apiKey, modeState.policyVersion);
346
+ const policyConfig = await getPolicy2(deps.apiKey, modeState.policyVersion, "claude_code");
347
347
  const policy = resolvePolicy2(policyConfig, toolName, modeState.mode, input);
348
348
  if (policy.timeoutSeconds === 0 && policy.timeoutAction === "approve") return allow(input);
349
349
  if (policy.timeoutSeconds === 0 && policy.timeoutAction === "deny") {
@@ -509,7 +509,7 @@ var startCommandPoller = (opts) => {
509
509
  schedule(nextDelay());
510
510
  };
511
511
  schedule(fast);
512
- opts.log?.("[pushary] wrapper command poller active (experimental)");
512
+ opts.log?.("[pushary] command poller active");
513
513
  return {
514
514
  stop() {
515
515
  stopped = true;
@@ -1130,6 +1130,11 @@ var runDualMode = async (binary, args2, startingMode) => {
1130
1130
  const agentName = `Claude Code - ${projectName}`;
1131
1131
  const machineId = getMachineId();
1132
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
+ };
1133
1138
  const { initialPrompt, resume, permissionMode } = parseRemoteArgs(args2);
1134
1139
  const userManagesSession = args2.some((a) => SESSION_FLAG_RE.test(a));
1135
1140
  let sessionId = resume;
@@ -1164,10 +1169,8 @@ var runDualMode = async (binary, args2, startingMode) => {
1164
1169
  agentType: "claude_code",
1165
1170
  getSessionId: () => sessionId,
1166
1171
  onCommand: (command) => onPhoneCommand(command),
1167
- onFatal: (message) => process.stderr.write(`[pushary] relay unavailable (${message}); using polling
1168
- `),
1169
- log: (message) => process.stderr.write(`${message}
1170
- `)
1172
+ onFatal: (message) => verboseLog(`[pushary] relay unavailable (${message}); using polling`),
1173
+ log: verboseLog
1171
1174
  });
1172
1175
  relay.start();
1173
1176
  }
@@ -1228,8 +1231,7 @@ var runDualMode = async (binary, args2, startingMode) => {
1228
1231
  activeLeg?.stop("kill");
1229
1232
  }
1230
1233
  },
1231
- log: (message) => process.stderr.write(`${message}
1232
- `)
1234
+ log: verboseLog
1233
1235
  });
1234
1236
  const signals = ["SIGINT", "SIGTERM", "SIGHUP"];
1235
1237
  const onSignal = () => {
@@ -1265,6 +1267,9 @@ var runDualMode = async (binary, args2, startingMode) => {
1265
1267
  };
1266
1268
  try {
1267
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
+ );
1268
1273
  void announcePresence("session_start", "Session ready \u2014 reachable from your phone");
1269
1274
  }
1270
1275
  if (startingMode === "remote" && initialPrompt) input.push(userMessage(initialPrompt));
@@ -1,20 +1,20 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  removeClaudeAlias
4
- } from "../chunk-OW2AJ74U.js";
4
+ } from "../chunk-BC3VCZ3E.js";
5
5
  import {
6
6
  removeCodexHooks,
7
7
  removeGeminiSettings,
8
8
  removeInstructionBlock
9
- } from "../chunk-SVVEC4QV.js";
9
+ } from "../chunk-FDRSMTJS.js";
10
10
  import {
11
11
  execNpm
12
12
  } from "../chunk-J7JWI3KU.js";
13
13
  import {
14
14
  removeClaudeMcpServers,
15
15
  removePusharySettings
16
- } from "../chunk-7EW3USQF.js";
17
- import "../chunk-Z5PL3K7C.js";
16
+ } from "../chunk-PUT2N5VA.js";
17
+ import "../chunk-WI3F6XNZ.js";
18
18
 
19
19
  // bin/pushary-clean.ts
20
20
  import { existsSync, readFileSync, writeFileSync, rmSync, readdirSync } from "fs";
@@ -277,6 +277,13 @@ var main = async () => {
277
277
  console.log();
278
278
  console.log(` ${green(bold("Clean complete."))}`);
279
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).")}`);
280
287
  console.log();
281
288
  };
282
289
  main();
@@ -31,21 +31,21 @@ import {
31
31
  toCodexWire,
32
32
  toPolicyLookup,
33
33
  waitForAnswer
34
- } from "../chunk-O5MFSRWV.js";
34
+ } from "../chunk-SCXPHMAS.js";
35
35
  import {
36
36
  getMachineId
37
37
  } from "../chunk-RN3NOEJF.js";
38
38
  import {
39
39
  isGatingMoment,
40
40
  recordKeylessMoment
41
- } from "../chunk-R5AJNXZS.js";
41
+ } from "../chunk-V42K3XQI.js";
42
42
  import "../chunk-DWED7BS3.js";
43
43
  import {
44
44
  DECISION_LINE_MAX,
45
45
  effectiveWaitSeconds,
46
46
  hookWaitClamped,
47
47
  hookWaitDeadline
48
- } from "../chunk-Z5PL3K7C.js";
48
+ } from "../chunk-WI3F6XNZ.js";
49
49
  import {
50
50
  getApiKey
51
51
  } from "../chunk-NKXSILEW.js";
@@ -170,7 +170,7 @@ var decidePermissionRequest = async (input) => {
170
170
  const modeState = await fetchModeState(apiKey, input.session_id);
171
171
  if (modeState.kill) return codexDeny(KILL_REASON);
172
172
  const lookup = toPolicyLookup(input.tool_name ?? "", input.tool_input ?? {}, input.cwd);
173
- const policy = await getPolicy(apiKey, modeState.policyVersion);
173
+ const policy = await getPolicy(apiKey, modeState.policyVersion, "codex");
174
174
  const toolPolicy = resolvePolicy(policy, lookup.tool, modeState.mode, lookup.input);
175
175
  if (toolPolicy.timeoutSeconds === 0 && toolPolicy.timeoutAction === "approve") return codexAllow();
176
176
  if (toolPolicy.timeoutSeconds === 0 && toolPolicy.timeoutAction === "deny") {
@@ -229,7 +229,7 @@ var decidePreToolUse = async (input) => {
229
229
  const modeState = await fetchModeState(apiKey, input.session_id);
230
230
  if (modeState.kill) return codexDeny(KILL_REASON);
231
231
  const lookup = toPolicyLookup(input.tool_name ?? "", input.tool_input ?? {}, input.cwd);
232
- const policy = await getPolicy(apiKey, modeState.policyVersion);
232
+ const policy = await getPolicy(apiKey, modeState.policyVersion, "codex");
233
233
  const toolPolicy = resolvePolicy(policy, lookup.tool, modeState.mode, lookup.input);
234
234
  if (toolPolicy.timeoutSeconds === 0 && toolPolicy.timeoutAction === "approve") return codexPass();
235
235
  if (toolPolicy.timeoutSeconds === 0 && toolPolicy.timeoutAction === "deny") {
@@ -3,12 +3,12 @@ import {
3
3
  askUser,
4
4
  reportEvent,
5
5
  waitForAnswer
6
- } from "../chunk-O5MFSRWV.js";
6
+ } from "../chunk-SCXPHMAS.js";
7
7
  import {
8
8
  getMachineId
9
9
  } from "../chunk-RN3NOEJF.js";
10
10
  import "../chunk-DWED7BS3.js";
11
- import "../chunk-Z5PL3K7C.js";
11
+ import "../chunk-WI3F6XNZ.js";
12
12
  import {
13
13
  getApiKey
14
14
  } from "../chunk-NKXSILEW.js";
@@ -14,8 +14,9 @@ import {
14
14
  } from "../chunk-NKXSILEW.js";
15
15
 
16
16
  // src/spawn-daemon.ts
17
+ import { spawn } from "child_process";
17
18
  import { existsSync } from "fs";
18
- import { basename } from "path";
19
+ import { basename, dirname, join } from "path";
19
20
  var DRAIN_PATH = "/api/agent/spawn/drain";
20
21
  var FAST_POLL_MS = 3e3;
21
22
  var SLOW_POLL_MS = 3e4;
@@ -43,6 +44,8 @@ var runSpawnDaemon = async () => {
43
44
  const machineId = getMachineId();
44
45
  const baseUrl = getBaseUrl();
45
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);
46
49
  let stopped = false;
47
50
  let idleTicks = 0;
48
51
  let errorStreak = 0;
@@ -50,20 +53,11 @@ var runSpawnDaemon = async () => {
50
53
  let timer;
51
54
  let inflight;
52
55
  const launch = (req) => {
53
- if (!spawnRateAllows(spawnTimestamps, Date.now())) {
54
- stderr(`[pushary] spawn rate limit reached; skipping a queued session
55
- `);
56
- return;
57
- }
58
56
  spawnTimestamps.push(Date.now());
59
57
  const cwd = req.cwd && existsSync(req.cwd) ? req.cwd : process.cwd();
58
+ const args = ["claude", "--remote", "-p", req.prompt];
60
59
  try {
61
- const child = spawnClaude(pusharyBin, ["claude", "--remote", "-p", req.prompt], {
62
- cwd,
63
- detached: true,
64
- stdio: "ignore",
65
- env: process.env
66
- });
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 });
67
61
  child.on("error", (err) => stderr(`[pushary] could not launch session: ${err.message}
68
62
  `));
69
63
  child.unref();
@@ -83,8 +77,8 @@ var runSpawnDaemon = async () => {
83
77
  });
84
78
  if (!res.ok) throw new Error(`drain ${res.status}`);
85
79
  const data = await res.json();
86
- const spawn = data.spawn;
87
- return spawn && typeof spawn.prompt === "string" && typeof spawn.id === "string" ? spawn : null;
80
+ const spawn2 = data.spawn;
81
+ return spawn2 && typeof spawn2.prompt === "string" && typeof spawn2.id === "string" ? spawn2 : null;
88
82
  };
89
83
  const nextDelay = () => {
90
84
  if (errorStreak > 0) return Math.min(SLOW_POLL_MS * 2 ** (errorStreak - 1), MAX_ERROR_BACKOFF_MS);
@@ -93,6 +87,10 @@ var runSpawnDaemon = async () => {
93
87
  const jitter = (ms) => Math.max(1, Math.round(ms * (0.85 + Math.random() * 0.3)));
94
88
  const tick = async () => {
95
89
  if (stopped) return;
90
+ if (!spawnRateAllows(spawnTimestamps, Date.now())) {
91
+ timer = setTimeout(tick, jitter(nextDelay()));
92
+ return;
93
+ }
96
94
  const controller = new AbortController();
97
95
  inflight = controller;
98
96
  const timeout = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
@@ -119,18 +117,18 @@ var runSpawnDaemon = async () => {
119
117
  if (timer) clearTimeout(timer);
120
118
  inflight?.abort();
121
119
  };
122
- const signals = ["SIGINT", "SIGTERM", "SIGHUP"];
123
- const onSignal = () => {
124
- teardown();
125
- process.exit(0);
126
- };
127
- for (const signal of signals) process.on(signal, onSignal);
128
120
  stderr(
129
121
  `[pushary] daemon online \u2014 this machine (${machineId}) can now be sent a new session from your phone. Ctrl-C to stop.
130
122
  `
131
123
  );
132
124
  timer = setTimeout(tick, FAST_POLL_MS);
133
- await new Promise(() => {
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);
134
132
  });
135
133
  return 0;
136
134
  };
@@ -11,18 +11,18 @@ import {
11
11
  missingGeminiHookEvents,
12
12
  readCodexMcpAuth,
13
13
  untrustedCodexHookEvents
14
- } from "../chunk-SVVEC4QV.js";
14
+ } from "../chunk-FDRSMTJS.js";
15
15
  import {
16
16
  execNpm
17
17
  } from "../chunk-J7JWI3KU.js";
18
18
  import {
19
19
  readLedgerSummary
20
- } from "../chunk-R5AJNXZS.js";
20
+ } from "../chunk-V42K3XQI.js";
21
21
  import {
22
22
  callMcpTool,
23
23
  sendMcpRequest
24
24
  } from "../chunk-DWED7BS3.js";
25
- import "../chunk-Z5PL3K7C.js";
25
+ import "../chunk-WI3F6XNZ.js";
26
26
  import {
27
27
  getBaseUrl
28
28
  } from "../chunk-NKXSILEW.js";
@@ -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");
@@ -23,21 +23,21 @@ import {
23
23
  savePendingQuestion,
24
24
  sendNotification,
25
25
  waitForAnswer
26
- } from "../chunk-O5MFSRWV.js";
26
+ } from "../chunk-SCXPHMAS.js";
27
27
  import {
28
28
  getMachineId
29
29
  } from "../chunk-RN3NOEJF.js";
30
30
  import {
31
31
  isGatingMoment,
32
32
  recordKeylessMoment
33
- } from "../chunk-R5AJNXZS.js";
33
+ } from "../chunk-V42K3XQI.js";
34
34
  import "../chunk-DWED7BS3.js";
35
35
  import {
36
36
  DECISION_LINE_MAX,
37
37
  effectiveWaitSeconds,
38
38
  hookWaitClamped,
39
39
  hookWaitDeadline
40
- } from "../chunk-Z5PL3K7C.js";
40
+ } from "../chunk-WI3F6XNZ.js";
41
41
  import {
42
42
  getApiKey
43
43
  } from "../chunk-NKXSILEW.js";
@@ -209,7 +209,7 @@ var decideBeforeTool = async (input) => {
209
209
  const modeState = await fetchModeState(apiKey, input.session_id);
210
210
  if (modeState.kill) return geminiDeny(KILL_REASON);
211
211
  const lookup = toGeminiPolicyLookup(input.tool_name ?? "", input.tool_input ?? {});
212
- const policy = await getPolicy(apiKey, modeState.policyVersion);
212
+ const policy = await getPolicy(apiKey, modeState.policyVersion, "gemini_cli");
213
213
  const toolPolicy = resolvePolicy(policy, lookup.tool, modeState.mode, lookup.input);
214
214
  if (toolPolicy.timeoutSeconds === 0 && toolPolicy.timeoutAction === "approve") return geminiAllow();
215
215
  if (toolPolicy.timeoutSeconds === 0 && toolPolicy.timeoutAction === "deny") {
@@ -1,14 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  handlePreToolUse
4
- } from "../chunk-OQMEGREX.js";
5
- import "../chunk-7EW3USQF.js";
4
+ } from "../chunk-M24KUFLW.js";
5
+ import "../chunk-PUT2N5VA.js";
6
6
  import "../chunk-KQYIHZ5E.js";
7
- import "../chunk-O5MFSRWV.js";
7
+ import "../chunk-SCXPHMAS.js";
8
8
  import "../chunk-RN3NOEJF.js";
9
- import "../chunk-R5AJNXZS.js";
9
+ import "../chunk-V42K3XQI.js";
10
10
  import "../chunk-DWED7BS3.js";
11
- import "../chunk-Z5PL3K7C.js";
11
+ import "../chunk-WI3F6XNZ.js";
12
12
  import "../chunk-NKXSILEW.js";
13
13
 
14
14
  // bin/pushary-hook.ts
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  handleNotification
4
- } from "../chunk-O5MFSRWV.js";
4
+ } from "../chunk-SCXPHMAS.js";
5
5
  import "../chunk-RN3NOEJF.js";
6
6
  import "../chunk-DWED7BS3.js";
7
- import "../chunk-Z5PL3K7C.js";
7
+ import "../chunk-WI3F6XNZ.js";
8
8
  import "../chunk-NKXSILEW.js";
9
9
 
10
10
  // bin/pushary-notification-hook.ts
@@ -1,14 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  handlePermissionDenied
4
- } from "../chunk-OQMEGREX.js";
5
- import "../chunk-7EW3USQF.js";
4
+ } from "../chunk-M24KUFLW.js";
5
+ import "../chunk-PUT2N5VA.js";
6
6
  import "../chunk-KQYIHZ5E.js";
7
- import "../chunk-O5MFSRWV.js";
7
+ import "../chunk-SCXPHMAS.js";
8
8
  import "../chunk-RN3NOEJF.js";
9
- import "../chunk-R5AJNXZS.js";
9
+ import "../chunk-V42K3XQI.js";
10
10
  import "../chunk-DWED7BS3.js";
11
- import "../chunk-Z5PL3K7C.js";
11
+ import "../chunk-WI3F6XNZ.js";
12
12
  import "../chunk-NKXSILEW.js";
13
13
 
14
14
  // bin/pushary-permission-denied-hook.ts
@@ -1,14 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  handlePermissionRequest
4
- } from "../chunk-OQMEGREX.js";
5
- import "../chunk-7EW3USQF.js";
4
+ } from "../chunk-M24KUFLW.js";
5
+ import "../chunk-PUT2N5VA.js";
6
6
  import "../chunk-KQYIHZ5E.js";
7
- import "../chunk-O5MFSRWV.js";
7
+ import "../chunk-SCXPHMAS.js";
8
8
  import "../chunk-RN3NOEJF.js";
9
- import "../chunk-R5AJNXZS.js";
9
+ import "../chunk-V42K3XQI.js";
10
10
  import "../chunk-DWED7BS3.js";
11
- import "../chunk-Z5PL3K7C.js";
11
+ import "../chunk-WI3F6XNZ.js";
12
12
  import "../chunk-NKXSILEW.js";
13
13
 
14
14
  // bin/pushary-permission-hook.ts
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  handlePostToolUse
4
- } from "../chunk-O5MFSRWV.js";
4
+ } from "../chunk-SCXPHMAS.js";
5
5
  import "../chunk-RN3NOEJF.js";
6
6
  import "../chunk-DWED7BS3.js";
7
- import "../chunk-Z5PL3K7C.js";
7
+ import "../chunk-WI3F6XNZ.js";
8
8
  import "../chunk-NKXSILEW.js";
9
9
 
10
10
  // bin/pushary-post-hook.ts
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  handleUserPrompt
4
- } from "../chunk-O5MFSRWV.js";
4
+ } from "../chunk-SCXPHMAS.js";
5
5
  import "../chunk-RN3NOEJF.js";
6
6
  import "../chunk-DWED7BS3.js";
7
- import "../chunk-Z5PL3K7C.js";
7
+ import "../chunk-WI3F6XNZ.js";
8
8
  import "../chunk-NKXSILEW.js";
9
9
 
10
10
  // bin/pushary-prompt-hook.ts
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  handleSessionEnd
4
- } from "../chunk-O5MFSRWV.js";
4
+ } from "../chunk-SCXPHMAS.js";
5
5
  import "../chunk-RN3NOEJF.js";
6
6
  import "../chunk-DWED7BS3.js";
7
- import "../chunk-Z5PL3K7C.js";
7
+ import "../chunk-WI3F6XNZ.js";
8
8
  import "../chunk-NKXSILEW.js";
9
9
 
10
10
  // bin/pushary-session-end-hook.ts
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  handleSessionStart
4
- } from "../chunk-O5MFSRWV.js";
4
+ } from "../chunk-SCXPHMAS.js";
5
5
  import "../chunk-RN3NOEJF.js";
6
6
  import "../chunk-DWED7BS3.js";
7
- import "../chunk-Z5PL3K7C.js";
7
+ import "../chunk-WI3F6XNZ.js";
8
8
  import "../chunk-NKXSILEW.js";
9
9
 
10
10
  // bin/pushary-session-start-hook.ts
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  installClaudeAlias,
4
4
  resolveShellRc
5
- } from "../chunk-OW2AJ74U.js";
5
+ } from "../chunk-BC3VCZ3E.js";
6
6
  import {
7
7
  GEMINI_HOOK_BINARY,
8
8
  addCodexHookTrust,
@@ -13,7 +13,7 @@ import {
13
13
  renderAgentInstructions,
14
14
  renderProjectAgentInstructions,
15
15
  writeInstructionBlock
16
- } from "../chunk-SVVEC4QV.js";
16
+ } from "../chunk-FDRSMTJS.js";
17
17
  import {
18
18
  execNpm,
19
19
  npmErrorMessage,
@@ -25,15 +25,15 @@ import {
25
25
  addClaudeMcpServer,
26
26
  addPusharyHooks,
27
27
  addPusharyToolPermissions
28
- } from "../chunk-7EW3USQF.js";
28
+ } from "../chunk-PUT2N5VA.js";
29
29
  import {
30
30
  reportEvent
31
- } from "../chunk-O5MFSRWV.js";
31
+ } from "../chunk-SCXPHMAS.js";
32
32
  import "../chunk-RN3NOEJF.js";
33
33
  import "../chunk-DWED7BS3.js";
34
34
  import {
35
35
  isValidApiKey
36
- } from "../chunk-Z5PL3K7C.js";
36
+ } from "../chunk-WI3F6XNZ.js";
37
37
  import "../chunk-NKXSILEW.js";
38
38
 
39
39
  // bin/pushary-setup.ts
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  readLedgerSummary
4
- } from "../chunk-R5AJNXZS.js";
5
- import "../chunk-Z5PL3K7C.js";
4
+ } from "../chunk-V42K3XQI.js";
5
+ import "../chunk-WI3F6XNZ.js";
6
6
  import {
7
7
  getApiKey
8
8
  } from "../chunk-NKXSILEW.js";
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  handleStop
4
- } from "../chunk-O5MFSRWV.js";
4
+ } from "../chunk-SCXPHMAS.js";
5
5
  import "../chunk-RN3NOEJF.js";
6
6
  import "../chunk-DWED7BS3.js";
7
- import "../chunk-Z5PL3K7C.js";
7
+ import "../chunk-WI3F6XNZ.js";
8
8
  import "../chunk-NKXSILEW.js";
9
9
 
10
10
  // bin/pushary-stop-hook.ts
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  handleStopFailure
4
- } from "../chunk-O5MFSRWV.js";
4
+ } from "../chunk-SCXPHMAS.js";
5
5
  import "../chunk-RN3NOEJF.js";
6
6
  import "../chunk-DWED7BS3.js";
7
- import "../chunk-Z5PL3K7C.js";
7
+ import "../chunk-WI3F6XNZ.js";
8
8
  import "../chunk-NKXSILEW.js";
9
9
 
10
10
  // bin/pushary-stopfailure-hook.ts
@@ -8,8 +8,8 @@ import {
8
8
  addClaudeMcpServer,
9
9
  addPusharyHooks,
10
10
  addPusharyToolPermissions
11
- } from "../chunk-7EW3USQF.js";
12
- import "../chunk-Z5PL3K7C.js";
11
+ } from "../chunk-PUT2N5VA.js";
12
+ import "../chunk-WI3F6XNZ.js";
13
13
  import {
14
14
  getApiKey
15
15
  } from "../chunk-NKXSILEW.js";
@@ -37,7 +37,9 @@ var resolveShellRc = () => {
37
37
  if (shell === "zsh") return { path: join(home, ".zshrc"), shell };
38
38
  if (shell === "bash") {
39
39
  const bashrc = join(home, ".bashrc");
40
- return { path: existsSync(bashrc) ? bashrc : join(home, ".bash_profile"), shell };
40
+ const profile = join(home, ".bash_profile");
41
+ if (process.platform === "darwin") return { path: profile, shell };
42
+ return { path: existsSync(bashrc) ? bashrc : profile, shell };
41
43
  }
42
44
  return null;
43
45
  };
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  HOOK_BUDGETS
3
- } from "./chunk-Z5PL3K7C.js";
3
+ } from "./chunk-WI3F6XNZ.js";
4
4
 
5
5
  // src/codex-config.ts
6
6
  import { createHash } from "crypto";
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  PRETOOLUSE_HANDLED_TOOLS
3
- } from "./chunk-7EW3USQF.js";
3
+ } from "./chunk-PUT2N5VA.js";
4
4
  import {
5
5
  denyReasonFrom,
6
6
  isDeferAnswer
@@ -23,19 +23,19 @@ import {
23
23
  sendNotification,
24
24
  throttlePass,
25
25
  waitForAnswer
26
- } from "./chunk-O5MFSRWV.js";
26
+ } from "./chunk-SCXPHMAS.js";
27
27
  import {
28
28
  getMachineId
29
29
  } from "./chunk-RN3NOEJF.js";
30
30
  import {
31
31
  isGatingMoment,
32
32
  recordKeylessMoment
33
- } from "./chunk-R5AJNXZS.js";
33
+ } from "./chunk-V42K3XQI.js";
34
34
  import {
35
35
  effectiveWaitSeconds,
36
36
  hookWaitClamped,
37
37
  hookWaitDeadline
38
- } from "./chunk-Z5PL3K7C.js";
38
+ } from "./chunk-WI3F6XNZ.js";
39
39
  import {
40
40
  getApiKey
41
41
  } from "./chunk-NKXSILEW.js";
@@ -322,7 +322,7 @@ var handlePreToolUse = async (input) => {
322
322
  }
323
323
  try {
324
324
  const modeState = await fetchModeState(apiKey, input.session_id);
325
- const policy = await getPolicy(apiKey, modeState.policyVersion);
325
+ const policy = await getPolicy(apiKey, modeState.policyVersion, "claude_code");
326
326
  if (modeState.kill) {
327
327
  return deny("Stopped by user \u2014 this agent was halted from Pushary");
328
328
  }
@@ -374,7 +374,7 @@ var handlePermissionRequest = async (input) => {
374
374
  }
375
375
  try {
376
376
  const modeState = await fetchModeState(apiKey, input.session_id);
377
- const policy = await getPolicy(apiKey, modeState.policyVersion);
377
+ const policy = await getPolicy(apiKey, modeState.policyVersion, "claude_code");
378
378
  if (modeState.kill) return permReqDeny("Stopped by user \u2014 this agent was halted from Pushary");
379
379
  const toolPolicy = resolvePolicy(policy, input.tool_name, modeState.mode, input.tool_input);
380
380
  if (toolPolicy.timeoutSeconds === 0 && toolPolicy.timeoutAction === "approve") return permReqAllow();
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  HOOK_BUDGETS
3
- } from "./chunk-Z5PL3K7C.js";
3
+ } from "./chunk-WI3F6XNZ.js";
4
4
 
5
5
  // src/claude-config.ts
6
6
  import { join } from "path";
@@ -8,6 +8,7 @@ import {
8
8
  import {
9
9
  ACTION_BODY_MAX,
10
10
  DECISION_LINE_MAX,
11
+ classifyDecisionRisk,
11
12
  extractPolicyArg,
12
13
  isApprovalMode,
13
14
  isSafeReadOnlyCommand,
@@ -15,7 +16,7 @@ import {
15
16
  matchToolPattern,
16
17
  redactSecrets,
17
18
  redactSecretsDeep
18
- } from "./chunk-Z5PL3K7C.js";
19
+ } from "./chunk-WI3F6XNZ.js";
19
20
  import {
20
21
  getApiKey,
21
22
  getBaseUrl
@@ -46,14 +47,15 @@ var isWaitForAnswerResponse = (data) => {
46
47
 
47
48
  // src/policy.ts
48
49
  var CACHE_TTL_MS = 60 * 1e3;
49
- var cacheFile = (apiKey) => {
50
- const hash = createHash("sha256").update(apiKey).digest("hex").slice(0, 12);
50
+ var cacheFile = (apiKey, agent) => {
51
+ const hash = createHash("sha256").update(agent ? `${apiKey}:${agent}` : apiKey).digest("hex").slice(0, 12);
51
52
  return join(tmpdir(), `pushary-policy-${hash}.json`);
52
53
  };
53
- var fetchPolicy = async (apiKey) => {
54
+ var fetchPolicy = async (apiKey, agent) => {
54
55
  return withRetry(async () => {
55
56
  const baseUrl = getBaseUrl();
56
- const response = await fetch(`${baseUrl}/api/mcp/policy`, {
57
+ const url = agent ? `${baseUrl}/api/mcp/policy?agent=${encodeURIComponent(agent)}` : `${baseUrl}/api/mcp/policy`;
58
+ const response = await fetch(url, {
57
59
  headers: { "Authorization": `Bearer ${apiKey}` },
58
60
  signal: AbortSignal.timeout(1e4)
59
61
  });
@@ -65,8 +67,8 @@ var fetchPolicy = async (apiKey) => {
65
67
  return raw;
66
68
  }, { maxAttempts: 2 });
67
69
  };
68
- var getPolicy = async (apiKey, expectedVersion) => {
69
- const path = cacheFile(apiKey);
70
+ var getPolicy = async (apiKey, expectedVersion, agent) => {
71
+ const path = cacheFile(apiKey, agent);
70
72
  let staleCache = null;
71
73
  if (existsSync(path)) {
72
74
  try {
@@ -83,7 +85,7 @@ var getPolicy = async (apiKey, expectedVersion) => {
83
85
  }
84
86
  }
85
87
  try {
86
- const policy = await fetchPolicy(apiKey);
88
+ const policy = await fetchPolicy(apiKey, agent);
87
89
  try {
88
90
  writeFileSync(path, JSON.stringify({ ...policy, _cachedAt: Date.now(), _policyVersion: expectedVersion ?? null }), "utf-8");
89
91
  } catch {
@@ -141,6 +143,16 @@ var resolvePolicy = (config, toolName, modeOverride, toolInput) => {
141
143
  if (!governedBySpecificRule && toolName === "Bash" && typeof arg === "string" && isSafeReadOnlyCommand(arg)) {
142
144
  base = autoApprove(base.tool);
143
145
  }
146
+ const wouldAutoApprove = base.timeoutSeconds === 0 && base.timeoutAction === "approve";
147
+ if (!governedBySpecificRule && wouldAutoApprove && toolName === "Bash" && typeof arg === "string" && classifyDecisionRisk(toolName, arg) === "destructive") {
148
+ base = {
149
+ tool: base.tool,
150
+ timeoutSeconds: config.defaultTimeoutSeconds,
151
+ timeoutAction: "wait",
152
+ mode: "push_first",
153
+ pushFirstSeconds: config.defaultPushFirstSeconds ?? 20
154
+ };
155
+ }
144
156
  const effectiveOverride = modeOverride ?? config.modeOverride;
145
157
  if (effectiveOverride) {
146
158
  return { ...base, mode: effectiveOverride };
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  isSafeReadOnlyCommand
3
- } from "./chunk-Z5PL3K7C.js";
3
+ } from "./chunk-WI3F6XNZ.js";
4
4
 
5
5
  // src/ledger.ts
6
6
  import { appendFileSync, existsSync, mkdirSync, readFileSync, renameSync, rmSync, statSync } from "fs";
@@ -279,6 +279,31 @@ var API_KEY_PATTERN = /^pk_[a-f0-9]+\.[a-f0-9]+$/;
279
279
  var isValidApiKey = (value) => API_KEY_PATTERN.test(value);
280
280
  var ACTION_BODY_MAX = 4e3;
281
281
  var DECISION_LINE_MAX = 500;
282
+ var APPROVAL_FATIGUE_FLAG_PATTERNS = [
283
+ /\brm\s+-[a-z]*r/i,
284
+ /\brmdir\b/i,
285
+ /git\s+push[^\n]*(--force|--force-with-lease|\s-f\b)/i,
286
+ /git\s+reset\s+--hard/i,
287
+ /git\s+clean\s+-[a-z]*f/i,
288
+ /\bdrop\s+(table|database|schema)\b/i,
289
+ /\bdelete\s+from\b/i,
290
+ /\btruncate\b/i,
291
+ /\b(deploy|publish|release)\b/i,
292
+ /\bnpm\s+publish\b/i,
293
+ /\bsudo\b/i,
294
+ /chmod\s+-?R?\s*777/i,
295
+ /\bkubectl\s+delete\b/i,
296
+ /\bterraform\s+(apply|destroy)\b/i,
297
+ /\bdocker\s+system\s+prune\b/i
298
+ ];
299
+ var HIGH_CONSEQUENCE_PATTERNS = [
300
+ /(^|[\\/])\.env(\.|$|[\\/])/i,
301
+ /secrets?/i,
302
+ /credentials?/i,
303
+ /(^|[\\/])deploy/i,
304
+ /\.github[\\/]workflows/i,
305
+ /package\.json/i
306
+ ];
282
307
  var SECRET_REDACTION_RULES = [
283
308
  { pattern: /-----BEGIN[A-Z0-9 ]*PRIVATE KEY-----[\s\S]*?-----END[A-Z0-9 ]*PRIVATE KEY-----/g, replacement: "[redacted key]" },
284
309
  { pattern: /\bsk-[A-Za-z0-9_-]{16,}\b/g, replacement: "[redacted]" },
@@ -303,6 +328,13 @@ var SECRET_REDACTION_RULES = [
303
328
  var HIGH_ENTROPY_RULE = { pattern: /[A-Za-z0-9+/]{40,}={0,2}/g, replacement: "[redacted]" };
304
329
  var redactSecrets = (text) => SECRET_REDACTION_RULES.reduce((acc, rule) => acc.replace(rule.pattern, rule.replacement), text);
305
330
  var redactSecretsDeep = (text) => redactSecrets(text).replace(HIGH_ENTROPY_RULE.pattern, HIGH_ENTROPY_RULE.replacement);
331
+ var classifyDecisionRisk = (toolName, toolTarget) => {
332
+ const parts = [toolName, toolTarget ?? ""];
333
+ const matchesAny = (patterns) => parts.some((part) => patterns.some((re) => re.test(part)));
334
+ if (matchesAny(APPROVAL_FATIGUE_FLAG_PATTERNS)) return "destructive";
335
+ if (matchesAny(HIGH_CONSEQUENCE_PATTERNS)) return "elevated";
336
+ return "standard";
337
+ };
306
338
 
307
339
  export {
308
340
  isApprovalMode,
@@ -318,5 +350,6 @@ export {
318
350
  ACTION_BODY_MAX,
319
351
  DECISION_LINE_MAX,
320
352
  redactSecrets,
321
- redactSecretsDeep
353
+ redactSecretsDeep,
354
+ classifyDecisionRisk
322
355
  };
@@ -26,7 +26,7 @@ interface ReceiptMeta {
26
26
  ok: boolean;
27
27
  }
28
28
 
29
- declare const getPolicy: (apiKey: string, expectedVersion?: string | null) => Promise<PolicyConfig>;
29
+ declare const getPolicy: (apiKey: string, expectedVersion?: string | null, agent?: string) => Promise<PolicyConfig>;
30
30
  type AutoResolveOrigin = 'safe_readonly' | 'policy_timeout';
31
31
  declare const resolvePolicy: (config: PolicyConfig, toolName: string, modeOverride?: ApprovalMode | null, toolInput?: Record<string, unknown>) => ToolPolicy;
32
32
  interface ModeState {
package/dist/src/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  handlePreToolUse
3
- } from "../chunk-OQMEGREX.js";
4
- import "../chunk-7EW3USQF.js";
3
+ } from "../chunk-M24KUFLW.js";
4
+ import "../chunk-PUT2N5VA.js";
5
5
  import "../chunk-KQYIHZ5E.js";
6
6
  import {
7
7
  askUser,
@@ -15,11 +15,11 @@ import {
15
15
  reportEvent,
16
16
  resolvePolicy,
17
17
  waitForAnswer
18
- } from "../chunk-O5MFSRWV.js";
18
+ } from "../chunk-SCXPHMAS.js";
19
19
  import "../chunk-RN3NOEJF.js";
20
- import "../chunk-R5AJNXZS.js";
20
+ import "../chunk-V42K3XQI.js";
21
21
  import "../chunk-DWED7BS3.js";
22
- import "../chunk-Z5PL3K7C.js";
22
+ import "../chunk-WI3F6XNZ.js";
23
23
  import {
24
24
  getApiKey,
25
25
  getBaseUrl
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pushary/agent-hooks",
3
- "version": "0.49.0",
3
+ "version": "0.50.0",
4
4
  "description": "Permission hooks for AI coding agents: route tool approvals through Pushary push notifications",
5
5
  "keywords": [
6
6
  "pushary",
@@ -73,12 +73,13 @@
73
73
  "scripts": {
74
74
  "build": "node scripts/bundle-plugin.mjs && tsup",
75
75
  "dev": "tsup --watch",
76
- "test": "bun test src/api.test.ts && bun test src/claude-config.test.ts && bun test src/config.test.ts && bun test src/mcp-http.test.ts && bun test src/retry.test.ts && bun test src/usage.test.ts && bun test src/validate.test.ts && bun test src/policy.test.ts && bun test src/npm.test.ts && bun test src/identity.test.ts && bun test src/pending.test.ts && bun test src/events.test.ts && bun test src/describe.test.ts && bun test src/reapply.test.ts && bun test src/suggestions.test.ts && bun test src/safe-commands.test.ts && bun test src/hook.test.ts && bun test src/codex-adapter.test.ts && bun test src/codex-config.test.ts && bun test src/gemini-adapter.test.ts && bun test src/gemini-config.test.ts && bun test src/instruction-file.test.ts && bun test src/pairing.test.ts && bun test src/wait-ladder.test.ts && bun test src/ledger.test.ts && bun test src/wrapper/wrapper.test.ts && bun test src/wrapper/messageQueue.test.ts && bun test src/wrapper/approver.test.ts && bun test src/wrapper/remoteLoop.test.ts && bun test src/wrapper/spawnClaude.test.ts && bun test src/wrapper/wsProtocol.test.ts && bun test src/wrapper/relayClient.test.ts && bun test src/wrapper/modeLoop.test.ts && bun test src/wrapper/stdinHandoff.test.ts && bun test src/wrapper/localLeg.test.ts"
76
+ "test": "bun test src/api.test.ts && bun test src/claude-config.test.ts && bun test src/config.test.ts && bun test src/mcp-http.test.ts && bun test src/retry.test.ts && bun test src/usage.test.ts && bun test src/validate.test.ts && bun test src/policy.test.ts && bun test src/npm.test.ts && bun test src/identity.test.ts && bun test src/pending.test.ts && bun test src/events.test.ts && bun test src/describe.test.ts && bun test src/reapply.test.ts && bun test src/suggestions.test.ts && bun test src/safe-commands.test.ts && bun test src/hook.test.ts && bun test src/codex-adapter.test.ts && bun test src/codex-config.test.ts && bun test src/gemini-adapter.test.ts && bun test src/gemini-config.test.ts && bun test src/instruction-file.test.ts && bun test src/pairing.test.ts && bun test src/wait-ladder.test.ts && bun test src/ledger.test.ts && bun test src/wrapper/wrapper.test.ts && bun test src/wrapper/messageQueue.test.ts && bun test src/wrapper/approver.test.ts && bun test src/wrapper/remoteLoop.test.ts && bun test src/wrapper/spawnClaude.test.ts && bun test src/wrapper/wsProtocol.test.ts && bun test src/wrapper/relayClient.test.ts && bun test src/wrapper/modeLoop.test.ts && bun test src/wrapper/stdinHandoff.test.ts && bun test src/wrapper/localLeg.test.ts && bun test src/crypto.test.ts && bun test src/transcript-capture.test.ts"
77
77
  },
78
78
  "dependencies": {
79
79
  "@inquirer/prompts": "^8.4.2",
80
80
  "qrcode-terminal": "^0.12.0",
81
- "smol-toml": "^1.6.1"
81
+ "smol-toml": "^1.6.1",
82
+ "tweetnacl": "^1.0.3"
82
83
  },
83
84
  "devDependencies": {
84
85
  "@pushary/contracts": "workspace:*",