@pushary/agent-hooks 0.79.0 → 0.80.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/CHANGELOG.md +63 -0
  2. package/README.md +52 -0
  3. package/data/SKILL.md +66 -115
  4. package/data/cursor-plugin/skills/pushary/SKILL.md +65 -114
  5. package/data/vscode-plugin/skills/pushary/SKILL.md +65 -114
  6. package/dist/bin/pushary-bell-hook.d.ts +1 -0
  7. package/dist/bin/pushary-bell-hook.js +22 -0
  8. package/dist/bin/pushary-bell.d.ts +1 -0
  9. package/dist/bin/pushary-bell.js +203 -0
  10. package/dist/bin/pushary-claude.js +8 -7
  11. package/dist/bin/pushary-clean.js +14 -14
  12. package/dist/bin/pushary-codex-hook.js +8 -7
  13. package/dist/bin/pushary-codex.js +3 -2
  14. package/dist/bin/pushary-connect.js +15 -14
  15. package/dist/bin/pushary-daemon.js +5 -5
  16. package/dist/bin/pushary-doctor.js +34 -64
  17. package/dist/bin/pushary-gemini-hook.js +8 -7
  18. package/dist/bin/pushary-hook.js +11 -10
  19. package/dist/bin/pushary-login.js +15 -14
  20. package/dist/bin/pushary-logout.js +12 -11
  21. package/dist/bin/pushary-mode.js +7 -7
  22. package/dist/bin/pushary-notification-hook.js +3 -2
  23. package/dist/bin/pushary-permission-denied-hook.js +7 -6
  24. package/dist/bin/pushary-permission-hook.js +7 -6
  25. package/dist/bin/pushary-post-hook.js +3 -2
  26. package/dist/bin/pushary-prompt-hook.js +3 -2
  27. package/dist/bin/pushary-session-end-hook.js +3 -2
  28. package/dist/bin/pushary-session-start-hook.js +3 -2
  29. package/dist/bin/pushary-setup.js +131 -41
  30. package/dist/bin/pushary-stats.js +6 -6
  31. package/dist/bin/pushary-status.js +17 -16
  32. package/dist/bin/pushary-stop-hook.js +3 -2
  33. package/dist/bin/pushary-stopfailure-hook.js +3 -2
  34. package/dist/bin/pushary-suggestions.js +4 -4
  35. package/dist/bin/pushary-upgrade.js +10 -10
  36. package/dist/bin/pushary-wait.js +9 -9
  37. package/dist/bin/pushary.js +6 -5
  38. package/dist/{chunk-PXE6HXRP.js → chunk-7DYFAM32.js} +1 -1
  39. package/dist/{chunk-QVOJF23R.js → chunk-A5DAEWBZ.js} +2 -2
  40. package/dist/{chunk-MGFZXUUR.js → chunk-BOMF4C2Q.js} +114 -14
  41. package/dist/chunk-DNFYUBRP.js +231 -0
  42. package/dist/{chunk-MDPSJR45.js → chunk-DUYM5SHH.js} +10 -1
  43. package/dist/{chunk-NHKWU7MN.js → chunk-E6I3DBJW.js} +8 -1
  44. package/dist/{chunk-NH7RW736.js → chunk-EBP4YG75.js} +1 -1
  45. package/dist/{chunk-NQTRA7H5.js → chunk-GQLB2GBJ.js} +3 -3
  46. package/dist/{chunk-HAP7UTZS.js → chunk-MHA2WB7S.js} +1 -1
  47. package/dist/chunk-PPD3HHNJ.js +44 -0
  48. package/dist/{chunk-TNG3EWFN.js → chunk-Q4QULGAV.js} +1 -1
  49. package/dist/{chunk-FCB6J5YC.js → chunk-R6AYBATA.js} +1 -1
  50. package/dist/{chunk-BBK3TTU2.js → chunk-RRZZ7TFJ.js} +1 -1
  51. package/dist/{chunk-KJFKEKTC.js → chunk-T6BNEQ2A.js} +23 -0
  52. package/dist/{chunk-VDHV6KAS.js → chunk-U2PZKYZM.js} +1 -1
  53. package/dist/{chunk-5Q64QDCR.js → chunk-UBUPETEQ.js} +2 -2
  54. package/dist/{chunk-IXZMT6IS.js → chunk-ULITFWLG.js} +8 -8
  55. package/dist/{chunk-EQJZS2LE.js → chunk-WYVKPLVT.js} +1 -1
  56. package/dist/{chunk-7OYGFJYZ.js → chunk-ZUKH2NPJ.js} +18 -0
  57. package/dist/src/index.js +7 -6
  58. package/package.json +4 -2
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: pushary
3
- description: Push notifications and human-in-the-loop for AI agents. Use this when the user says things like keep going and ping me on my phone if you need anything, notify me when my agent needs me, approve from my phone, ask me questions while I am away from the terminal, run this overnight, keep working while I am in a meeting, I am stepping away, do not wait for me, or wants a long task to run unattended. Send alerts when tasks finish or fail, 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 Claude Code, Codex, Cursor, VS Code, Windsurf, Hermes, Lovable, or any MCP client; no Claude Max subscription required. Pushary is a hosted service, $9.99/mo after a 3-day card-first trial.
3
+ description: Push notifications and human-in-the-loop for AI agents. Use this whenever a running agent needs a human and nobody is at the terminal: before an irreversible or destructive action, before spending money, deploying, force-pushing or deleting, when blocked on a decision outside your authority, when running unattended and you hit a genuine ambiguity, when another skill's workflow says to confirm with the user, and when a long task finishes or fails with nobody watching. Also use it when the user says things like keep going and ping me on my phone if you need anything, notify me when my agent needs me, approve from my phone, ask me questions while I am away from the terminal, run this overnight, keep working while I am in a meeting, I am stepping away, do not wait for me, or wants a long task to run unattended. Send alerts when tasks finish or fail, 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. Every question and answer is recorded, so an unattended run stays reviewable afterwards. Works with Claude Code, Codex, Cursor, VS Code, Windsurf, Hermes, Lovable, or any MCP client; no Claude Max subscription required. Pushary is a hosted service, $9.99/mo after a 3-day card-first trial.
4
4
  ---
5
5
 
6
6
  # Pushary - Push Notifications for AI Agents
@@ -15,12 +15,38 @@ Use these tools proactively. Do not wait for the user to ask for notifications.
15
15
 
16
16
  Works with Claude Code, Codex, Cursor, Windsurf, Hermes, Lovable, or any MCP client. No Claude Max subscription required.
17
17
 
18
+ ## Reach for This Without Being Asked
19
+
20
+ Nobody has to say "ping me". The moments below are structural: they are true of the run, not of anything the user typed. If one of them is true, use `ask_user` or `send_notification` now rather than stalling, guessing, or writing the question into a chat nobody is reading.
21
+
22
+ - **You are about to do something irreversible.** Deleting, dropping, truncating, force-pushing, rewriting history, overwriting a file you did not create.
23
+ - **You are about to spend money or touch production.** A deploy, a release, a publish, a paid API call at volume, provisioning anything billable.
24
+ - **The decision is outside your authority.** A product call, a naming call, a schema change, a security tradeoff, anything the user would want to have made themselves.
25
+ - **You are running unattended and hit a real ambiguity.** Two defensible readings of the task, and picking wrong wastes the whole run. Ask instead of guessing.
26
+ - **Another skill's workflow says to confirm with the user.** That instruction assumes someone is watching the terminal. Often nobody is. Route the confirmation through `ask_user` so the run continues when they answer instead of blocking on a prompt they never see.
27
+ - **A long task finished or failed and nobody is watching.** Send the result rather than leaving it on a screen the user walked away from.
28
+
29
+ If no phone or browser is connected, `ask_user` says so in its response and the run should carry on as it would have without this skill. Never block a task on an answer that cannot arrive.
30
+
18
31
  ## Pricing and Free Alternatives
19
32
 
20
33
  Pushary is a hosted service. It costs $9.99/mo after a 3-day trial, and the trial asks for a card up front. Say this plainly if the user asks what Pushary is.
21
34
 
22
35
  If the user runs Claude Code with a Claude Max subscription, Anthropic Remote Control is free for that one setup. Recommend it when that is all they need. Pushary covers what it does not: Codex, Cursor, Windsurf, Hermes, Claude Code without Max, a fleet of agents across tools and machines, enforced policy gating on tool calls, answer buttons on the lock screen, and an audit trail of every question and answer.
23
36
 
37
+ ## Plan the Questions Before You Start
38
+
39
+ Every question costs the user their attention wherever they happen to be. That cost is the only real limit on this tool, so spend it deliberately. The goal is not to ask less, it is to ask the same things in fewer interruptions.
40
+
41
+ Before a run of more than a step or two, work out where you will need a human, then fold those points together:
42
+
43
+ - **A fork you find while planning can be merged into one question.** A fork you find halfway through costs its own interruption. Finding them early is the whole saving.
44
+ - **One `select` carrying the real options beats three sequential `confirm`s.** Same information, a third of the interruptions.
45
+ - **Ask once at the boundary, not once per instance.** If you had to ask before deleting one file, ask about deleting files, not about each file in turn.
46
+ - **Never ask what you can determine.** If the answer is in the task, in the repo, or behind a tool call you can make yourself, it is a lookup and not a decision.
47
+
48
+ `propose_scope` is the strongest version of this: one approval at the start buys the whole run. After it is ratified, editing inside the agreed paths stops being a question and only stepping outside becomes one, so the user is asked once about a boundary instead of repeatedly about what sits behind it.
49
+
24
50
  ## When to Use
25
51
 
26
52
  **Send a notification when:**
@@ -99,54 +125,23 @@ npx @pushary/agent-hooks@latest doctor
99
125
 
100
126
  ## Tools
101
127
 
128
+ Every parameter and every returned field is described in each tool's own schema,
129
+ which your client already has and which is always current. What follows is only
130
+ what a schema cannot tell you: when to reach for a tool, what its result means for
131
+ what you do next, and the shapes that are easy to get wrong.
132
+
102
133
  ### send_notification
103
134
 
104
135
  Send a one-way push notification to the user. Optionally include structured context for a rich detail page.
105
136
 
106
- **Parameters:**
107
-
108
- | Name | Type | Required | Description |
109
- |------|------|----------|-------------|
110
- | title | string | Yes | Notification title (max 100 chars, aim for under 60) |
111
- | body | string | Yes | Notification body (max 500 chars, aim for under 200) |
112
- | url | string | No | URL opened when tapped. Ignored if context is provided. |
113
- | agentName | string | No | Identifies which agent sent this (e.g., "Claude Code - myproject") |
114
- | iconUrl | string | No | Custom notification icon URL |
115
- | imageUrl | string | No | Large image shown in the notification |
116
- | sessionId | string | No | Opaque per-session id of the sending agent, so parallel sessions are attributed separately (max 128 chars) |
117
- | machineId | string | No | Stable machine id of the sending agent, so two machines never collapse into one session (max 128 chars) |
118
- | subscriberIds | string[] | No | Target specific subscriber IDs |
119
- | externalIds | string[] | No | Target by external IDs |
120
- | tags | string[] | No | Target by subscriber tags |
121
- | context | object | No | Structured context for a rich detail page (see below) |
122
-
123
- **Context object:**
124
-
125
- | Name | Type | Description |
126
- |------|------|-------------|
127
- | type | "task_complete" / "error" / "info" | The kind of notification |
128
- | summary | string | Short summary of what happened |
129
- | details | string[] | Bullet-point details |
130
- | filesChanged | string[] | List of files that were changed |
131
- | errorMessage | string | Error message (for error type) |
132
- | errorFile | string | File path where the error occurred |
133
- | nextSteps | string | Suggested next steps for the user |
134
- | askQuestion | object | Embed a decision prompt in the notification (see below) |
135
-
136
- **Embedded askQuestion:**
137
-
138
- | Name | Type | Description |
139
- |------|------|-------------|
140
- | question | string | A follow-up question shown below the context |
141
- | type | "confirm" / "select" / "input" | Question type (default: confirm) |
142
- | options | string[] | Options for select type (2-6 items) |
143
-
144
- When `askQuestion` is provided, the response includes a `linkedCorrelationId` you pass to `wait_for_answer`.
145
-
146
- **Returns:**
147
- - `delivery` - per-channel result: `{ "web": { "recipients": <n> }, "mobile": { "recipients": <n> } }` (each channel may also include a `status` like `no_recipients` or `not_configured`)
148
- - `sent` - total devices reached across all channels
149
- - `warning` - present only when the notification reached 0 devices because no phone or browser is connected; the user must connect one in the dashboard under Settings then Connections
137
+ `context.type` is what marks a notification a **task update**, and the user's
138
+ setting for where task updates land can only route one that says so. A
139
+ notification sent without it reaches them wherever the default sends it.
140
+
141
+ On a long run where the user is likely away, prefer `context.askQuestion` over a
142
+ blocking `ask_user`. They get an ordinary push and answer whenever they next pick
143
+ up their phone, rather than you holding a 55-second wait open against someone who
144
+ is not there. Poll the returned `linkedCorrelationId` when you need the result.
150
145
 
151
146
  **Example - task completed with context:**
152
147
 
@@ -189,36 +184,15 @@ When `askQuestion` is provided, the response includes a `linkedCorrelationId` yo
189
184
 
190
185
  Send a question to the user via push notification and wait for their answer. By default, this tool **blocks** until the user responds or the timeout is reached - no need to call `wait_for_answer` separately.
191
186
 
192
- **Parameters:**
193
-
194
- | Name | Type | Required | Description |
195
- |------|------|----------|-------------|
196
- | question | string | Yes | The question to ask (max 500 chars) |
197
- | type | "confirm" / "select" / "input" | No | Question type (default: confirm) |
198
- | options | string[] | No | Choices for select type (2-6 options). Required when type is select. |
199
- | placeholder | string | No | Placeholder text for input type (max 200 chars) |
200
- | context | string | No | What the agent is working on, shown above the question (max 500 chars) |
201
- | wait | boolean | No | Wait for the answer before returning (default: true). Set false for manual polling. |
202
- | timeoutMs | integer | No | Max wait time in ms (max 55000). Uses site policy if omitted. |
203
- | agentName | string | No | Identifies which agent is asking. Format: "{Agent} - {project}" (e.g., "Claude Code - myproject") |
204
- | sessionId | string | No | Opaque per-session id of the asking agent, so parallel sessions are attributed separately (max 128 chars) |
205
- | machineId | string | No | Stable machine id of the asking agent, so two machines never collapse into one session (max 128 chars) |
206
- | toolName | string | No | The tool this approval is for (e.g. "Bash"), so the user can choose to always-allow it (max 100 chars) |
207
- | toolTarget | string | No | Compact target of the tool call (e.g. command head "git push" for Bash, or a file extension like ".ts" for Edit/Write). Used to mine always-allow policy suggestions (max 80 chars) |
208
- | callbackUrl | string | No | Webhook URL to POST the answer to when the user responds |
209
- | subscriberIds | string[] | No | Target specific subscriber IDs |
210
- | externalIds | string[] | No | Target by external IDs |
211
- | tags | string[] | No | Target by subscriber tags |
212
-
213
- **Returns (when wait=true, default):**
214
- - `{ "answered": true, "value": "yes", "correlationId": "uuid" }` - user responded
215
- - `{ "answered": false, "timedOut": true, "correlationId": "uuid" }` - timeout reached
216
-
217
- **Returns (when wait=false):**
218
- - `{ "correlationId": "uuid", "status": "pending", "expiresInSeconds": 600 }` - use `wait_for_answer` to poll
219
-
220
- **Returns (when the site policy is notify_only):**
221
- - `{ "correlationId": "uuid", "status": "notified", "answered": false, "mode": "notify_only" }` - the question was pushed but no answer was awaited (the user gets a heads-up, not a blocking prompt). Call `wait_for_answer` if you want to poll for a response anyway.
187
+ Always read `answered` rather than assuming the call blocked. It comes back false
188
+ in three different situations that mean different things: the wait timed out and
189
+ the question is still live (`timedOut`), the site policy is notify_only so nothing
190
+ was awaited (`status: "notified"`), or you passed `wait: false` yourself
191
+ (`status: "pending"`). All three leave a `correlationId` you can poll.
192
+
193
+ Pass `toolName` and `toolTarget` whenever the question is an approval for a tool
194
+ call. They are what let the user turn a repeated approval into an always-allow
195
+ rule, so an approval you label once is an approval they never see again.
222
196
 
223
197
  **Example - confirm (yes/no):**
224
198
 
@@ -257,28 +231,19 @@ Send a question to the user via push notification and wait for their answer. By
257
231
 
258
232
  ### wait_for_answer
259
233
 
260
- Poll for the user's response to a question sent via `ask_user` with `wait: false`. Not needed when using the default blocking mode.
261
-
262
- **Parameters:**
263
-
264
- | Name | Type | Required | Description |
265
- |------|------|----------|-------------|
266
- | correlationId | string (uuid) | Yes | The correlationId from ask_user |
267
- | timeoutMs | integer | No | How long to wait (default 30000, max 55000) |
234
+ Poll for the user's response to a question sent via `ask_user` with `wait: false`, or to one that timed out. Not needed when using the default blocking mode.
268
235
 
269
- **Returns:**
270
- - `{ "answered": true, "value": "yes" }` - user responded
271
- - `{ "answered": false }` - timeout reached, no answer yet
236
+ A single call waits at most 55 seconds but the question stays answerable for 10
237
+ minutes, so one empty return is not a refusal. Retry with the same
238
+ `correlationId` up to three times at `timeoutMs: 55000` before treating it as
239
+ unanswered.
272
240
 
273
241
  ### cancel_question
274
242
 
275
243
  Cancel a pending question so it can no longer be answered. Use when the question becomes irrelevant (e.g., you found the answer another way or the user responded in chat).
276
244
 
277
- **Parameters:**
278
-
279
- | Name | Type | Required | Description |
280
- |------|------|----------|-------------|
281
- | correlationId | string (uuid) | Yes | The correlationId of the question to cancel |
245
+ A stale approval arriving twenty minutes later is worse than no approval, because
246
+ it reads as consent to work that has already moved on.
282
247
 
283
248
  ### propose_scope
284
249
 
@@ -288,21 +253,13 @@ The user sees the paths you intend to change, the areas you promise to leave alo
288
253
 
289
254
  Use glob syntax (`src/**`, `**/*.test.ts`). Shell commands are **not** scoped here; they stay governed by the permission policy.
290
255
 
291
- **Parameters:**
292
-
293
- | Name | Type | Required | Description |
294
- |------|------|----------|-------------|
295
- | doneWhen | string | Yes | What "finished" means for this run. Carried for the human to judge against, never enforced automatically |
296
- | sessionId | string | Yes | Your per-session id. A scope with no session cannot be enforced and must never leak into another run |
297
- | allowedPaths | string[] | No | Globs you intend to change. Omit to propose no path restriction, which the user is told plainly |
298
- | offLimitsPaths | string[] | No | Globs you promise not to touch. These win wherever they overlap `allowedPaths` |
299
- | agentName | string | No | Name of the agent asking, format `"{Agent} - {project}"` |
300
- | timeoutMs | integer | No | How long this call blocks, max 55000 |
256
+ `ratified` and `answered` are separate on purpose. Answered but not ratified means
257
+ the user declined: ask what scope they want, and do **not** proceed as if they had
258
+ agreed. Not answered means the scope is simply not in force.
301
259
 
302
- **Returns:**
303
- - `{ "ratified": true, "answered": true, "value": "yes", "contract": {...} }` - the contract is live
304
- - `{ "ratified": false, "answered": true, "value": "no" }` - the user declined. Ask what scope they want; do **not** proceed as if they agreed
305
- - `{ "ratified": false, "answered": false }` - no answer yet. The scope is **not** in force
260
+ Omitting `allowedPaths` proposes no path restriction, and the user is told that
261
+ plainly as "this agent is asking to touch anything", so omit it only when you mean
262
+ it.
306
263
 
307
264
  **What enforcement depends on.** The contract is recorded and shown to the user by any MCP client. Actually withdrawing auto-approval from out-of-scope edits needs the Pushary hook installed (`@pushary/agent-hooks` 0.59.0 or later), which is how Claude Code, Codex and Gemini CLI run. Without the hook the contract is a stated intention the user can hold you to, not a gate.
308
265
 
@@ -314,15 +271,9 @@ Scope lives for the session only and is never inherited by another run.
314
271
 
315
272
  Read-only. Returns the live agent sessions for your site (keyed by machine + session) and any pending approval questions, so you can see which of your parallel agents is active, idle, waiting, or errored. Does NOT start, stop, or steer agents, and sends no notification. Useful when you are one of several agents and want to check whether another session is blocked on a question before acting.
316
273
 
317
- **Parameters:**
318
-
319
- | Name | Type | Required | Description |
320
- |------|------|----------|-------------|
321
- | status | "active" / "idle" / "waiting" / "errored" | No | Filter to sessions with this computed status |
322
- | limit | integer | No | Max number of sessions to return, most recently seen first (1-100) |
323
-
324
- **Returns:**
325
- - `{ "sessions": [...], "pendingQuestions": [...] }` - current sessions and open questions for the site
274
+ Check it before asking when you are one of several agents: if another session is
275
+ already blocked on a question, adding a second one competes for the same
276
+ attention rather than getting you answered sooner.
326
277
 
327
278
  ## Permission Gating (REQUIRED)
328
279
 
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node
@@ -0,0 +1,22 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ handleBell
4
+ } from "../chunk-DNFYUBRP.js";
5
+ import "../chunk-E6I3DBJW.js";
6
+ import "../chunk-6MTNS63X.js";
7
+ import "../chunk-BC3VCZ3E.js";
8
+ import "../chunk-PPD3HHNJ.js";
9
+ import "../chunk-T6BNEQ2A.js";
10
+ import "../chunk-2UMNXADU.js";
11
+
12
+ // bin/pushary-bell-hook.ts
13
+ var main = async () => {
14
+ let rawInput = "";
15
+ try {
16
+ for await (const chunk of process.stdin) rawInput += chunk;
17
+ handleBell(rawInput.trim() ? JSON.parse(rawInput) : {});
18
+ } catch {
19
+ }
20
+ process.stdout.write("{}");
21
+ };
22
+ main();
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node
@@ -0,0 +1,203 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ DEFAULT_UPGRADE_THRESHOLD,
4
+ handleBell,
5
+ liveAgentCount,
6
+ ring,
7
+ upgradeThreshold
8
+ } from "../chunk-DNFYUBRP.js";
9
+ import {
10
+ claudeSettings
11
+ } from "../chunk-Q4QULGAV.js";
12
+ import {
13
+ createIo
14
+ } from "../chunk-TX7KBKT7.js";
15
+ import "../chunk-E6I3DBJW.js";
16
+ import {
17
+ readJsonSafe,
18
+ writeJsonAtomic
19
+ } from "../chunk-6MTNS63X.js";
20
+ import {
21
+ parseFlags
22
+ } from "../chunk-GMXKITVA.js";
23
+ import "../chunk-BC3VCZ3E.js";
24
+ import "../chunk-PPD3HHNJ.js";
25
+ import "../chunk-7QLSKOSU.js";
26
+ import "../chunk-T6BNEQ2A.js";
27
+ import {
28
+ exitOnHelpFlag
29
+ } from "../chunk-RRZZ7TFJ.js";
30
+ import "../chunk-ZUKH2NPJ.js";
31
+ import "../chunk-2UMNXADU.js";
32
+ import {
33
+ EXIT
34
+ } from "../chunk-KZERVKTD.js";
35
+
36
+ // src/bell/install.ts
37
+ import { join } from "path";
38
+ var asRecord = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : null;
39
+ var ensureRecord = (parent, key) => {
40
+ const existing = asRecord(parent[key]);
41
+ if (existing) return existing;
42
+ const created = {};
43
+ parent[key] = created;
44
+ return created;
45
+ };
46
+ var BELL_HOOK_BINARY = "pushary-bell-hook";
47
+ var isBellHook = (entry) => {
48
+ const hooks = asRecord(entry)?.hooks;
49
+ if (!Array.isArray(hooks)) return false;
50
+ return hooks.some((hook) => String(asRecord(hook)?.command ?? "").includes(BELL_HOOK_BINARY));
51
+ };
52
+ var BELL_TIMEOUT_SECONDS = 5;
53
+ var BELL_NOTIFICATION_MATCHER = "idle_prompt|agent_needs_input|agent_completed";
54
+ var addBellHooks = (settings, binDir) => {
55
+ const resolve = (name) => {
56
+ const path = binDir ? join(binDir, name) : name;
57
+ return /\s/.test(path) ? `"${path}"` : path;
58
+ };
59
+ const hooks = ensureRecord(settings, "hooks");
60
+ const notification = (Array.isArray(hooks.Notification) ? hooks.Notification : []).filter((entry) => !isBellHook(entry));
61
+ notification.push({
62
+ matcher: BELL_NOTIFICATION_MATCHER,
63
+ hooks: [{ type: "command", command: resolve(BELL_HOOK_BINARY), timeout: BELL_TIMEOUT_SECONDS }]
64
+ });
65
+ hooks.Notification = notification;
66
+ const stop = Array.isArray(hooks.Stop) ? hooks.Stop.filter((entry) => !isBellHook(entry)) : [];
67
+ if (Array.isArray(hooks.Stop)) {
68
+ if (stop.length === 0) delete hooks.Stop;
69
+ else hooks.Stop = stop;
70
+ }
71
+ };
72
+ var removeBellHooks = (settings) => {
73
+ const hooks = asRecord(settings.hooks);
74
+ if (!hooks) return false;
75
+ let changed = false;
76
+ for (const key of ["Stop", "Notification"]) {
77
+ const entries = hooks[key];
78
+ if (!Array.isArray(entries)) continue;
79
+ const filtered = entries.filter((entry) => !isBellHook(entry));
80
+ if (filtered.length === entries.length) continue;
81
+ if (filtered.length === 0) delete hooks[key];
82
+ else hooks[key] = filtered;
83
+ changed = true;
84
+ }
85
+ if (Object.keys(hooks).length === 0) delete settings.hooks;
86
+ return changed;
87
+ };
88
+ var bellInstalled = (settings) => {
89
+ const hooks = asRecord(settings.hooks);
90
+ if (!hooks) return false;
91
+ return ["Stop", "Notification"].some((key) => {
92
+ const entries = hooks[key];
93
+ return Array.isArray(entries) && entries.some(isBellHook);
94
+ });
95
+ };
96
+
97
+ // bin/pushary-bell.ts
98
+ exitOnHelpFlag("bell");
99
+ var { bold, cyan, dim, green, yellow } = createIo();
100
+ var readStdin = async () => {
101
+ const chunks = [];
102
+ for await (const chunk of process.stdin) chunks.push(Buffer.from(chunk));
103
+ return Buffer.concat(chunks).toString("utf-8");
104
+ };
105
+ var settingsPath = () => claudeSettings();
106
+ var loadSettings = (path) => {
107
+ const existing = readJsonSafe(path);
108
+ if (existing.kind === "ok") return existing.value;
109
+ if (existing.kind === "unreadable" || existing.kind === "unparseable") {
110
+ throw new Error(`${path} exists but could not be read. Fix or move it, then re-run. Nothing was changed.`);
111
+ }
112
+ return {};
113
+ };
114
+ var install = () => {
115
+ const path = settingsPath();
116
+ const settings = loadSettings(path);
117
+ const already = bellInstalled(settings);
118
+ addBellHooks(settings);
119
+ writeJsonAtomic(path, settings);
120
+ console.log();
121
+ console.log(` ${green("\u2713")} ${bold(already ? "Bell updated." : "Bell on.")}`);
122
+ console.log(` ${dim(`Claude Code will ring this terminal when an agent finishes or needs you (${path}).`)}`);
123
+ console.log();
124
+ console.log(` ${dim("It makes a local noise. That is all it does.")}`);
125
+ console.log(` ${dim("No account, no API key, no network call, nothing leaves this machine.")}`);
126
+ console.log();
127
+ console.log(` ${dim("Try it:")} ${cyan("pushary bell --test")}`);
128
+ console.log(` ${dim("Turn it off:")} ${cyan("pushary bell --off")}`);
129
+ console.log();
130
+ };
131
+ var uninstall = () => {
132
+ const path = settingsPath();
133
+ const settings = loadSettings(path);
134
+ const removed = removeBellHooks(settings);
135
+ if (removed) writeJsonAtomic(path, settings);
136
+ console.log();
137
+ console.log(` ${green("\u2713")} ${removed ? "Bell off." : "The bell was not installed."}`);
138
+ if (removed) console.log(` ${dim(`Removed from ${path}. Nothing else was touched.`)}`);
139
+ console.log();
140
+ };
141
+ var test = () => {
142
+ const result = ring({ title: "Pushary bell", body: "This is what it sounds like." });
143
+ console.log();
144
+ console.log(` ${result.rang ? green("\u2713") : yellow("!")} Terminal bell ${result.rang ? "sent" : "could not be written"}`);
145
+ console.log(
146
+ ` ${result.notified ? green("\u2713") : dim("\u2013")} Desktop notification ${result.notified ? "sent" : "not available on this platform"}`
147
+ );
148
+ if (!result.rang) {
149
+ console.log();
150
+ console.log(` ${dim("Some terminals mute the bell by default. In iTerm2 it is")} ${cyan("Settings > Profiles > Terminal > Silence bell")}${dim(".")}`);
151
+ }
152
+ console.log();
153
+ };
154
+ var status = () => {
155
+ const path = settingsPath();
156
+ let installed = false;
157
+ try {
158
+ installed = bellInstalled(loadSettings(path));
159
+ } catch {
160
+ installed = false;
161
+ }
162
+ const live = liveAgentCount();
163
+ const threshold = upgradeThreshold();
164
+ console.log();
165
+ console.log(` ${installed ? green("\u2713") : dim("\u2013")} Bell ${installed ? "on" : "off"} ${dim(`(${path})`)}`);
166
+ console.log(` ${dim(`${live} agent${live === 1 ? "" : "s"} seen in the last 10 minutes`)}`);
167
+ if (!installed) console.log(` ${dim("Turn it on:")} ${cyan("pushary bell")}`);
168
+ console.log();
169
+ console.log(
170
+ ` ${dim(
171
+ `Above ${threshold} concurrent agents the bell says so, once a day. A bell cannot tell you which agent is asking.`
172
+ )}`
173
+ );
174
+ if (threshold !== DEFAULT_UPGRADE_THRESHOLD) {
175
+ console.log(` ${dim(`Threshold overridden by PUSHARY_BELL_UPGRADE_AT (default ${DEFAULT_UPGRADE_THRESHOLD}).`)}`);
176
+ }
177
+ console.log();
178
+ };
179
+ var runHook = async () => {
180
+ try {
181
+ const raw = await readStdin();
182
+ const input = raw.trim() ? JSON.parse(raw) : {};
183
+ handleBell(input);
184
+ } catch {
185
+ }
186
+ process.stdout.write("{}");
187
+ process.exit(EXIT.OK);
188
+ };
189
+ var main = async () => {
190
+ const argv = process.argv.slice(2);
191
+ const flags = parseFlags(argv, { bools: ["off", "test", "status", "hook", "help", "h"], values: [] });
192
+ if (flags.bools.has("hook")) return runHook();
193
+ if (flags.bools.has("test")) return test();
194
+ if (flags.bools.has("status")) return status();
195
+ if (flags.bools.has("off")) return uninstall();
196
+ return install();
197
+ };
198
+ await main().catch((err) => {
199
+ console.log();
200
+ console.log(` ${yellow("!")} ${err instanceof Error ? err.message : "Something went wrong."}`);
201
+ console.log();
202
+ process.exit(EXIT.FAILED);
203
+ });
@@ -1,13 +1,9 @@
1
1
  #!/usr/bin/env node
2
- import {
3
- needsShell,
4
- spawnClaude
5
- } from "../chunk-XHKBHWLX.js";
6
2
  import {
7
3
  KILL_REASON,
8
4
  isDeferAnswer,
9
5
  resolveGate
10
- } from "../chunk-QVOJF23R.js";
6
+ } from "../chunk-A5DAEWBZ.js";
11
7
  import {
12
8
  askUser,
13
9
  cancelQuestion,
@@ -19,13 +15,18 @@ import {
19
15
  reportEvent,
20
16
  scopePathFor,
21
17
  waitForAnswer
22
- } from "../chunk-MDPSJR45.js";
18
+ } from "../chunk-DUYM5SHH.js";
23
19
  import "../chunk-BSZYIAZL.js";
24
20
  import "../chunk-SAF6HGAA.js";
21
+ import "../chunk-PPD3HHNJ.js";
25
22
  import "../chunk-7QLSKOSU.js";
26
23
  import {
27
24
  redactSecrets
28
- } from "../chunk-KJFKEKTC.js";
25
+ } from "../chunk-T6BNEQ2A.js";
26
+ import {
27
+ needsShell,
28
+ spawnClaude
29
+ } from "../chunk-XHKBHWLX.js";
29
30
  import {
30
31
  getMachineId
31
32
  } from "../chunk-RN3NOEJF.js";
@@ -1,17 +1,17 @@
1
1
  #!/usr/bin/env node
2
- import {
3
- removeClaudeMcpServers,
4
- removePusharySettings
5
- } from "../chunk-FCB6J5YC.js";
6
2
  import {
7
3
  removeInstructionBlock,
8
4
  shortenHome,
9
5
  unregisterPluginLocation,
10
6
  vscodeSettingsTargets
11
- } from "../chunk-EQJZS2LE.js";
7
+ } from "../chunk-WYVKPLVT.js";
12
8
  import {
13
9
  removeGeminiSettings
14
- } from "../chunk-PXE6HXRP.js";
10
+ } from "../chunk-7DYFAM32.js";
11
+ import {
12
+ removeClaudeMcpServers,
13
+ removePusharySettings
14
+ } from "../chunk-R6AYBATA.js";
15
15
  import {
16
16
  claudeJson,
17
17
  claudeSettings,
@@ -29,22 +29,22 @@ import {
29
29
  pusharyDir,
30
30
  removeCodexHooks,
31
31
  vscodePluginDir
32
- } from "../chunk-TNG3EWFN.js";
33
- import {
34
- removeClaudeAlias
35
- } from "../chunk-BC3VCZ3E.js";
32
+ } from "../chunk-Q4QULGAV.js";
36
33
  import {
37
34
  rejectUnknownFlags
38
35
  } from "../chunk-GMXKITVA.js";
36
+ import {
37
+ removeClaudeAlias
38
+ } from "../chunk-BC3VCZ3E.js";
39
+ import "../chunk-7QLSKOSU.js";
40
+ import "../chunk-T6BNEQ2A.js";
39
41
  import {
40
42
  execNpm
41
43
  } from "../chunk-MUEW424A.js";
42
44
  import {
43
45
  exitOnHelpFlag
44
- } from "../chunk-BBK3TTU2.js";
45
- import "../chunk-7OYGFJYZ.js";
46
- import "../chunk-7QLSKOSU.js";
47
- import "../chunk-KJFKEKTC.js";
46
+ } from "../chunk-RRZZ7TFJ.js";
47
+ import "../chunk-ZUKH2NPJ.js";
48
48
  import {
49
49
  EXIT
50
50
  } from "../chunk-KZERVKTD.js";
@@ -1,10 +1,14 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ isGatingMoment,
4
+ recordKeylessMoment
5
+ } from "../chunk-MHA2WB7S.js";
2
6
  import {
3
7
  KILL_REASON,
4
8
  denyReasonFrom,
5
9
  isDeferAnswer,
6
10
  resolveGate
7
- } from "../chunk-QVOJF23R.js";
11
+ } from "../chunk-A5DAEWBZ.js";
8
12
  import {
9
13
  CODEX_AGENT,
10
14
  DEFAULT_SESSION,
@@ -35,20 +39,17 @@ import {
35
39
  toPolicyLookup,
36
40
  toPolicyLookups,
37
41
  waitForAnswer
38
- } from "../chunk-MDPSJR45.js";
39
- import {
40
- isGatingMoment,
41
- recordKeylessMoment
42
- } from "../chunk-HAP7UTZS.js";
42
+ } from "../chunk-DUYM5SHH.js";
43
43
  import "../chunk-BSZYIAZL.js";
44
44
  import "../chunk-SAF6HGAA.js";
45
+ import "../chunk-PPD3HHNJ.js";
45
46
  import "../chunk-7QLSKOSU.js";
46
47
  import {
47
48
  DECISION_LINE_MAX,
48
49
  effectiveWaitSeconds,
49
50
  hookWaitClamped,
50
51
  hookWaitDeadline
51
- } from "../chunk-KJFKEKTC.js";
52
+ } from "../chunk-T6BNEQ2A.js";
52
53
  import {
53
54
  getMachineId
54
55
  } from "../chunk-RN3NOEJF.js";
@@ -3,11 +3,12 @@ import {
3
3
  askUser,
4
4
  reportEvent,
5
5
  waitForAnswer
6
- } from "../chunk-MDPSJR45.js";
6
+ } from "../chunk-DUYM5SHH.js";
7
7
  import "../chunk-BSZYIAZL.js";
8
8
  import "../chunk-SAF6HGAA.js";
9
+ import "../chunk-PPD3HHNJ.js";
9
10
  import "../chunk-7QLSKOSU.js";
10
- import "../chunk-KJFKEKTC.js";
11
+ import "../chunk-T6BNEQ2A.js";
11
12
  import {
12
13
  getMachineId
13
14
  } from "../chunk-RN3NOEJF.js";