@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.
- package/CHANGELOG.md +63 -0
- package/README.md +52 -0
- package/data/SKILL.md +66 -115
- package/data/cursor-plugin/skills/pushary/SKILL.md +65 -114
- package/data/vscode-plugin/skills/pushary/SKILL.md +65 -114
- package/dist/bin/pushary-bell-hook.d.ts +1 -0
- package/dist/bin/pushary-bell-hook.js +22 -0
- package/dist/bin/pushary-bell.d.ts +1 -0
- package/dist/bin/pushary-bell.js +203 -0
- package/dist/bin/pushary-claude.js +8 -7
- package/dist/bin/pushary-clean.js +14 -14
- package/dist/bin/pushary-codex-hook.js +8 -7
- package/dist/bin/pushary-codex.js +3 -2
- package/dist/bin/pushary-connect.js +15 -14
- package/dist/bin/pushary-daemon.js +5 -5
- package/dist/bin/pushary-doctor.js +34 -64
- package/dist/bin/pushary-gemini-hook.js +8 -7
- package/dist/bin/pushary-hook.js +11 -10
- package/dist/bin/pushary-login.js +15 -14
- package/dist/bin/pushary-logout.js +12 -11
- package/dist/bin/pushary-mode.js +7 -7
- package/dist/bin/pushary-notification-hook.js +3 -2
- package/dist/bin/pushary-permission-denied-hook.js +7 -6
- package/dist/bin/pushary-permission-hook.js +7 -6
- package/dist/bin/pushary-post-hook.js +3 -2
- package/dist/bin/pushary-prompt-hook.js +3 -2
- package/dist/bin/pushary-session-end-hook.js +3 -2
- package/dist/bin/pushary-session-start-hook.js +3 -2
- package/dist/bin/pushary-setup.js +131 -41
- package/dist/bin/pushary-stats.js +6 -6
- package/dist/bin/pushary-status.js +17 -16
- package/dist/bin/pushary-stop-hook.js +3 -2
- package/dist/bin/pushary-stopfailure-hook.js +3 -2
- package/dist/bin/pushary-suggestions.js +4 -4
- package/dist/bin/pushary-upgrade.js +10 -10
- package/dist/bin/pushary-wait.js +9 -9
- package/dist/bin/pushary.js +6 -5
- package/dist/{chunk-PXE6HXRP.js → chunk-7DYFAM32.js} +1 -1
- package/dist/{chunk-QVOJF23R.js → chunk-A5DAEWBZ.js} +2 -2
- package/dist/{chunk-MGFZXUUR.js → chunk-BOMF4C2Q.js} +114 -14
- package/dist/chunk-DNFYUBRP.js +231 -0
- package/dist/{chunk-MDPSJR45.js → chunk-DUYM5SHH.js} +10 -1
- package/dist/{chunk-NHKWU7MN.js → chunk-E6I3DBJW.js} +8 -1
- package/dist/{chunk-NH7RW736.js → chunk-EBP4YG75.js} +1 -1
- package/dist/{chunk-NQTRA7H5.js → chunk-GQLB2GBJ.js} +3 -3
- package/dist/{chunk-HAP7UTZS.js → chunk-MHA2WB7S.js} +1 -1
- package/dist/chunk-PPD3HHNJ.js +44 -0
- package/dist/{chunk-TNG3EWFN.js → chunk-Q4QULGAV.js} +1 -1
- package/dist/{chunk-FCB6J5YC.js → chunk-R6AYBATA.js} +1 -1
- package/dist/{chunk-BBK3TTU2.js → chunk-RRZZ7TFJ.js} +1 -1
- package/dist/{chunk-KJFKEKTC.js → chunk-T6BNEQ2A.js} +23 -0
- package/dist/{chunk-VDHV6KAS.js → chunk-U2PZKYZM.js} +1 -1
- package/dist/{chunk-5Q64QDCR.js → chunk-UBUPETEQ.js} +2 -2
- package/dist/{chunk-IXZMT6IS.js → chunk-ULITFWLG.js} +8 -8
- package/dist/{chunk-EQJZS2LE.js → chunk-WYVKPLVT.js} +1 -1
- package/dist/{chunk-7OYGFJYZ.js → chunk-ZUKH2NPJ.js} +18 -0
- package/dist/src/index.js +7 -6
- 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
|
-
**
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
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
|
|
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
|
-
|
|
270
|
-
|
|
271
|
-
|
|
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
|
-
|
|
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
|
-
|
|
292
|
-
|
|
293
|
-
|
|
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
|
-
|
|
303
|
-
|
|
304
|
-
|
|
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
|
-
|
|
318
|
-
|
|
319
|
-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
7
|
+
} from "../chunk-WYVKPLVT.js";
|
|
12
8
|
import {
|
|
13
9
|
removeGeminiSettings
|
|
14
|
-
} from "../chunk-
|
|
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-
|
|
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-
|
|
45
|
-
import "../chunk-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
11
|
+
import "../chunk-T6BNEQ2A.js";
|
|
11
12
|
import {
|
|
12
13
|
getMachineId
|
|
13
14
|
} from "../chunk-RN3NOEJF.js";
|