@pushary/agent-hooks 0.80.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 +17 -0
- package/data/SKILL.md +53 -115
- package/data/cursor-plugin/skills/pushary/SKILL.md +52 -114
- package/data/vscode-plugin/skills/pushary/SKILL.md +52 -114
- package/dist/bin/pushary-bell-hook.js +1 -1
- package/dist/bin/pushary-bell.js +1 -1
- package/dist/{chunk-22EQOB2T.js → chunk-DNFYUBRP.js} +34 -15
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.80.1
|
|
4
|
+
|
|
5
|
+
### The bell's heartbeat directory is private to you now
|
|
6
|
+
|
|
7
|
+
`pushary bell` counts how many agents are live by keeping one empty file
|
|
8
|
+
per session in the system temp directory. On macOS that is already a
|
|
9
|
+
per-user directory, so this was fine. On Linux it is the shared `/tmp`,
|
|
10
|
+
where it was not: the first user to create `pushary-bell` owned it, and
|
|
11
|
+
every other user on the box got a permission error and silently counted
|
|
12
|
+
zero agents. Anyone who could write to it could also inflate the count
|
|
13
|
+
and trigger the upgrade line.
|
|
14
|
+
|
|
15
|
+
The directory is now per uid and `0700`, the heartbeats inside it `0600`,
|
|
16
|
+
and the sweep judges an entry with `lstat` so a planted symlink is dropped
|
|
17
|
+
rather than followed. Nothing about the bell itself changes, and it still
|
|
18
|
+
makes no network calls.
|
|
19
|
+
|
|
3
20
|
## 0.80.0
|
|
4
21
|
|
|
5
22
|
### A bell, free, for when you only need to know it finished
|
package/data/SKILL.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: pushary
|
|
3
|
-
version: 0.
|
|
4
|
-
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. 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
|
+
version: 0.9.0
|
|
4
|
+
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.
|
|
5
5
|
metadata:
|
|
6
6
|
hermes:
|
|
7
7
|
tags: [notifications, push, human-in-the-loop, alerts, permissions, mcp]
|
|
@@ -41,6 +41,19 @@ Pushary is a hosted service. It costs $9.99/mo after a 3-day trial, and the tria
|
|
|
41
41
|
|
|
42
42
|
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.
|
|
43
43
|
|
|
44
|
+
## Plan the Questions Before You Start
|
|
45
|
+
|
|
46
|
+
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.
|
|
47
|
+
|
|
48
|
+
Before a run of more than a step or two, work out where you will need a human, then fold those points together:
|
|
49
|
+
|
|
50
|
+
- **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.
|
|
51
|
+
- **One `select` carrying the real options beats three sequential `confirm`s.** Same information, a third of the interruptions.
|
|
52
|
+
- **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.
|
|
53
|
+
- **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.
|
|
54
|
+
|
|
55
|
+
`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.
|
|
56
|
+
|
|
44
57
|
## When to Use
|
|
45
58
|
|
|
46
59
|
**Send a notification when:**
|
|
@@ -119,54 +132,23 @@ npx @pushary/agent-hooks@latest doctor
|
|
|
119
132
|
|
|
120
133
|
## Tools
|
|
121
134
|
|
|
135
|
+
Every parameter and every returned field is described in each tool's own schema,
|
|
136
|
+
which your client already has and which is always current. What follows is only
|
|
137
|
+
what a schema cannot tell you: when to reach for a tool, what its result means for
|
|
138
|
+
what you do next, and the shapes that are easy to get wrong.
|
|
139
|
+
|
|
122
140
|
### send_notification
|
|
123
141
|
|
|
124
142
|
Send a one-way push notification to the user. Optionally include structured context for a rich detail page.
|
|
125
143
|
|
|
126
|
-
**
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
| iconUrl | string | No | Custom notification icon URL |
|
|
135
|
-
| imageUrl | string | No | Large image shown in the notification |
|
|
136
|
-
| sessionId | string | No | Opaque per-session id of the sending agent, so parallel sessions are attributed separately (max 128 chars) |
|
|
137
|
-
| machineId | string | No | Stable machine id of the sending agent, so two machines never collapse into one session (max 128 chars) |
|
|
138
|
-
| subscriberIds | string[] | No | Target specific subscriber IDs |
|
|
139
|
-
| externalIds | string[] | No | Target by external IDs |
|
|
140
|
-
| tags | string[] | No | Target by subscriber tags |
|
|
141
|
-
| context | object | Yes for task updates | Structured context for a rich detail page (see below). `context.type` marks the notification a task update, and the user's setting for where task updates land can only route one that says so. |
|
|
142
|
-
|
|
143
|
-
**Context object:**
|
|
144
|
-
|
|
145
|
-
| Name | Type | Description |
|
|
146
|
-
|------|------|-------------|
|
|
147
|
-
| type | "task_complete" / "error" / "info" | The kind of notification |
|
|
148
|
-
| summary | string | Short summary of what happened |
|
|
149
|
-
| details | string[] | Bullet-point details |
|
|
150
|
-
| filesChanged | string[] | List of files that were changed |
|
|
151
|
-
| errorMessage | string | Error message (for error type) |
|
|
152
|
-
| errorFile | string | File path where the error occurred |
|
|
153
|
-
| nextSteps | string | Suggested next steps for the user |
|
|
154
|
-
| askQuestion | object | Embed a decision prompt in the notification (see below) |
|
|
155
|
-
|
|
156
|
-
**Embedded askQuestion:**
|
|
157
|
-
|
|
158
|
-
| Name | Type | Description |
|
|
159
|
-
|------|------|-------------|
|
|
160
|
-
| question | string | A follow-up question shown below the context |
|
|
161
|
-
| type | "confirm" / "select" / "input" | Question type (default: confirm) |
|
|
162
|
-
| options | string[] | Options for select type (2-6 items) |
|
|
163
|
-
|
|
164
|
-
When `askQuestion` is provided, the response includes a `linkedCorrelationId` you pass to `wait_for_answer`.
|
|
165
|
-
|
|
166
|
-
**Returns:**
|
|
167
|
-
- `delivery` - per-channel result: `{ "web": { "recipients": <n> }, "mobile": { "recipients": <n> } }` (each channel may also include a `status` like `no_recipients` or `not_configured`)
|
|
168
|
-
- `sent` - total devices reached across all channels
|
|
169
|
-
- `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
|
|
144
|
+
`context.type` is what marks a notification a **task update**, and the user's
|
|
145
|
+
setting for where task updates land can only route one that says so. A
|
|
146
|
+
notification sent without it reaches them wherever the default sends it.
|
|
147
|
+
|
|
148
|
+
On a long run where the user is likely away, prefer `context.askQuestion` over a
|
|
149
|
+
blocking `ask_user`. They get an ordinary push and answer whenever they next pick
|
|
150
|
+
up their phone, rather than you holding a 55-second wait open against someone who
|
|
151
|
+
is not there. Poll the returned `linkedCorrelationId` when you need the result.
|
|
170
152
|
|
|
171
153
|
**Example - task completed with context:**
|
|
172
154
|
|
|
@@ -209,36 +191,15 @@ When `askQuestion` is provided, the response includes a `linkedCorrelationId` yo
|
|
|
209
191
|
|
|
210
192
|
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.
|
|
211
193
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
| wait | boolean | No | Wait for the answer before returning (default: true). Set false for manual polling. |
|
|
222
|
-
| timeoutMs | integer | No | Max wait time in ms (max 55000). Uses site policy if omitted. |
|
|
223
|
-
| agentName | string | No | Identifies which agent is asking. Format: "{Agent} - {project}" (e.g., "Claude Code - myproject") |
|
|
224
|
-
| sessionId | string | No | Opaque per-session id of the asking agent, so parallel sessions are attributed separately (max 128 chars) |
|
|
225
|
-
| machineId | string | No | Stable machine id of the asking agent, so two machines never collapse into one session (max 128 chars) |
|
|
226
|
-
| toolName | string | No | The tool this approval is for (e.g. "Bash"), so the user can choose to always-allow it (max 100 chars) |
|
|
227
|
-
| 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) |
|
|
228
|
-
| callbackUrl | string | No | Webhook URL to POST the answer to when the user responds |
|
|
229
|
-
| subscriberIds | string[] | No | Target specific subscriber IDs |
|
|
230
|
-
| externalIds | string[] | No | Target by external IDs |
|
|
231
|
-
| tags | string[] | No | Target by subscriber tags |
|
|
232
|
-
|
|
233
|
-
**Returns (when wait=true, default):**
|
|
234
|
-
- `{ "answered": true, "value": "yes", "correlationId": "uuid" }` - user responded
|
|
235
|
-
- `{ "answered": false, "timedOut": true, "correlationId": "uuid" }` - timeout reached
|
|
236
|
-
|
|
237
|
-
**Returns (when wait=false):**
|
|
238
|
-
- `{ "correlationId": "uuid", "status": "pending", "expiresInSeconds": 600 }` - use `wait_for_answer` to poll
|
|
239
|
-
|
|
240
|
-
**Returns (when the site policy is notify_only):**
|
|
241
|
-
- `{ "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.
|
|
194
|
+
Always read `answered` rather than assuming the call blocked. It comes back false
|
|
195
|
+
in three different situations that mean different things: the wait timed out and
|
|
196
|
+
the question is still live (`timedOut`), the site policy is notify_only so nothing
|
|
197
|
+
was awaited (`status: "notified"`), or you passed `wait: false` yourself
|
|
198
|
+
(`status: "pending"`). All three leave a `correlationId` you can poll.
|
|
199
|
+
|
|
200
|
+
Pass `toolName` and `toolTarget` whenever the question is an approval for a tool
|
|
201
|
+
call. They are what let the user turn a repeated approval into an always-allow
|
|
202
|
+
rule, so an approval you label once is an approval they never see again.
|
|
242
203
|
|
|
243
204
|
**Example - confirm (yes/no):**
|
|
244
205
|
|
|
@@ -277,28 +238,19 @@ Send a question to the user via push notification and wait for their answer. By
|
|
|
277
238
|
|
|
278
239
|
### wait_for_answer
|
|
279
240
|
|
|
280
|
-
Poll for the user's response to a question sent via `ask_user` with `wait: false
|
|
281
|
-
|
|
282
|
-
**Parameters:**
|
|
241
|
+
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.
|
|
283
242
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
**Returns:**
|
|
290
|
-
- `{ "answered": true, "value": "yes" }` - user responded
|
|
291
|
-
- `{ "answered": false }` - timeout reached, no answer yet
|
|
243
|
+
A single call waits at most 55 seconds but the question stays answerable for 10
|
|
244
|
+
minutes, so one empty return is not a refusal. Retry with the same
|
|
245
|
+
`correlationId` up to three times at `timeoutMs: 55000` before treating it as
|
|
246
|
+
unanswered.
|
|
292
247
|
|
|
293
248
|
### cancel_question
|
|
294
249
|
|
|
295
250
|
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).
|
|
296
251
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
| Name | Type | Required | Description |
|
|
300
|
-
|------|------|----------|-------------|
|
|
301
|
-
| correlationId | string (uuid) | Yes | The correlationId of the question to cancel |
|
|
252
|
+
A stale approval arriving twenty minutes later is worse than no approval, because
|
|
253
|
+
it reads as consent to work that has already moved on.
|
|
302
254
|
|
|
303
255
|
### propose_scope
|
|
304
256
|
|
|
@@ -308,21 +260,13 @@ The user sees the paths you intend to change, the areas you promise to leave alo
|
|
|
308
260
|
|
|
309
261
|
Use glob syntax (`src/**`, `**/*.test.ts`). Shell commands are **not** scoped here; they stay governed by the permission policy.
|
|
310
262
|
|
|
311
|
-
|
|
263
|
+
`ratified` and `answered` are separate on purpose. Answered but not ratified means
|
|
264
|
+
the user declined: ask what scope they want, and do **not** proceed as if they had
|
|
265
|
+
agreed. Not answered means the scope is simply not in force.
|
|
312
266
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
| sessionId | string | Yes | Your per-session id. A scope with no session cannot be enforced and must never leak into another run |
|
|
317
|
-
| allowedPaths | string[] | No | Globs you intend to change. Omit to propose no path restriction, which the user is told plainly |
|
|
318
|
-
| offLimitsPaths | string[] | No | Globs you promise not to touch. These win wherever they overlap `allowedPaths` |
|
|
319
|
-
| agentName | string | No | Name of the agent asking, format `"{Agent} - {project}"` |
|
|
320
|
-
| timeoutMs | integer | No | How long this call blocks, max 55000 |
|
|
321
|
-
|
|
322
|
-
**Returns:**
|
|
323
|
-
- `{ "ratified": true, "answered": true, "value": "yes", "contract": {...} }` - the contract is live
|
|
324
|
-
- `{ "ratified": false, "answered": true, "value": "no" }` - the user declined. Ask what scope they want; do **not** proceed as if they agreed
|
|
325
|
-
- `{ "ratified": false, "answered": false }` - no answer yet. The scope is **not** in force
|
|
267
|
+
Omitting `allowedPaths` proposes no path restriction, and the user is told that
|
|
268
|
+
plainly as "this agent is asking to touch anything", so omit it only when you mean
|
|
269
|
+
it.
|
|
326
270
|
|
|
327
271
|
**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.
|
|
328
272
|
|
|
@@ -334,15 +278,9 @@ Scope lives for the session only and is never inherited by another run.
|
|
|
334
278
|
|
|
335
279
|
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.
|
|
336
280
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|------|------|----------|-------------|
|
|
341
|
-
| status | "active" / "idle" / "waiting" / "errored" | No | Filter to sessions with this computed status |
|
|
342
|
-
| limit | integer | No | Max number of sessions to return, most recently seen first (1-100) |
|
|
343
|
-
|
|
344
|
-
**Returns:**
|
|
345
|
-
- `{ "sessions": [...], "pendingQuestions": [...] }` - current sessions and open questions for the site
|
|
281
|
+
Check it before asking when you are one of several agents: if another session is
|
|
282
|
+
already blocked on a question, adding a second one competes for the same
|
|
283
|
+
attention rather than getting you answered sooner.
|
|
346
284
|
|
|
347
285
|
## Permission Gating (REQUIRED)
|
|
348
286
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: pushary
|
|
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. 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
|
|
@@ -34,6 +34,19 @@ Pushary is a hosted service. It costs $9.99/mo after a 3-day trial, and the tria
|
|
|
34
34
|
|
|
35
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.
|
|
36
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
|
+
|
|
37
50
|
## When to Use
|
|
38
51
|
|
|
39
52
|
**Send a notification when:**
|
|
@@ -112,54 +125,23 @@ npx @pushary/agent-hooks@latest doctor
|
|
|
112
125
|
|
|
113
126
|
## Tools
|
|
114
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
|
+
|
|
115
133
|
### send_notification
|
|
116
134
|
|
|
117
135
|
Send a one-way push notification to the user. Optionally include structured context for a rich detail page.
|
|
118
136
|
|
|
119
|
-
**
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
| iconUrl | string | No | Custom notification icon URL |
|
|
128
|
-
| imageUrl | string | No | Large image shown in the notification |
|
|
129
|
-
| sessionId | string | No | Opaque per-session id of the sending agent, so parallel sessions are attributed separately (max 128 chars) |
|
|
130
|
-
| machineId | string | No | Stable machine id of the sending agent, so two machines never collapse into one session (max 128 chars) |
|
|
131
|
-
| subscriberIds | string[] | No | Target specific subscriber IDs |
|
|
132
|
-
| externalIds | string[] | No | Target by external IDs |
|
|
133
|
-
| tags | string[] | No | Target by subscriber tags |
|
|
134
|
-
| context | object | Yes for task updates | Structured context for a rich detail page (see below). `context.type` marks the notification a task update, and the user's setting for where task updates land can only route one that says so. |
|
|
135
|
-
|
|
136
|
-
**Context object:**
|
|
137
|
-
|
|
138
|
-
| Name | Type | Description |
|
|
139
|
-
|------|------|-------------|
|
|
140
|
-
| type | "task_complete" / "error" / "info" | The kind of notification |
|
|
141
|
-
| summary | string | Short summary of what happened |
|
|
142
|
-
| details | string[] | Bullet-point details |
|
|
143
|
-
| filesChanged | string[] | List of files that were changed |
|
|
144
|
-
| errorMessage | string | Error message (for error type) |
|
|
145
|
-
| errorFile | string | File path where the error occurred |
|
|
146
|
-
| nextSteps | string | Suggested next steps for the user |
|
|
147
|
-
| askQuestion | object | Embed a decision prompt in the notification (see below) |
|
|
148
|
-
|
|
149
|
-
**Embedded askQuestion:**
|
|
150
|
-
|
|
151
|
-
| Name | Type | Description |
|
|
152
|
-
|------|------|-------------|
|
|
153
|
-
| question | string | A follow-up question shown below the context |
|
|
154
|
-
| type | "confirm" / "select" / "input" | Question type (default: confirm) |
|
|
155
|
-
| options | string[] | Options for select type (2-6 items) |
|
|
156
|
-
|
|
157
|
-
When `askQuestion` is provided, the response includes a `linkedCorrelationId` you pass to `wait_for_answer`.
|
|
158
|
-
|
|
159
|
-
**Returns:**
|
|
160
|
-
- `delivery` - per-channel result: `{ "web": { "recipients": <n> }, "mobile": { "recipients": <n> } }` (each channel may also include a `status` like `no_recipients` or `not_configured`)
|
|
161
|
-
- `sent` - total devices reached across all channels
|
|
162
|
-
- `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.
|
|
163
145
|
|
|
164
146
|
**Example - task completed with context:**
|
|
165
147
|
|
|
@@ -202,36 +184,15 @@ When `askQuestion` is provided, the response includes a `linkedCorrelationId` yo
|
|
|
202
184
|
|
|
203
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.
|
|
204
186
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
| wait | boolean | No | Wait for the answer before returning (default: true). Set false for manual polling. |
|
|
215
|
-
| timeoutMs | integer | No | Max wait time in ms (max 55000). Uses site policy if omitted. |
|
|
216
|
-
| agentName | string | No | Identifies which agent is asking. Format: "{Agent} - {project}" (e.g., "Claude Code - myproject") |
|
|
217
|
-
| sessionId | string | No | Opaque per-session id of the asking agent, so parallel sessions are attributed separately (max 128 chars) |
|
|
218
|
-
| machineId | string | No | Stable machine id of the asking agent, so two machines never collapse into one session (max 128 chars) |
|
|
219
|
-
| toolName | string | No | The tool this approval is for (e.g. "Bash"), so the user can choose to always-allow it (max 100 chars) |
|
|
220
|
-
| 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) |
|
|
221
|
-
| callbackUrl | string | No | Webhook URL to POST the answer to when the user responds |
|
|
222
|
-
| subscriberIds | string[] | No | Target specific subscriber IDs |
|
|
223
|
-
| externalIds | string[] | No | Target by external IDs |
|
|
224
|
-
| tags | string[] | No | Target by subscriber tags |
|
|
225
|
-
|
|
226
|
-
**Returns (when wait=true, default):**
|
|
227
|
-
- `{ "answered": true, "value": "yes", "correlationId": "uuid" }` - user responded
|
|
228
|
-
- `{ "answered": false, "timedOut": true, "correlationId": "uuid" }` - timeout reached
|
|
229
|
-
|
|
230
|
-
**Returns (when wait=false):**
|
|
231
|
-
- `{ "correlationId": "uuid", "status": "pending", "expiresInSeconds": 600 }` - use `wait_for_answer` to poll
|
|
232
|
-
|
|
233
|
-
**Returns (when the site policy is notify_only):**
|
|
234
|
-
- `{ "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.
|
|
235
196
|
|
|
236
197
|
**Example - confirm (yes/no):**
|
|
237
198
|
|
|
@@ -270,28 +231,19 @@ Send a question to the user via push notification and wait for their answer. By
|
|
|
270
231
|
|
|
271
232
|
### wait_for_answer
|
|
272
233
|
|
|
273
|
-
Poll for the user's response to a question sent via `ask_user` with `wait: false
|
|
274
|
-
|
|
275
|
-
**Parameters:**
|
|
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.
|
|
276
235
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
**Returns:**
|
|
283
|
-
- `{ "answered": true, "value": "yes" }` - user responded
|
|
284
|
-
- `{ "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.
|
|
285
240
|
|
|
286
241
|
### cancel_question
|
|
287
242
|
|
|
288
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).
|
|
289
244
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
| Name | Type | Required | Description |
|
|
293
|
-
|------|------|----------|-------------|
|
|
294
|
-
| 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.
|
|
295
247
|
|
|
296
248
|
### propose_scope
|
|
297
249
|
|
|
@@ -301,21 +253,13 @@ The user sees the paths you intend to change, the areas you promise to leave alo
|
|
|
301
253
|
|
|
302
254
|
Use glob syntax (`src/**`, `**/*.test.ts`). Shell commands are **not** scoped here; they stay governed by the permission policy.
|
|
303
255
|
|
|
304
|
-
|
|
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.
|
|
305
259
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
| sessionId | string | Yes | Your per-session id. A scope with no session cannot be enforced and must never leak into another run |
|
|
310
|
-
| allowedPaths | string[] | No | Globs you intend to change. Omit to propose no path restriction, which the user is told plainly |
|
|
311
|
-
| offLimitsPaths | string[] | No | Globs you promise not to touch. These win wherever they overlap `allowedPaths` |
|
|
312
|
-
| agentName | string | No | Name of the agent asking, format `"{Agent} - {project}"` |
|
|
313
|
-
| timeoutMs | integer | No | How long this call blocks, max 55000 |
|
|
314
|
-
|
|
315
|
-
**Returns:**
|
|
316
|
-
- `{ "ratified": true, "answered": true, "value": "yes", "contract": {...} }` - the contract is live
|
|
317
|
-
- `{ "ratified": false, "answered": true, "value": "no" }` - the user declined. Ask what scope they want; do **not** proceed as if they agreed
|
|
318
|
-
- `{ "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.
|
|
319
263
|
|
|
320
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.
|
|
321
265
|
|
|
@@ -327,15 +271,9 @@ Scope lives for the session only and is never inherited by another run.
|
|
|
327
271
|
|
|
328
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.
|
|
329
273
|
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|------|------|----------|-------------|
|
|
334
|
-
| status | "active" / "idle" / "waiting" / "errored" | No | Filter to sessions with this computed status |
|
|
335
|
-
| limit | integer | No | Max number of sessions to return, most recently seen first (1-100) |
|
|
336
|
-
|
|
337
|
-
**Returns:**
|
|
338
|
-
- `{ "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.
|
|
339
277
|
|
|
340
278
|
## Permission Gating (REQUIRED)
|
|
341
279
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: pushary
|
|
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. 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
|
|
@@ -34,6 +34,19 @@ Pushary is a hosted service. It costs $9.99/mo after a 3-day trial, and the tria
|
|
|
34
34
|
|
|
35
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.
|
|
36
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
|
+
|
|
37
50
|
## When to Use
|
|
38
51
|
|
|
39
52
|
**Send a notification when:**
|
|
@@ -112,54 +125,23 @@ npx @pushary/agent-hooks@latest doctor
|
|
|
112
125
|
|
|
113
126
|
## Tools
|
|
114
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
|
+
|
|
115
133
|
### send_notification
|
|
116
134
|
|
|
117
135
|
Send a one-way push notification to the user. Optionally include structured context for a rich detail page.
|
|
118
136
|
|
|
119
|
-
**
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
| iconUrl | string | No | Custom notification icon URL |
|
|
128
|
-
| imageUrl | string | No | Large image shown in the notification |
|
|
129
|
-
| sessionId | string | No | Opaque per-session id of the sending agent, so parallel sessions are attributed separately (max 128 chars) |
|
|
130
|
-
| machineId | string | No | Stable machine id of the sending agent, so two machines never collapse into one session (max 128 chars) |
|
|
131
|
-
| subscriberIds | string[] | No | Target specific subscriber IDs |
|
|
132
|
-
| externalIds | string[] | No | Target by external IDs |
|
|
133
|
-
| tags | string[] | No | Target by subscriber tags |
|
|
134
|
-
| context | object | Yes for task updates | Structured context for a rich detail page (see below). `context.type` marks the notification a task update, and the user's setting for where task updates land can only route one that says so. |
|
|
135
|
-
|
|
136
|
-
**Context object:**
|
|
137
|
-
|
|
138
|
-
| Name | Type | Description |
|
|
139
|
-
|------|------|-------------|
|
|
140
|
-
| type | "task_complete" / "error" / "info" | The kind of notification |
|
|
141
|
-
| summary | string | Short summary of what happened |
|
|
142
|
-
| details | string[] | Bullet-point details |
|
|
143
|
-
| filesChanged | string[] | List of files that were changed |
|
|
144
|
-
| errorMessage | string | Error message (for error type) |
|
|
145
|
-
| errorFile | string | File path where the error occurred |
|
|
146
|
-
| nextSteps | string | Suggested next steps for the user |
|
|
147
|
-
| askQuestion | object | Embed a decision prompt in the notification (see below) |
|
|
148
|
-
|
|
149
|
-
**Embedded askQuestion:**
|
|
150
|
-
|
|
151
|
-
| Name | Type | Description |
|
|
152
|
-
|------|------|-------------|
|
|
153
|
-
| question | string | A follow-up question shown below the context |
|
|
154
|
-
| type | "confirm" / "select" / "input" | Question type (default: confirm) |
|
|
155
|
-
| options | string[] | Options for select type (2-6 items) |
|
|
156
|
-
|
|
157
|
-
When `askQuestion` is provided, the response includes a `linkedCorrelationId` you pass to `wait_for_answer`.
|
|
158
|
-
|
|
159
|
-
**Returns:**
|
|
160
|
-
- `delivery` - per-channel result: `{ "web": { "recipients": <n> }, "mobile": { "recipients": <n> } }` (each channel may also include a `status` like `no_recipients` or `not_configured`)
|
|
161
|
-
- `sent` - total devices reached across all channels
|
|
162
|
-
- `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.
|
|
163
145
|
|
|
164
146
|
**Example - task completed with context:**
|
|
165
147
|
|
|
@@ -202,36 +184,15 @@ When `askQuestion` is provided, the response includes a `linkedCorrelationId` yo
|
|
|
202
184
|
|
|
203
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.
|
|
204
186
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
| wait | boolean | No | Wait for the answer before returning (default: true). Set false for manual polling. |
|
|
215
|
-
| timeoutMs | integer | No | Max wait time in ms (max 55000). Uses site policy if omitted. |
|
|
216
|
-
| agentName | string | No | Identifies which agent is asking. Format: "{Agent} - {project}" (e.g., "Claude Code - myproject") |
|
|
217
|
-
| sessionId | string | No | Opaque per-session id of the asking agent, so parallel sessions are attributed separately (max 128 chars) |
|
|
218
|
-
| machineId | string | No | Stable machine id of the asking agent, so two machines never collapse into one session (max 128 chars) |
|
|
219
|
-
| toolName | string | No | The tool this approval is for (e.g. "Bash"), so the user can choose to always-allow it (max 100 chars) |
|
|
220
|
-
| 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) |
|
|
221
|
-
| callbackUrl | string | No | Webhook URL to POST the answer to when the user responds |
|
|
222
|
-
| subscriberIds | string[] | No | Target specific subscriber IDs |
|
|
223
|
-
| externalIds | string[] | No | Target by external IDs |
|
|
224
|
-
| tags | string[] | No | Target by subscriber tags |
|
|
225
|
-
|
|
226
|
-
**Returns (when wait=true, default):**
|
|
227
|
-
- `{ "answered": true, "value": "yes", "correlationId": "uuid" }` - user responded
|
|
228
|
-
- `{ "answered": false, "timedOut": true, "correlationId": "uuid" }` - timeout reached
|
|
229
|
-
|
|
230
|
-
**Returns (when wait=false):**
|
|
231
|
-
- `{ "correlationId": "uuid", "status": "pending", "expiresInSeconds": 600 }` - use `wait_for_answer` to poll
|
|
232
|
-
|
|
233
|
-
**Returns (when the site policy is notify_only):**
|
|
234
|
-
- `{ "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.
|
|
235
196
|
|
|
236
197
|
**Example - confirm (yes/no):**
|
|
237
198
|
|
|
@@ -270,28 +231,19 @@ Send a question to the user via push notification and wait for their answer. By
|
|
|
270
231
|
|
|
271
232
|
### wait_for_answer
|
|
272
233
|
|
|
273
|
-
Poll for the user's response to a question sent via `ask_user` with `wait: false
|
|
274
|
-
|
|
275
|
-
**Parameters:**
|
|
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.
|
|
276
235
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
**Returns:**
|
|
283
|
-
- `{ "answered": true, "value": "yes" }` - user responded
|
|
284
|
-
- `{ "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.
|
|
285
240
|
|
|
286
241
|
### cancel_question
|
|
287
242
|
|
|
288
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).
|
|
289
244
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
| Name | Type | Required | Description |
|
|
293
|
-
|------|------|----------|-------------|
|
|
294
|
-
| 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.
|
|
295
247
|
|
|
296
248
|
### propose_scope
|
|
297
249
|
|
|
@@ -301,21 +253,13 @@ The user sees the paths you intend to change, the areas you promise to leave alo
|
|
|
301
253
|
|
|
302
254
|
Use glob syntax (`src/**`, `**/*.test.ts`). Shell commands are **not** scoped here; they stay governed by the permission policy.
|
|
303
255
|
|
|
304
|
-
|
|
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.
|
|
305
259
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
| sessionId | string | Yes | Your per-session id. A scope with no session cannot be enforced and must never leak into another run |
|
|
310
|
-
| allowedPaths | string[] | No | Globs you intend to change. Omit to propose no path restriction, which the user is told plainly |
|
|
311
|
-
| offLimitsPaths | string[] | No | Globs you promise not to touch. These win wherever they overlap `allowedPaths` |
|
|
312
|
-
| agentName | string | No | Name of the agent asking, format `"{Agent} - {project}"` |
|
|
313
|
-
| timeoutMs | integer | No | How long this call blocks, max 55000 |
|
|
314
|
-
|
|
315
|
-
**Returns:**
|
|
316
|
-
- `{ "ratified": true, "answered": true, "value": "yes", "contract": {...} }` - the contract is live
|
|
317
|
-
- `{ "ratified": false, "answered": true, "value": "no" }` - the user declined. Ask what scope they want; do **not** proceed as if they agreed
|
|
318
|
-
- `{ "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.
|
|
319
263
|
|
|
320
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.
|
|
321
265
|
|
|
@@ -327,15 +271,9 @@ Scope lives for the session only and is never inherited by another run.
|
|
|
327
271
|
|
|
328
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.
|
|
329
273
|
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|------|------|----------|-------------|
|
|
334
|
-
| status | "active" / "idle" / "waiting" / "errored" | No | Filter to sessions with this computed status |
|
|
335
|
-
| limit | integer | No | Max number of sessions to return, most recently seen first (1-100) |
|
|
336
|
-
|
|
337
|
-
**Returns:**
|
|
338
|
-
- `{ "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.
|
|
339
277
|
|
|
340
278
|
## Permission Gating (REQUIRED)
|
|
341
279
|
|
package/dist/bin/pushary-bell.js
CHANGED
|
@@ -3,20 +3,37 @@ import {
|
|
|
3
3
|
} from "./chunk-E6I3DBJW.js";
|
|
4
4
|
|
|
5
5
|
// src/bell/fleet.ts
|
|
6
|
-
import {
|
|
6
|
+
import { closeSync, constants, futimesSync, lstatSync, mkdirSync, openSync, readdirSync, unlinkSync } from "fs";
|
|
7
7
|
import { createHash } from "crypto";
|
|
8
|
-
import { tmpdir } from "os";
|
|
8
|
+
import { tmpdir, userInfo } from "os";
|
|
9
9
|
import { join } from "path";
|
|
10
|
-
var
|
|
10
|
+
var ownerTag = () => {
|
|
11
|
+
try {
|
|
12
|
+
const { uid } = userInfo();
|
|
13
|
+
return typeof uid === "number" && uid >= 0 ? String(uid) : "shared";
|
|
14
|
+
} catch {
|
|
15
|
+
return "shared";
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
var BELL_DIR = join(tmpdir(), `pushary-bell-${ownerTag()}`);
|
|
19
|
+
var DIR_MODE = 448;
|
|
20
|
+
var FILE_MODE = 384;
|
|
11
21
|
var SESSION_TTL_MS = 10 * 60 * 1e3;
|
|
12
22
|
var sessionFile = (sessionId) => join(BELL_DIR, createHash("sha256").update(sessionId).digest("hex").slice(0, 16));
|
|
13
23
|
var heartbeat = (sessionId, now = Date.now()) => {
|
|
14
24
|
try {
|
|
15
|
-
mkdirSync(BELL_DIR, { recursive: true });
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
25
|
+
mkdirSync(BELL_DIR, { recursive: true, mode: DIR_MODE });
|
|
26
|
+
const fd = openSync(
|
|
27
|
+
sessionFile(sessionId),
|
|
28
|
+
constants.O_WRONLY | constants.O_CREAT | constants.O_NOFOLLOW,
|
|
29
|
+
FILE_MODE
|
|
30
|
+
);
|
|
31
|
+
try {
|
|
32
|
+
const seconds = now / 1e3;
|
|
33
|
+
futimesSync(fd, seconds, seconds);
|
|
34
|
+
} finally {
|
|
35
|
+
closeSync(fd);
|
|
36
|
+
}
|
|
20
37
|
} catch {
|
|
21
38
|
}
|
|
22
39
|
};
|
|
@@ -26,7 +43,9 @@ var liveAgentCount = (now = Date.now()) => {
|
|
|
26
43
|
for (const name of readdirSync(BELL_DIR)) {
|
|
27
44
|
const path = join(BELL_DIR, name);
|
|
28
45
|
try {
|
|
29
|
-
|
|
46
|
+
const entry = lstatSync(path);
|
|
47
|
+
if (!entry.isFile()) unlinkSync(path);
|
|
48
|
+
else if (now - entry.mtimeMs < SESSION_TTL_MS) live += 1;
|
|
30
49
|
else unlinkSync(path);
|
|
31
50
|
} catch {
|
|
32
51
|
}
|
|
@@ -43,16 +62,16 @@ var recordAndCount = (sessionId, now = Date.now()) => {
|
|
|
43
62
|
|
|
44
63
|
// src/bell/ring.ts
|
|
45
64
|
import { execFile } from "child_process";
|
|
46
|
-
import { openSync, writeSync, closeSync } from "fs";
|
|
65
|
+
import { openSync as openSync2, writeSync, closeSync as closeSync2 } from "fs";
|
|
47
66
|
var platformOf = (value = process.platform) => value === "darwin" || value === "linux" || value === "win32" ? value : "other";
|
|
48
67
|
var writeBell = () => {
|
|
49
68
|
try {
|
|
50
|
-
const fd =
|
|
69
|
+
const fd = openSync2("/dev/tty", "w");
|
|
51
70
|
try {
|
|
52
71
|
writeSync(fd, "\x07");
|
|
53
72
|
return true;
|
|
54
73
|
} finally {
|
|
55
|
-
|
|
74
|
+
closeSync2(fd);
|
|
56
75
|
}
|
|
57
76
|
} catch {
|
|
58
77
|
try {
|
|
@@ -96,7 +115,7 @@ var ring = (notice, deps = {}) => {
|
|
|
96
115
|
};
|
|
97
116
|
|
|
98
117
|
// src/bell/upgrade.ts
|
|
99
|
-
import { existsSync, mkdirSync as mkdirSync2, statSync
|
|
118
|
+
import { existsSync, mkdirSync as mkdirSync2, statSync, writeFileSync } from "fs";
|
|
100
119
|
import { tmpdir as tmpdir2 } from "os";
|
|
101
120
|
import { join as join2 } from "path";
|
|
102
121
|
var DEFAULT_UPGRADE_THRESHOLD = 3;
|
|
@@ -109,7 +128,7 @@ var upgradeThreshold = (env = process.env) => {
|
|
|
109
128
|
};
|
|
110
129
|
var readLastShown = () => {
|
|
111
130
|
try {
|
|
112
|
-
return existsSync(UPGRADE_MARKER) ?
|
|
131
|
+
return existsSync(UPGRADE_MARKER) ? statSync(UPGRADE_MARKER).mtimeMs : null;
|
|
113
132
|
} catch {
|
|
114
133
|
return null;
|
|
115
134
|
}
|
|
@@ -117,7 +136,7 @@ var readLastShown = () => {
|
|
|
117
136
|
var writeLastShown = (at) => {
|
|
118
137
|
try {
|
|
119
138
|
mkdirSync2(tmpdir2(), { recursive: true });
|
|
120
|
-
|
|
139
|
+
writeFileSync(UPGRADE_MARKER, String(at), "utf-8");
|
|
121
140
|
} catch {
|
|
122
141
|
}
|
|
123
142
|
};
|