@pushary/agent-hooks 0.31.0 → 0.32.2
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/data/SKILL.md +6 -2
- package/dist/bin/pushary-claude.d.ts +1 -0
- package/dist/bin/pushary-claude.js +175 -0
- package/dist/bin/pushary-clean.js +2 -2
- package/dist/bin/pushary-codex-hook.js +5 -5
- package/dist/bin/pushary-codex.js +1 -1
- package/dist/bin/pushary-doctor.js +1 -1
- package/dist/bin/pushary-gemini-hook.js +5 -5
- package/dist/bin/pushary-hook.js +2 -2
- package/dist/bin/pushary-notification-hook.d.ts +1 -0
- package/dist/bin/pushary-notification-hook.js +24 -0
- package/dist/bin/pushary-post-hook.js +1 -1
- package/dist/bin/pushary-prompt-hook.js +1 -1
- package/dist/bin/pushary-setup.js +3 -3
- package/dist/bin/pushary-stop-hook.js +1 -1
- package/dist/bin/pushary.js +3 -0
- package/dist/{chunk-CY5YP34L.js → chunk-2AMV2BDM.js} +20 -10
- package/dist/{chunk-H3LQRYMW.js → chunk-M6YUEVRV.js} +14 -4
- package/dist/{chunk-IAOXM7X5.js → chunk-P7B3U6DR.js} +89 -32
- package/dist/{chunk-WUNVKFFY.js → chunk-SVVEC4QV.js} +1 -0
- package/dist/src/index.d.ts +4 -1
- package/dist/src/index.js +3 -3
- package/package.json +4 -2
package/data/SKILL.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: pushary
|
|
3
|
-
version: 0.4.
|
|
3
|
+
version: 0.4.2
|
|
4
4
|
description: Push notifications and human-in-the-loop for AI coding agents. Send alerts when tasks finish, ask questions (yes/no, multiple choice, or free text) via push, and get answers from the user's lock screen. Use these tools proactively — do not wait for the user to ask for notifications. Works with Cursor, Claude Code, Windsurf, Hermes, and any MCP client.
|
|
5
5
|
metadata:
|
|
6
6
|
hermes:
|
|
@@ -248,7 +248,7 @@ Cancel a pending question so it can no longer be answered. Use when the question
|
|
|
248
248
|
|
|
249
249
|
## Permission Gating (REQUIRED)
|
|
250
250
|
|
|
251
|
-
Before executing any of the following,
|
|
251
|
+
Before executing any of the following, make sure the user has approved it. Call `ask_user` with type "confirm" and wait for an explicit "yes" before you proceed:
|
|
252
252
|
|
|
253
253
|
- File deletion (`rm`, `unlink`, any destructive file operation)
|
|
254
254
|
- Database mutations (`DROP`, `DELETE`, `TRUNCATE`, migrations)
|
|
@@ -258,6 +258,10 @@ Before executing any of the following, you MUST call `ask_user` with type "confi
|
|
|
258
258
|
- Network configuration changes (firewall, DNS, proxy)
|
|
259
259
|
- Any command the user has flagged as dangerous
|
|
260
260
|
|
|
261
|
+
Honor the user's delivery mode: it decides WHERE approval is requested (phone, terminal, or awareness only), not WHETHER approval is required. In "Terminal" mode the user wants to approve at the keyboard, so do not force a phone push; a single terminal approval is enough.
|
|
262
|
+
|
|
263
|
+
If the Pushary hook is installed, it also gates these at the tool level and routes them by delivery mode. When it has already surfaced the approval for an action, one "yes" is enough, do not ask a second time for the same action.
|
|
264
|
+
|
|
261
265
|
If `ask_user` returns `answered: false`, do NOT execute the command. Send a notification that the operation was skipped due to no response.
|
|
262
266
|
|
|
263
267
|
This is not optional. Treat it as a hard constraint, not a suggestion.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
getApiKey
|
|
4
|
+
} from "../chunk-NKXSILEW.js";
|
|
5
|
+
|
|
6
|
+
// src/wrapper/claudeBinary.ts
|
|
7
|
+
import { statSync } from "fs";
|
|
8
|
+
import { join, delimiter } from "path";
|
|
9
|
+
var WRAPPER_ACTIVE_ENV = "PUSHARY_WRAPPER_ACTIVE";
|
|
10
|
+
var isFile = (path) => {
|
|
11
|
+
try {
|
|
12
|
+
return statSync(path).isFile();
|
|
13
|
+
} catch {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
var findClaudeBinary = () => {
|
|
18
|
+
const override = process.env.PUSHARY_CLAUDE_BIN?.trim();
|
|
19
|
+
if (override) return isFile(override) ? override : null;
|
|
20
|
+
const pathVar = process.env.PATH ?? "";
|
|
21
|
+
const exts = process.platform === "win32" ? [".cmd", ".exe", ".bat", ""] : [""];
|
|
22
|
+
for (const dir of pathVar.split(delimiter)) {
|
|
23
|
+
if (!dir) continue;
|
|
24
|
+
for (const ext of exts) {
|
|
25
|
+
const candidate = join(dir, `claude${ext}`);
|
|
26
|
+
if (isFile(candidate)) return candidate;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return null;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
// src/wrapper/localPassthrough.ts
|
|
33
|
+
import { spawn } from "child_process";
|
|
34
|
+
var SIGNAL_NUMBERS = {
|
|
35
|
+
SIGHUP: 1,
|
|
36
|
+
SIGINT: 2,
|
|
37
|
+
SIGQUIT: 3,
|
|
38
|
+
SIGKILL: 9,
|
|
39
|
+
SIGTERM: 15
|
|
40
|
+
};
|
|
41
|
+
var FORWARDED_SIGNALS = ["SIGINT", "SIGTERM", "SIGHUP"];
|
|
42
|
+
var runLocalPassthrough = (binary, args2) => {
|
|
43
|
+
return new Promise((resolve) => {
|
|
44
|
+
let child;
|
|
45
|
+
try {
|
|
46
|
+
child = spawn(binary, args2, {
|
|
47
|
+
stdio: "inherit",
|
|
48
|
+
env: { ...process.env, [WRAPPER_ACTIVE_ENV]: "1" }
|
|
49
|
+
});
|
|
50
|
+
} catch {
|
|
51
|
+
resolve(127);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
const forward = (signal) => {
|
|
55
|
+
try {
|
|
56
|
+
child.kill(signal);
|
|
57
|
+
} catch {
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
for (const signal of FORWARDED_SIGNALS) process.on(signal, forward);
|
|
61
|
+
const cleanup = () => {
|
|
62
|
+
for (const signal of FORWARDED_SIGNALS) process.off(signal, forward);
|
|
63
|
+
};
|
|
64
|
+
child.on("error", () => {
|
|
65
|
+
cleanup();
|
|
66
|
+
resolve(127);
|
|
67
|
+
});
|
|
68
|
+
child.on("exit", (code, signal) => {
|
|
69
|
+
cleanup();
|
|
70
|
+
if (typeof code === "number") resolve(code);
|
|
71
|
+
else if (signal) resolve(128 + (SIGNAL_NUMBERS[signal] ?? 0));
|
|
72
|
+
else resolve(0);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
// src/wrapper/commandPoller.ts
|
|
78
|
+
var FAST_POLL_MS = 2e3;
|
|
79
|
+
var SLOW_POLL_MS = 3e4;
|
|
80
|
+
var isEnabled = () => {
|
|
81
|
+
const flag = process.env.PUSHARY_WRAPPER_POLL;
|
|
82
|
+
return flag === "1" || flag === "true";
|
|
83
|
+
};
|
|
84
|
+
var startCommandPoller = (opts) => {
|
|
85
|
+
let stopped = false;
|
|
86
|
+
let timer;
|
|
87
|
+
if (!isEnabled()) return { stop() {
|
|
88
|
+
} };
|
|
89
|
+
try {
|
|
90
|
+
getApiKey();
|
|
91
|
+
} catch {
|
|
92
|
+
return { stop() {
|
|
93
|
+
} };
|
|
94
|
+
}
|
|
95
|
+
const schedule = (delayMs) => {
|
|
96
|
+
if (stopped) return;
|
|
97
|
+
timer = setTimeout(tick, delayMs);
|
|
98
|
+
};
|
|
99
|
+
const tick = async () => {
|
|
100
|
+
if (stopped) return;
|
|
101
|
+
let next = SLOW_POLL_MS;
|
|
102
|
+
try {
|
|
103
|
+
const command = await drainPendingCommand(opts.sessionId);
|
|
104
|
+
if (command) {
|
|
105
|
+
next = FAST_POLL_MS;
|
|
106
|
+
opts.onCommand(command);
|
|
107
|
+
}
|
|
108
|
+
} catch {
|
|
109
|
+
}
|
|
110
|
+
schedule(next);
|
|
111
|
+
};
|
|
112
|
+
opts.log?.("[pushary] wrapper command poller enabled (experimental)");
|
|
113
|
+
schedule(SLOW_POLL_MS);
|
|
114
|
+
return {
|
|
115
|
+
stop() {
|
|
116
|
+
stopped = true;
|
|
117
|
+
if (timer) clearTimeout(timer);
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
};
|
|
121
|
+
var drainPendingCommand = async (_sessionId) => {
|
|
122
|
+
return null;
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
// src/wrapper/remoteMode.ts
|
|
126
|
+
var runRemoteMode = async (_binary, _args) => {
|
|
127
|
+
return { implemented: false };
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
// src/wrapper/runClaudeWrapper.ts
|
|
131
|
+
var runClaudeWrapper = async (args2) => {
|
|
132
|
+
const binary = findClaudeBinary();
|
|
133
|
+
if (!binary) {
|
|
134
|
+
process.stderr.write(
|
|
135
|
+
"[pushary] Could not find the `claude` binary on your PATH. Install Claude Code, or set PUSHARY_CLAUDE_BIN to its full path.\n"
|
|
136
|
+
);
|
|
137
|
+
return 127;
|
|
138
|
+
}
|
|
139
|
+
const nested = process.env[WRAPPER_ACTIVE_ENV] === "1";
|
|
140
|
+
if (!nested && process.env.PUSHARY_WRAPPER_REMOTE === "1") {
|
|
141
|
+
try {
|
|
142
|
+
const remote = await runRemoteMode(binary, args2);
|
|
143
|
+
if (remote.implemented) return remote.exitCode ?? 0;
|
|
144
|
+
} catch {
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
const poller = nested ? { stop() {
|
|
148
|
+
} } : startCommandPoller({
|
|
149
|
+
onCommand: () => {
|
|
150
|
+
},
|
|
151
|
+
log: (m) => process.stderr.write(`${m}
|
|
152
|
+
`)
|
|
153
|
+
});
|
|
154
|
+
try {
|
|
155
|
+
return await runLocalPassthrough(binary, args2);
|
|
156
|
+
} finally {
|
|
157
|
+
poller.stop();
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
// src/wrapper/args.ts
|
|
162
|
+
var resolveClaudeArgs = (argv) => {
|
|
163
|
+
const rest = argv.slice(2);
|
|
164
|
+
return rest[0] === "claude" ? rest.slice(1) : rest;
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
// bin/pushary-claude.ts
|
|
168
|
+
var args = resolveClaudeArgs(process.argv);
|
|
169
|
+
runClaudeWrapper(args).then((code) => process.exit(code)).catch((err) => {
|
|
170
|
+
process.stderr.write(
|
|
171
|
+
`[pushary] wrapper error: ${err instanceof Error ? err.message : String(err)}
|
|
172
|
+
`
|
|
173
|
+
);
|
|
174
|
+
process.exit(1);
|
|
175
|
+
});
|
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
import {
|
|
3
3
|
removeClaudeMcpServers,
|
|
4
4
|
removePusharySettings
|
|
5
|
-
} from "../chunk-
|
|
5
|
+
} from "../chunk-M6YUEVRV.js";
|
|
6
6
|
import {
|
|
7
7
|
removeCodexHooks,
|
|
8
8
|
removeGeminiSettings,
|
|
9
9
|
removeInstructionBlock
|
|
10
|
-
} from "../chunk-
|
|
10
|
+
} from "../chunk-SVVEC4QV.js";
|
|
11
11
|
import {
|
|
12
12
|
execNpm
|
|
13
13
|
} from "../chunk-J7JWI3KU.js";
|
|
@@ -3,10 +3,6 @@ import {
|
|
|
3
3
|
denyReasonFrom,
|
|
4
4
|
isDeferAnswer
|
|
5
5
|
} from "../chunk-KQYIHZ5E.js";
|
|
6
|
-
import {
|
|
7
|
-
isGatingMoment,
|
|
8
|
-
recordKeylessMoment
|
|
9
|
-
} from "../chunk-R5AJNXZS.js";
|
|
10
6
|
import {
|
|
11
7
|
CODEX_AGENT,
|
|
12
8
|
DEFAULT_SESSION,
|
|
@@ -36,7 +32,11 @@ import {
|
|
|
36
32
|
toCodexWire,
|
|
37
33
|
toPolicyLookup,
|
|
38
34
|
waitForAnswer
|
|
39
|
-
} from "../chunk-
|
|
35
|
+
} from "../chunk-P7B3U6DR.js";
|
|
36
|
+
import {
|
|
37
|
+
isGatingMoment,
|
|
38
|
+
recordKeylessMoment
|
|
39
|
+
} from "../chunk-R5AJNXZS.js";
|
|
40
40
|
import "../chunk-DWED7BS3.js";
|
|
41
41
|
import {
|
|
42
42
|
DECISION_LINE_MAX,
|
|
@@ -3,10 +3,6 @@ import {
|
|
|
3
3
|
denyReasonFrom,
|
|
4
4
|
isDeferAnswer
|
|
5
5
|
} from "../chunk-KQYIHZ5E.js";
|
|
6
|
-
import {
|
|
7
|
-
isGatingMoment,
|
|
8
|
-
recordKeylessMoment
|
|
9
|
-
} from "../chunk-R5AJNXZS.js";
|
|
10
6
|
import {
|
|
11
7
|
DEFAULT_SESSION,
|
|
12
8
|
askUser,
|
|
@@ -28,7 +24,11 @@ import {
|
|
|
28
24
|
savePendingQuestion,
|
|
29
25
|
sendNotification,
|
|
30
26
|
waitForAnswer
|
|
31
|
-
} from "../chunk-
|
|
27
|
+
} from "../chunk-P7B3U6DR.js";
|
|
28
|
+
import {
|
|
29
|
+
isGatingMoment,
|
|
30
|
+
recordKeylessMoment
|
|
31
|
+
} from "../chunk-R5AJNXZS.js";
|
|
32
32
|
import "../chunk-DWED7BS3.js";
|
|
33
33
|
import {
|
|
34
34
|
DECISION_LINE_MAX,
|
package/dist/bin/pushary-hook.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
handlePreToolUse
|
|
4
|
-
} from "../chunk-
|
|
4
|
+
} from "../chunk-2AMV2BDM.js";
|
|
5
5
|
import "../chunk-KQYIHZ5E.js";
|
|
6
|
+
import "../chunk-P7B3U6DR.js";
|
|
6
7
|
import "../chunk-R5AJNXZS.js";
|
|
7
|
-
import "../chunk-IAOXM7X5.js";
|
|
8
8
|
import "../chunk-DWED7BS3.js";
|
|
9
9
|
import "../chunk-Z5PL3K7C.js";
|
|
10
10
|
import "../chunk-NKXSILEW.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
handleNotification
|
|
4
|
+
} from "../chunk-P7B3U6DR.js";
|
|
5
|
+
import "../chunk-DWED7BS3.js";
|
|
6
|
+
import "../chunk-Z5PL3K7C.js";
|
|
7
|
+
import "../chunk-NKXSILEW.js";
|
|
8
|
+
|
|
9
|
+
// bin/pushary-notification-hook.ts
|
|
10
|
+
var main = async () => {
|
|
11
|
+
let rawInput = "";
|
|
12
|
+
for await (const chunk of process.stdin) {
|
|
13
|
+
rawInput += chunk;
|
|
14
|
+
}
|
|
15
|
+
if (!rawInput.trim()) {
|
|
16
|
+
process.exit(0);
|
|
17
|
+
}
|
|
18
|
+
try {
|
|
19
|
+
const input = JSON.parse(rawInput);
|
|
20
|
+
await handleNotification(input);
|
|
21
|
+
} catch {
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
main();
|
|
@@ -3,7 +3,7 @@ import {
|
|
|
3
3
|
addClaudeMcpServer,
|
|
4
4
|
addPusharyHooks,
|
|
5
5
|
addPusharyToolPermissions
|
|
6
|
-
} from "../chunk-
|
|
6
|
+
} from "../chunk-M6YUEVRV.js";
|
|
7
7
|
import {
|
|
8
8
|
GEMINI_HOOK_BINARY,
|
|
9
9
|
addCodexHookTrust,
|
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
renderAgentInstructions,
|
|
15
15
|
renderProjectAgentInstructions,
|
|
16
16
|
writeInstructionBlock
|
|
17
|
-
} from "../chunk-
|
|
17
|
+
} from "../chunk-SVVEC4QV.js";
|
|
18
18
|
import {
|
|
19
19
|
execNpm,
|
|
20
20
|
npmErrorMessage,
|
|
@@ -24,7 +24,7 @@ import {
|
|
|
24
24
|
} from "../chunk-J7JWI3KU.js";
|
|
25
25
|
import {
|
|
26
26
|
reportEvent
|
|
27
|
-
} from "../chunk-
|
|
27
|
+
} from "../chunk-P7B3U6DR.js";
|
|
28
28
|
import "../chunk-DWED7BS3.js";
|
|
29
29
|
import {
|
|
30
30
|
isValidApiKey
|
package/dist/bin/pushary.js
CHANGED
|
@@ -18,12 +18,15 @@ if (command === "setup") {
|
|
|
18
18
|
await import("./pushary-stats.js");
|
|
19
19
|
} else if (command === "upgrade") {
|
|
20
20
|
await import("./pushary-upgrade.js");
|
|
21
|
+
} else if (command === "claude") {
|
|
22
|
+
await import("./pushary-claude.js");
|
|
21
23
|
} else {
|
|
22
24
|
console.log(`
|
|
23
25
|
Pushary Agent Hooks
|
|
24
26
|
|
|
25
27
|
Commands:
|
|
26
28
|
setup Configure Claude Code, Codex, Gemini CLI, Hermes, or Cursor with Pushary
|
|
29
|
+
claude Run Claude Code through Pushary (experimental wrapper; today a transparent passthrough)
|
|
27
30
|
doctor Verify your Pushary installation is working
|
|
28
31
|
clean Remove all Pushary configuration (--yes for non-interactive)
|
|
29
32
|
mode Switch approval mode (push_only, push_first, terminal_only)
|
|
@@ -2,10 +2,6 @@ import {
|
|
|
2
2
|
denyReasonFrom,
|
|
3
3
|
isDeferAnswer
|
|
4
4
|
} from "./chunk-KQYIHZ5E.js";
|
|
5
|
-
import {
|
|
6
|
-
isGatingMoment,
|
|
7
|
-
recordKeylessMoment
|
|
8
|
-
} from "./chunk-R5AJNXZS.js";
|
|
9
5
|
import {
|
|
10
6
|
DEFAULT_SESSION,
|
|
11
7
|
askUser,
|
|
@@ -24,7 +20,11 @@ import {
|
|
|
24
20
|
savePendingQuestion,
|
|
25
21
|
sendNotification,
|
|
26
22
|
waitForAnswer
|
|
27
|
-
} from "./chunk-
|
|
23
|
+
} from "./chunk-P7B3U6DR.js";
|
|
24
|
+
import {
|
|
25
|
+
isGatingMoment,
|
|
26
|
+
recordKeylessMoment
|
|
27
|
+
} from "./chunk-R5AJNXZS.js";
|
|
28
28
|
import {
|
|
29
29
|
effectiveWaitSeconds,
|
|
30
30
|
hookWaitClamped,
|
|
@@ -98,7 +98,7 @@ var handlePushOnly = async (apiKey, description, projectName, timeoutSeconds, ti
|
|
|
98
98
|
case "deny":
|
|
99
99
|
return deny("Push notification failed, denying per policy");
|
|
100
100
|
default:
|
|
101
|
-
return
|
|
101
|
+
return void 0;
|
|
102
102
|
}
|
|
103
103
|
}
|
|
104
104
|
if (result.suppressed) {
|
|
@@ -113,7 +113,7 @@ var handlePushOnly = async (apiKey, description, projectName, timeoutSeconds, ti
|
|
|
113
113
|
case "deny":
|
|
114
114
|
return deny("No device connected to approve on");
|
|
115
115
|
default:
|
|
116
|
-
return
|
|
116
|
+
return void 0;
|
|
117
117
|
}
|
|
118
118
|
}
|
|
119
119
|
const effectiveWait = effectiveWaitSeconds(timeoutAction, timeoutSeconds, "claude");
|
|
@@ -156,7 +156,7 @@ var handlePushFirst = async (apiKey, description, projectName, pushFirstSeconds,
|
|
|
156
156
|
...decision
|
|
157
157
|
});
|
|
158
158
|
} catch {
|
|
159
|
-
return
|
|
159
|
+
return void 0;
|
|
160
160
|
}
|
|
161
161
|
if (result.suppressed) {
|
|
162
162
|
await cancelQuestion(apiKey, result.correlationId).catch(() => {
|
|
@@ -164,7 +164,7 @@ var handlePushFirst = async (apiKey, description, projectName, pushFirstSeconds,
|
|
|
164
164
|
return ask("You are at the keyboard \u2014 approve here.");
|
|
165
165
|
}
|
|
166
166
|
if (result.noDevices) {
|
|
167
|
-
return
|
|
167
|
+
return void 0;
|
|
168
168
|
}
|
|
169
169
|
const deadline = hookWaitDeadline(START_MS, pushFirstSeconds, "claude", Date.now());
|
|
170
170
|
const answer = await pollForAnswer(apiKey, result.correlationId, deadline, 1500);
|
|
@@ -200,6 +200,12 @@ var keylessNoticeOnce = (sessionId) => {
|
|
|
200
200
|
} catch {
|
|
201
201
|
}
|
|
202
202
|
};
|
|
203
|
+
var DEFER_PERMISSION_MODES = /* @__PURE__ */ new Set(["bypassPermissions", "plan", "auto"]);
|
|
204
|
+
var shouldDeferToNativeMode = (permissionMode) => {
|
|
205
|
+
const flag = process.env.PUSHARY_RESPECT_PERMISSION_MODE;
|
|
206
|
+
if (flag !== "1" && flag !== "true") return false;
|
|
207
|
+
return typeof permissionMode === "string" && DEFER_PERMISSION_MODES.has(permissionMode);
|
|
208
|
+
};
|
|
203
209
|
var handleKeyless = (input) => {
|
|
204
210
|
try {
|
|
205
211
|
if (isGatingMoment(input.tool_name, input.tool_input ?? {})) {
|
|
@@ -216,6 +222,7 @@ var handleKeyless = (input) => {
|
|
|
216
222
|
return void 0;
|
|
217
223
|
};
|
|
218
224
|
var handlePreToolUse = async (input) => {
|
|
225
|
+
if (input.tool_name.startsWith("mcp__pushary__")) return void 0;
|
|
219
226
|
let apiKey;
|
|
220
227
|
try {
|
|
221
228
|
apiKey = getApiKey();
|
|
@@ -228,6 +235,9 @@ var handlePreToolUse = async (input) => {
|
|
|
228
235
|
if (modeState.kill) {
|
|
229
236
|
return deny("Stopped by user \u2014 this agent was halted from Pushary");
|
|
230
237
|
}
|
|
238
|
+
if (shouldDeferToNativeMode(input.permission_mode)) {
|
|
239
|
+
return void 0;
|
|
240
|
+
}
|
|
231
241
|
const toolPolicy = resolvePolicy(policy, input.tool_name, modeState.mode, input.tool_input);
|
|
232
242
|
if (toolPolicy.timeoutSeconds === 0 && toolPolicy.timeoutAction === "approve") {
|
|
233
243
|
return allow();
|
|
@@ -260,7 +270,7 @@ var handlePreToolUse = async (input) => {
|
|
|
260
270
|
return handlePushFirst(apiKey, description, projectName, toolPolicy.pushFirstSeconds, sessionId, machineId, input.tool_name, toolTarget, decision);
|
|
261
271
|
}
|
|
262
272
|
} catch {
|
|
263
|
-
return
|
|
273
|
+
return void 0;
|
|
264
274
|
}
|
|
265
275
|
};
|
|
266
276
|
|
|
@@ -20,7 +20,7 @@ var isPusharyHook = (entry) => {
|
|
|
20
20
|
if (!Array.isArray(hooks)) return false;
|
|
21
21
|
return hooks.some((hook) => {
|
|
22
22
|
const command = String(asRecord(hook)?.command ?? "");
|
|
23
|
-
return command.includes("pushary-hook") || command.includes("pushary-post-hook") || command.includes("pushary-stop-hook") || command.includes("pushary-prompt-hook");
|
|
23
|
+
return command.includes("pushary-hook") || command.includes("pushary-post-hook") || command.includes("pushary-stop-hook") || command.includes("pushary-prompt-hook") || command.includes("pushary-notification-hook");
|
|
24
24
|
});
|
|
25
25
|
};
|
|
26
26
|
var addClaudeMcpServer = (config, apiKey) => {
|
|
@@ -67,7 +67,7 @@ var addPusharyHooks = (settings, binDir) => {
|
|
|
67
67
|
const hooks = ensureRecord(settings, "hooks");
|
|
68
68
|
const preToolUse = (Array.isArray(hooks.PreToolUse) ? hooks.PreToolUse : []).filter((entry) => !isPusharyHook(entry));
|
|
69
69
|
preToolUse.push({
|
|
70
|
-
matcher: "Bash|Write|Edit",
|
|
70
|
+
matcher: "Bash|Write|Edit|NotebookEdit",
|
|
71
71
|
hooks: [{
|
|
72
72
|
type: "command",
|
|
73
73
|
command: resolve("pushary-hook"),
|
|
@@ -77,7 +77,7 @@ var addPusharyHooks = (settings, binDir) => {
|
|
|
77
77
|
hooks.PreToolUse = preToolUse;
|
|
78
78
|
const postToolUse = (Array.isArray(hooks.PostToolUse) ? hooks.PostToolUse : []).filter((entry) => !isPusharyHook(entry));
|
|
79
79
|
postToolUse.push({
|
|
80
|
-
matcher: "Bash|Write|Edit",
|
|
80
|
+
matcher: "Bash|Write|Edit|NotebookEdit",
|
|
81
81
|
hooks: [{
|
|
82
82
|
type: "command",
|
|
83
83
|
command: resolve("pushary-post-hook"),
|
|
@@ -103,6 +103,16 @@ var addPusharyHooks = (settings, binDir) => {
|
|
|
103
103
|
}]
|
|
104
104
|
});
|
|
105
105
|
hooks.UserPromptSubmit = userPromptSubmit;
|
|
106
|
+
const notification = (Array.isArray(hooks.Notification) ? hooks.Notification : []).filter((entry) => !isPusharyHook(entry));
|
|
107
|
+
notification.push({
|
|
108
|
+
matcher: "permission_prompt|idle_prompt|agent_needs_input|agent_completed",
|
|
109
|
+
hooks: [{
|
|
110
|
+
type: "command",
|
|
111
|
+
command: resolve("pushary-notification-hook"),
|
|
112
|
+
timeout: 10
|
|
113
|
+
}]
|
|
114
|
+
});
|
|
115
|
+
hooks.Notification = notification;
|
|
106
116
|
};
|
|
107
117
|
var removePusharySettings = (settings) => {
|
|
108
118
|
let changed = removeClaudeMcpServers(settings);
|
|
@@ -121,7 +131,7 @@ var removePusharySettings = (settings) => {
|
|
|
121
131
|
}
|
|
122
132
|
const hooks = asRecord(settings.hooks);
|
|
123
133
|
if (hooks) {
|
|
124
|
-
for (const key of ["PreToolUse", "PostToolUse", "Stop", "UserPromptSubmit"]) {
|
|
134
|
+
for (const key of ["PreToolUse", "PostToolUse", "Stop", "UserPromptSubmit", "Notification"]) {
|
|
125
135
|
const entries = hooks[key];
|
|
126
136
|
if (!Array.isArray(entries)) continue;
|
|
127
137
|
const filtered = entries.filter((entry) => !isPusharyHook(entry));
|
|
@@ -752,6 +752,15 @@ var deriveUsage = (transcriptPath, sessionId) => {
|
|
|
752
752
|
return void 0;
|
|
753
753
|
}
|
|
754
754
|
};
|
|
755
|
+
var PENDING_COMMAND_PREFIX = "The user sent a new instruction from their phone via Pushary:";
|
|
756
|
+
var mergeAdditionalContext = (existing, reported) => {
|
|
757
|
+
const pendingCommand = reported.status === "fulfilled" ? reported.value?.pendingCommand : void 0;
|
|
758
|
+
if (typeof pendingCommand !== "string" || pendingCommand.trim().length === 0) return existing;
|
|
759
|
+
const injected = `${PENDING_COMMAND_PREFIX} ${pendingCommand.trim()}`;
|
|
760
|
+
return existing ? `${existing}
|
|
761
|
+
|
|
762
|
+
${injected}` : injected;
|
|
763
|
+
};
|
|
755
764
|
var detectInstallMode = () => (process.argv[1] ?? "").includes("_npx") ? "npx" : "cli";
|
|
756
765
|
var reportEvent = async (event, options = {}) => {
|
|
757
766
|
const apiKey = getApiKey();
|
|
@@ -766,7 +775,14 @@ var reportEvent = async (event, options = {}) => {
|
|
|
766
775
|
body: JSON.stringify({
|
|
767
776
|
...event,
|
|
768
777
|
machineId: event.machineId ?? getMachineId(),
|
|
769
|
-
installMode: detectInstallMode()
|
|
778
|
+
installMode: detectInstallMode(),
|
|
779
|
+
// Advertise that this client reads `pendingCommand` back off the
|
|
780
|
+
// response, so the server may drain the phone-queued instruction on
|
|
781
|
+
// activity events (tool_complete/tool_error/user_prompt), not only Stop.
|
|
782
|
+
// Only Claude Code consumes additionalContext from these hooks; Codex and
|
|
783
|
+
// Gemini keep their existing session_end-only drain, so they must NOT
|
|
784
|
+
// advertise it or the server would pop-and-drop their queued command.
|
|
785
|
+
canDrainCommand: event.agentType === CLAUDE_CODE_AGENT.type
|
|
770
786
|
}),
|
|
771
787
|
signal: AbortSignal.timeout(options.timeoutMs ?? 1e4)
|
|
772
788
|
});
|
|
@@ -794,22 +810,21 @@ var handlePostToolUse = async (input, agent = CLAUDE_CODE_AGENT) => {
|
|
|
794
810
|
}
|
|
795
811
|
const decisionSource = deriveDecisionSource(lookup.tool, lookup.input, liveMode);
|
|
796
812
|
const beacon = decisionSource === "policy_auto" && throttlePass(`autodecision:${sessionKey}:${lookup.tool}`, AUTO_DECISION_WINDOW_MS) ? deriveAutoDecisionBeacon(lookup.tool, lookup.input, liveMode) : void 0;
|
|
797
|
-
const
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
];
|
|
813
|
+
const toolReport = reportEvent({
|
|
814
|
+
event: isError ? "tool_error" : "tool_complete",
|
|
815
|
+
agentType: agent.type,
|
|
816
|
+
agentName: `${agent.label} - ${projectName}`,
|
|
817
|
+
action,
|
|
818
|
+
sessionId: input.session_id,
|
|
819
|
+
error: isError ? String(input.tool_result?.error ?? input.tool_result?.stderr ?? "").slice(0, 500) : void 0,
|
|
820
|
+
decisionSource,
|
|
821
|
+
...beacon ? { decisionOrigin: beacon.decisionOrigin, toolName: beacon.toolName, toolTarget: beacon.toolTarget } : {},
|
|
822
|
+
meta: receiptsEnabled ? deriveReceiptMeta(lookup.tool, lookup.input, input.tool_result, input.cwd ?? process.cwd()) : void 0,
|
|
823
|
+
usage: deriveUsage(input.transcript_path, input.session_id)
|
|
824
|
+
}, { maxAttempts: 1 });
|
|
825
|
+
const extraReports = [];
|
|
811
826
|
if (throttlePass(`hookactive:${sessionKey}`, HOOK_ACTIVE_WINDOW_MS)) {
|
|
812
|
-
|
|
827
|
+
extraReports.push(
|
|
813
828
|
reportEvent(
|
|
814
829
|
{
|
|
815
830
|
event: "agent_hook_active",
|
|
@@ -821,7 +836,10 @@ var handlePostToolUse = async (input, agent = CLAUDE_CODE_AGENT) => {
|
|
|
821
836
|
)
|
|
822
837
|
);
|
|
823
838
|
}
|
|
824
|
-
await Promise.allSettled(
|
|
839
|
+
const [reported] = await Promise.allSettled([toolReport, ...extraReports]);
|
|
840
|
+
if (agent.type === CLAUDE_CODE_AGENT.type) {
|
|
841
|
+
additionalContext = mergeAdditionalContext(additionalContext, reported);
|
|
842
|
+
}
|
|
825
843
|
return additionalContext;
|
|
826
844
|
} catch {
|
|
827
845
|
return void 0;
|
|
@@ -841,32 +859,47 @@ var handleUserPrompt = async (input, agent = CLAUDE_CODE_AGENT) => {
|
|
|
841
859
|
additionalContext = buildLateAnswerContext(late);
|
|
842
860
|
for (const l of late) removePendingQuestion(sessionKey, l.correlationId);
|
|
843
861
|
}
|
|
844
|
-
await
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
862
|
+
const [reported] = await Promise.allSettled([
|
|
863
|
+
reportEvent({
|
|
864
|
+
event: "user_prompt",
|
|
865
|
+
agentType: agent.type,
|
|
866
|
+
agentName: `${agent.label} - ${projectName}`,
|
|
867
|
+
sessionId: input.session_id,
|
|
868
|
+
taskTitle
|
|
869
|
+
}, { maxAttempts: 1, timeoutMs: 800 })
|
|
870
|
+
]);
|
|
871
|
+
if (agent.type === CLAUDE_CODE_AGENT.type) {
|
|
872
|
+
additionalContext = mergeAdditionalContext(additionalContext, reported);
|
|
873
|
+
}
|
|
851
874
|
return additionalContext;
|
|
852
875
|
} catch {
|
|
853
876
|
return void 0;
|
|
854
877
|
}
|
|
855
878
|
};
|
|
879
|
+
var STOP_SUMMARY_MAX_LENGTH = 200;
|
|
880
|
+
var summarizeFinalMessage = (message) => {
|
|
881
|
+
if (typeof message !== "string") return "Session ended";
|
|
882
|
+
const collapsed = message.replace(/\s+/g, " ").trim();
|
|
883
|
+
if (!collapsed) return "Session ended";
|
|
884
|
+
return collapsed.slice(0, STOP_SUMMARY_MAX_LENGTH);
|
|
885
|
+
};
|
|
856
886
|
var handleStop = async (input, agent = CLAUDE_CODE_AGENT) => {
|
|
857
887
|
try {
|
|
858
888
|
const projectName = basename(input.cwd ?? process.cwd());
|
|
859
889
|
const sessionKey = input.session_id || DEFAULT_SESSION;
|
|
860
890
|
const late = await reconcilePendingQuestions(sessionKey);
|
|
861
891
|
const tasks = [
|
|
892
|
+
// maxAttempts:1 for the same reason as the activity-event drains: session_end
|
|
893
|
+
// always pops the command server-side (single-use LPOP), so a retry after a
|
|
894
|
+
// slow-but-successful response would pop a second command and drop the first.
|
|
862
895
|
reportEvent({
|
|
863
896
|
event: "session_end",
|
|
864
897
|
agentType: agent.type,
|
|
865
898
|
agentName: `${agent.label} - ${projectName}`,
|
|
866
|
-
action:
|
|
899
|
+
action: summarizeFinalMessage(input.last_assistant_message),
|
|
867
900
|
sessionId: input.session_id,
|
|
868
901
|
usage: deriveUsage(input.transcript_path, input.session_id)
|
|
869
|
-
})
|
|
902
|
+
}, { maxAttempts: 1 })
|
|
870
903
|
];
|
|
871
904
|
if (late.length > 0) {
|
|
872
905
|
tasks.push(notifyLateAnswers(getApiKey(), late, `${agent.label} - ${projectName}`, input.session_id));
|
|
@@ -886,16 +919,40 @@ var handleStop = async (input, agent = CLAUDE_CODE_AGENT) => {
|
|
|
886
919
|
return void 0;
|
|
887
920
|
}
|
|
888
921
|
};
|
|
889
|
-
var
|
|
922
|
+
var NOTIFICATION_PUSH_TYPES = /* @__PURE__ */ new Set(["idle_prompt", "agent_needs_input"]);
|
|
923
|
+
var NOTIFICATION_THROTTLE_MS = 60 * 1e3;
|
|
924
|
+
var handleNotification = async (input, agent = CLAUDE_CODE_AGENT) => {
|
|
890
925
|
try {
|
|
891
926
|
const projectName = basename(input.cwd ?? process.cwd());
|
|
927
|
+
const notifType = input.notification_type ?? input.type ?? "";
|
|
928
|
+
const sessionKey = input.session_id || DEFAULT_SESSION;
|
|
929
|
+
if (NOTIFICATION_PUSH_TYPES.has(notifType)) {
|
|
930
|
+
if (!throttlePass(`notify:${sessionKey}:${notifType}`, NOTIFICATION_THROTTLE_MS)) return;
|
|
931
|
+
let apiKey;
|
|
932
|
+
try {
|
|
933
|
+
apiKey = getApiKey();
|
|
934
|
+
} catch {
|
|
935
|
+
return;
|
|
936
|
+
}
|
|
937
|
+
const mode = await fetchModeState(apiKey, input.session_id).catch(() => null);
|
|
938
|
+
if (mode?.kill || mode?.mode === "terminal_only") return;
|
|
939
|
+
await sendNotification(apiKey, {
|
|
940
|
+
title: `${agent.label} is waiting`,
|
|
941
|
+
body: (input.message ?? "Your agent is waiting for you.").slice(0, 200),
|
|
942
|
+
agentName: `${agent.label} - ${projectName}`,
|
|
943
|
+
sessionId: input.session_id
|
|
944
|
+
}, 4e3).catch(() => {
|
|
945
|
+
});
|
|
946
|
+
return;
|
|
947
|
+
}
|
|
892
948
|
await reportEvent({
|
|
893
|
-
event:
|
|
894
|
-
agentType:
|
|
895
|
-
agentName:
|
|
896
|
-
action: input.
|
|
949
|
+
event: notifType === "error" ? "error" : "notification",
|
|
950
|
+
agentType: agent.type,
|
|
951
|
+
agentName: `${agent.label} - ${projectName}`,
|
|
952
|
+
action: input.message ?? input.title ?? "Notification",
|
|
897
953
|
sessionId: input.session_id,
|
|
898
|
-
error:
|
|
954
|
+
error: notifType === "error" ? input.message ?? input.title : void 0
|
|
955
|
+
}).catch(() => {
|
|
899
956
|
});
|
|
900
957
|
} catch {
|
|
901
958
|
}
|
|
@@ -251,6 +251,7 @@ You have Pushary MCP tools that reach the user on their phone. Use them proactiv
|
|
|
251
251
|
- When you are blocked, stuck, or hit an error you cannot resolve on your own, call \`send_notification\` so the user knows, and call \`ask_user\` if you need a decision to continue.
|
|
252
252
|
- When a task that took several steps finishes, call \`send_notification\` so the user knows it is done.
|
|
253
253
|
- Use \`cancel_question\` to retract a pending question once it is no longer needed.
|
|
254
|
+
- Respect the user's Pushary delivery mode: it decides where an approval is surfaced (phone, terminal, or awareness only), not whether it is required. In Terminal mode do not expect a phone answer; the approval waits in the terminal. If the Pushary hook already gated an action, a single approval is enough, do not ask again.
|
|
254
255
|
|
|
255
256
|
Pass \`agentName\` as "${label} - <project folder name>" so the user knows which session is asking. If a question times out with no answer, retry once with a longer timeout before falling back to asking in the terminal.`;
|
|
256
257
|
var renderAgentInstructions = (label) => instructionBody(label);
|
package/dist/src/index.d.ts
CHANGED
|
@@ -7,6 +7,7 @@ interface ToolInput {
|
|
|
7
7
|
session_id?: string;
|
|
8
8
|
cwd?: string;
|
|
9
9
|
transcript_path?: string;
|
|
10
|
+
permission_mode?: string;
|
|
10
11
|
}
|
|
11
12
|
interface HookOutput {
|
|
12
13
|
hookSpecificOutput: {
|
|
@@ -90,14 +91,16 @@ declare const handleStop: (input: {
|
|
|
90
91
|
session_id?: string;
|
|
91
92
|
stop_hook_active?: boolean;
|
|
92
93
|
transcript_path?: string;
|
|
94
|
+
last_assistant_message?: string;
|
|
93
95
|
}, agent?: AgentIdentity) => Promise<StopHookOutput | undefined>;
|
|
94
96
|
declare const handleNotification: (input: {
|
|
95
97
|
message?: string;
|
|
96
98
|
title?: string;
|
|
97
99
|
type?: string;
|
|
100
|
+
notification_type?: string;
|
|
98
101
|
cwd?: string;
|
|
99
102
|
session_id?: string;
|
|
100
|
-
}) => Promise<void>;
|
|
103
|
+
}, agent?: AgentIdentity) => Promise<void>;
|
|
101
104
|
|
|
102
105
|
interface AskUserParams {
|
|
103
106
|
question: string;
|
package/dist/src/index.js
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
handlePreToolUse
|
|
3
|
-
} from "../chunk-
|
|
3
|
+
} from "../chunk-2AMV2BDM.js";
|
|
4
4
|
import "../chunk-KQYIHZ5E.js";
|
|
5
|
-
import "../chunk-R5AJNXZS.js";
|
|
6
5
|
import {
|
|
7
6
|
askUser,
|
|
8
7
|
cancelQuestion,
|
|
@@ -15,7 +14,8 @@ import {
|
|
|
15
14
|
reportEvent,
|
|
16
15
|
resolvePolicy,
|
|
17
16
|
waitForAnswer
|
|
18
|
-
} from "../chunk-
|
|
17
|
+
} from "../chunk-P7B3U6DR.js";
|
|
18
|
+
import "../chunk-R5AJNXZS.js";
|
|
19
19
|
import "../chunk-DWED7BS3.js";
|
|
20
20
|
import "../chunk-Z5PL3K7C.js";
|
|
21
21
|
import {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pushary/agent-hooks",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.32.2",
|
|
4
4
|
"description": "Permission hooks for AI coding agents: route tool approvals through Pushary push notifications",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pushary",
|
|
@@ -47,6 +47,8 @@
|
|
|
47
47
|
"pushary-post-hook": "./dist/bin/pushary-post-hook.js",
|
|
48
48
|
"pushary-stop-hook": "./dist/bin/pushary-stop-hook.js",
|
|
49
49
|
"pushary-prompt-hook": "./dist/bin/pushary-prompt-hook.js",
|
|
50
|
+
"pushary-notification-hook": "./dist/bin/pushary-notification-hook.js",
|
|
51
|
+
"pushary-claude": "./dist/bin/pushary-claude.js",
|
|
50
52
|
"pushary-codex": "./dist/bin/pushary-codex.js",
|
|
51
53
|
"pushary-codex-hook": "./dist/bin/pushary-codex-hook.js",
|
|
52
54
|
"pushary-gemini-hook": "./dist/bin/pushary-gemini-hook.js",
|
|
@@ -65,7 +67,7 @@
|
|
|
65
67
|
"scripts": {
|
|
66
68
|
"build": "node scripts/bundle-plugin.mjs && tsup",
|
|
67
69
|
"dev": "tsup --watch",
|
|
68
|
-
"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/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"
|
|
70
|
+
"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/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"
|
|
69
71
|
},
|
|
70
72
|
"dependencies": {
|
|
71
73
|
"@inquirer/prompts": "^8.4.2",
|