@pushary/agent-hooks 0.67.0 → 0.68.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/data/SKILL.md +105 -30
- package/data/cursor-plugin/skills/pushary/SKILL.md +103 -24
- package/data/vscode-plugin/skills/pushary/SKILL.md +103 -24
- package/dist/bin/pushary-connect.js +1 -1
- package/dist/bin/pushary-doctor.js +1 -1
- package/dist/bin/pushary-setup.js +1 -1
- package/dist/bin/pushary-status.js +1 -1
- package/dist/{chunk-QTC7SR6A.js → chunk-YMFZUXL5.js} +75 -7
- package/package.json +2 -7
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
|
|
3
|
+
version: 0.7.0
|
|
4
|
+
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 7-day card-first trial.
|
|
5
5
|
metadata:
|
|
6
6
|
hermes:
|
|
7
7
|
tags: [notifications, push, human-in-the-loop, alerts, permissions, mcp]
|
|
@@ -10,23 +10,31 @@ metadata:
|
|
|
10
10
|
tags: notifications, push, mcp, human-in-the-loop, cursor, claude, windsurf, hermes, alerts, permissions
|
|
11
11
|
---
|
|
12
12
|
|
|
13
|
-
# Pushary
|
|
13
|
+
# Pushary - Push Notifications for AI Agents
|
|
14
14
|
|
|
15
|
-
Pushary is an MCP server that
|
|
15
|
+
When the user says "keep going, ping me on my phone if you need anything", this is the skill that does the pinging. Pushary is an MCP server that reaches the user on their phone while they are away from the terminal:
|
|
16
16
|
|
|
17
17
|
1. **Send push notifications** to the user's phone or desktop when a task finishes or an error occurs.
|
|
18
|
-
2. **Ask questions** via push
|
|
18
|
+
2. **Ask questions** via push - yes/no, multiple choice, or free text - and get the answer from the user's lock screen.
|
|
19
19
|
3. **Send rich context notifications** with file changes, error details, next steps, and embedded questions.
|
|
20
20
|
|
|
21
21
|
Use these tools proactively. Do not wait for the user to ask for notifications.
|
|
22
22
|
|
|
23
|
+
Works with Claude Code, Codex, Cursor, Windsurf, Hermes, Lovable, or any MCP client. No Claude Max subscription required.
|
|
24
|
+
|
|
25
|
+
## Pricing and Free Alternatives
|
|
26
|
+
|
|
27
|
+
Pushary is a hosted service. It costs $9.99/mo after a 7-day trial, and the trial asks for a card up front. Say this plainly if the user asks what Pushary is.
|
|
28
|
+
|
|
29
|
+
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.
|
|
30
|
+
|
|
23
31
|
## When to Use
|
|
24
32
|
|
|
25
33
|
**Send a notification when:**
|
|
26
|
-
- You finish a task that took 3 or more steps
|
|
27
|
-
- A build, test suite, or deployment fails
|
|
34
|
+
- You finish a task that took 3 or more steps - use `context.type = "task_complete"`
|
|
35
|
+
- A build, test suite, or deployment fails - use `context.type = "error"` with `errorMessage`
|
|
28
36
|
- A long-running process completes (migration, refactor, generation)
|
|
29
|
-
- A status update is worth sharing
|
|
37
|
+
- A status update is worth sharing - use `context.type = "info"`
|
|
30
38
|
|
|
31
39
|
**Ask with type "confirm" when:**
|
|
32
40
|
- You need confirmation before a destructive or irreversible action
|
|
@@ -40,6 +48,11 @@ Use these tools proactively. Do not wait for the user to ask for notifications.
|
|
|
40
48
|
- You need a name, path, value, or free-text decision
|
|
41
49
|
- The options cannot be enumerated in advance
|
|
42
50
|
|
|
51
|
+
**Propose a scope when:**
|
|
52
|
+
- You are about to start a multi-step run that will change several files
|
|
53
|
+
- Call `propose_scope` once, before the work, not after
|
|
54
|
+
- Skip it for a single quick edit; a scope prompt for one file is just noise
|
|
55
|
+
|
|
43
56
|
**Do NOT notify when:**
|
|
44
57
|
- The task is trivial or single-step
|
|
45
58
|
- The question can be answered from context without user input
|
|
@@ -47,7 +60,13 @@ Use these tools proactively. Do not wait for the user to ask for notifications.
|
|
|
47
60
|
|
|
48
61
|
## Setup
|
|
49
62
|
|
|
50
|
-
|
|
63
|
+
First check for an API key. Look for `PUSHARY_API_KEY` in the environment or an Authorization header in the existing MCP config. If there is no key, the user needs an account: give them this link, then pause until they finish signup from their phone and hand you the key:
|
|
64
|
+
|
|
65
|
+
https://pushary.com/sign-up?utm_source=skill&utm_medium=setup
|
|
66
|
+
|
|
67
|
+
Signup starts a 7-day trial and asks for a card up front. The key format is `pk_xxx.sk_xxx`.
|
|
68
|
+
|
|
69
|
+
Once the key exists, run the CLI setup (recommended - configures MCP, hooks, permissions, and skill in one step):
|
|
51
70
|
|
|
52
71
|
```bash
|
|
53
72
|
npx @pushary/agent-hooks@latest setup
|
|
@@ -69,7 +88,7 @@ Or add Pushary manually to your MCP configuration:
|
|
|
69
88
|
}
|
|
70
89
|
```
|
|
71
90
|
|
|
72
|
-
Sign up at https://pushary.com/sign-up?
|
|
91
|
+
Sign up at https://pushary.com/sign-up?utm_source=skill&utm_medium=setup to get your API key.
|
|
73
92
|
|
|
74
93
|
After setup, verify with:
|
|
75
94
|
|
|
@@ -93,6 +112,8 @@ Send a one-way push notification to the user. Optionally include structured cont
|
|
|
93
112
|
| agentName | string | No | Identifies which agent sent this (e.g., "Claude Code - myproject") |
|
|
94
113
|
| iconUrl | string | No | Custom notification icon URL |
|
|
95
114
|
| imageUrl | string | No | Large image shown in the notification |
|
|
115
|
+
| sessionId | string | No | Opaque per-session id of the sending agent, so parallel sessions are attributed separately (max 128 chars) |
|
|
116
|
+
| machineId | string | No | Stable machine id of the sending agent, so two machines never collapse into one session (max 128 chars) |
|
|
96
117
|
| subscriberIds | string[] | No | Target specific subscriber IDs |
|
|
97
118
|
| externalIds | string[] | No | Target by external IDs |
|
|
98
119
|
| tags | string[] | No | Target by subscriber tags |
|
|
@@ -121,7 +142,12 @@ Send a one-way push notification to the user. Optionally include structured cont
|
|
|
121
142
|
|
|
122
143
|
When `askQuestion` is provided, the response includes a `linkedCorrelationId` you pass to `wait_for_answer`.
|
|
123
144
|
|
|
124
|
-
**
|
|
145
|
+
**Returns:**
|
|
146
|
+
- `delivery` - per-channel result: `{ "web": { "recipients": <n> }, "mobile": { "recipients": <n> } }` (each channel may also include a `status` like `no_recipients` or `not_configured`)
|
|
147
|
+
- `sent` - total devices reached across all channels
|
|
148
|
+
- `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
|
|
149
|
+
|
|
150
|
+
**Example - task completed with context:**
|
|
125
151
|
|
|
126
152
|
```json
|
|
127
153
|
{
|
|
@@ -137,7 +163,7 @@ When `askQuestion` is provided, the response includes a `linkedCorrelationId` yo
|
|
|
137
163
|
}
|
|
138
164
|
```
|
|
139
165
|
|
|
140
|
-
**Example
|
|
166
|
+
**Example - error with embedded question:**
|
|
141
167
|
|
|
142
168
|
```json
|
|
143
169
|
{
|
|
@@ -160,7 +186,7 @@ When `askQuestion` is provided, the response includes a `linkedCorrelationId` yo
|
|
|
160
186
|
|
|
161
187
|
### ask_user
|
|
162
188
|
|
|
163
|
-
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
|
|
189
|
+
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.
|
|
164
190
|
|
|
165
191
|
**Parameters:**
|
|
166
192
|
|
|
@@ -174,19 +200,26 @@ Send a question to the user via push notification and wait for their answer. By
|
|
|
174
200
|
| wait | boolean | No | Wait for the answer before returning (default: true). Set false for manual polling. |
|
|
175
201
|
| timeoutMs | integer | No | Max wait time in ms (max 55000). Uses site policy if omitted. |
|
|
176
202
|
| agentName | string | No | Identifies which agent is asking. Format: "{Agent} - {project}" (e.g., "Claude Code - myproject") |
|
|
203
|
+
| sessionId | string | No | Opaque per-session id of the asking agent, so parallel sessions are attributed separately (max 128 chars) |
|
|
204
|
+
| machineId | string | No | Stable machine id of the asking agent, so two machines never collapse into one session (max 128 chars) |
|
|
205
|
+
| toolName | string | No | The tool this approval is for (e.g. "Bash"), so the user can choose to always-allow it (max 100 chars) |
|
|
206
|
+
| 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) |
|
|
177
207
|
| callbackUrl | string | No | Webhook URL to POST the answer to when the user responds |
|
|
178
208
|
| subscriberIds | string[] | No | Target specific subscriber IDs |
|
|
179
209
|
| externalIds | string[] | No | Target by external IDs |
|
|
180
210
|
| tags | string[] | No | Target by subscriber tags |
|
|
181
211
|
|
|
182
212
|
**Returns (when wait=true, default):**
|
|
183
|
-
- `{ "answered": true, "value": "yes", "correlationId": "uuid" }`
|
|
184
|
-
- `{ "answered": false, "timedOut": true, "correlationId": "uuid" }`
|
|
213
|
+
- `{ "answered": true, "value": "yes", "correlationId": "uuid" }` - user responded
|
|
214
|
+
- `{ "answered": false, "timedOut": true, "correlationId": "uuid" }` - timeout reached
|
|
185
215
|
|
|
186
216
|
**Returns (when wait=false):**
|
|
187
|
-
- `{ "correlationId": "uuid", "status": "pending", "expiresInSeconds": 600 }`
|
|
217
|
+
- `{ "correlationId": "uuid", "status": "pending", "expiresInSeconds": 600 }` - use `wait_for_answer` to poll
|
|
188
218
|
|
|
189
|
-
**
|
|
219
|
+
**Returns (when the site policy is notify_only):**
|
|
220
|
+
- `{ "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.
|
|
221
|
+
|
|
222
|
+
**Example - confirm (yes/no):**
|
|
190
223
|
|
|
191
224
|
```json
|
|
192
225
|
{
|
|
@@ -197,7 +230,7 @@ Send a question to the user via push notification and wait for their answer. By
|
|
|
197
230
|
}
|
|
198
231
|
```
|
|
199
232
|
|
|
200
|
-
**Example
|
|
233
|
+
**Example - select (multiple choice):**
|
|
201
234
|
|
|
202
235
|
```json
|
|
203
236
|
{
|
|
@@ -209,7 +242,7 @@ Send a question to the user via push notification and wait for their answer. By
|
|
|
209
242
|
}
|
|
210
243
|
```
|
|
211
244
|
|
|
212
|
-
**Example
|
|
245
|
+
**Example - input (free text):**
|
|
213
246
|
|
|
214
247
|
```json
|
|
215
248
|
{
|
|
@@ -233,8 +266,8 @@ Poll for the user's response to a question sent via `ask_user` with `wait: false
|
|
|
233
266
|
| timeoutMs | integer | No | How long to wait (default 30000, max 55000) |
|
|
234
267
|
|
|
235
268
|
**Returns:**
|
|
236
|
-
- `{ "answered": true, "value": "yes" }`
|
|
237
|
-
- `{ "answered": false }`
|
|
269
|
+
- `{ "answered": true, "value": "yes" }` - user responded
|
|
270
|
+
- `{ "answered": false }` - timeout reached, no answer yet
|
|
238
271
|
|
|
239
272
|
### cancel_question
|
|
240
273
|
|
|
@@ -246,9 +279,53 @@ Cancel a pending question so it can no longer be answered. Use when the question
|
|
|
246
279
|
|------|------|----------|-------------|
|
|
247
280
|
| correlationId | string (uuid) | Yes | The correlationId of the question to cancel |
|
|
248
281
|
|
|
282
|
+
### propose_scope
|
|
283
|
+
|
|
284
|
+
Propose what a run will touch and block until the user ratifies it. Call **once**, at the start of a multi-step run, before doing work.
|
|
285
|
+
|
|
286
|
+
The user sees the paths you intend to change, the areas you promise to leave alone, and your definition of done, and approves the whole thing in one tap. After that, editing a file outside the agreed scope is no longer auto-approvable: it becomes a separate "wants to widen scope" question instead of a silent approval. Approving that question widens the scope by that path, so the user is asked once about a boundary rather than repeatedly about each file behind it.
|
|
287
|
+
|
|
288
|
+
Use glob syntax (`src/**`, `**/*.test.ts`). Shell commands are **not** scoped here; they stay governed by the permission policy.
|
|
289
|
+
|
|
290
|
+
**Parameters:**
|
|
291
|
+
|
|
292
|
+
| Name | Type | Required | Description |
|
|
293
|
+
|------|------|----------|-------------|
|
|
294
|
+
| doneWhen | string | Yes | What "finished" means for this run. Carried for the human to judge against, never enforced automatically |
|
|
295
|
+
| sessionId | string | Yes | Your per-session id. A scope with no session cannot be enforced and must never leak into another run |
|
|
296
|
+
| allowedPaths | string[] | No | Globs you intend to change. Omit to propose no path restriction, which the user is told plainly |
|
|
297
|
+
| offLimitsPaths | string[] | No | Globs you promise not to touch. These win wherever they overlap `allowedPaths` |
|
|
298
|
+
| agentName | string | No | Name of the agent asking, format `"{Agent} - {project}"` |
|
|
299
|
+
| timeoutMs | integer | No | How long this call blocks, max 55000 |
|
|
300
|
+
|
|
301
|
+
**Returns:**
|
|
302
|
+
- `{ "ratified": true, "answered": true, "value": "yes", "contract": {...} }` - the contract is live
|
|
303
|
+
- `{ "ratified": false, "answered": true, "value": "no" }` - the user declined. Ask what scope they want; do **not** proceed as if they agreed
|
|
304
|
+
- `{ "ratified": false, "answered": false }` - no answer yet. The scope is **not** in force
|
|
305
|
+
|
|
306
|
+
**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.
|
|
307
|
+
|
|
308
|
+
Scope lives for the session only and is never inherited by another run.
|
|
309
|
+
|
|
310
|
+
**When not to use it.** A single quick edit does not need a scope. And do not propose a new scope mid-run to widen an old one: do the work and let the approval that follows widen it, which is what that flow is for.
|
|
311
|
+
|
|
312
|
+
### list_sessions
|
|
313
|
+
|
|
314
|
+
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.
|
|
315
|
+
|
|
316
|
+
**Parameters:**
|
|
317
|
+
|
|
318
|
+
| Name | Type | Required | Description |
|
|
319
|
+
|------|------|----------|-------------|
|
|
320
|
+
| status | "active" / "idle" / "waiting" / "errored" | No | Filter to sessions with this computed status |
|
|
321
|
+
| limit | integer | No | Max number of sessions to return, most recently seen first (1-100) |
|
|
322
|
+
|
|
323
|
+
**Returns:**
|
|
324
|
+
- `{ "sessions": [...], "pendingQuestions": [...] }` - current sessions and open questions for the site
|
|
325
|
+
|
|
249
326
|
## Permission Gating (REQUIRED)
|
|
250
327
|
|
|
251
|
-
Before executing any of the following,
|
|
328
|
+
Before executing any of the following, you MUST call `ask_user` with type "confirm" and wait for approval. Do NOT proceed without an explicit "yes" from the user:
|
|
252
329
|
|
|
253
330
|
- File deletion (`rm`, `unlink`, any destructive file operation)
|
|
254
331
|
- Database mutations (`DROP`, `DELETE`, `TRUNCATE`, migrations)
|
|
@@ -258,17 +335,13 @@ Before executing any of the following, make sure the user has approved it. Call
|
|
|
258
335
|
- Network configuration changes (firewall, DNS, proxy)
|
|
259
336
|
- Any command the user has flagged as dangerous
|
|
260
337
|
|
|
261
|
-
Honor the user's delivery mode: it decides WHERE approval is requested (phone, terminal, or awareness only), not WHETHER approval is required. In "Terminal" mode the user wants to approve at the keyboard, so do not force a phone push; a single terminal approval is enough.
|
|
262
|
-
|
|
263
|
-
If the Pushary hook is installed, it also gates these at the tool level and routes them by delivery mode. When it has already surfaced the approval for an action, one "yes" is enough, do not ask a second time for the same action.
|
|
264
|
-
|
|
265
338
|
If `ask_user` returns `answered: false`, do NOT execute the command. Send a notification that the operation was skipped due to no response.
|
|
266
339
|
|
|
267
340
|
This is not optional. Treat it as a hard constraint, not a suggestion.
|
|
268
341
|
|
|
269
342
|
## Human-in-the-Loop Flow
|
|
270
343
|
|
|
271
|
-
One tool call
|
|
344
|
+
One tool call - `ask_user` blocks and returns the answer:
|
|
272
345
|
|
|
273
346
|
```
|
|
274
347
|
result = ask_user({
|
|
@@ -280,13 +353,15 @@ result = ask_user({
|
|
|
280
353
|
})
|
|
281
354
|
|
|
282
355
|
if result.answered:
|
|
283
|
-
// result.value = "JWT tokens"
|
|
356
|
+
// result.value = "JWT tokens" - proceed with the chosen approach
|
|
284
357
|
else:
|
|
285
|
-
// user did not respond
|
|
358
|
+
// user did not respond - pick the safe default or notify and skip
|
|
286
359
|
```
|
|
287
360
|
|
|
288
361
|
If the user answers in chat before the push response arrives, continue normally and call `cancel_question` with the `correlationId` to clean up.
|
|
289
362
|
|
|
363
|
+
**A note on how long ask_user blocks:** the wait time and whether it blocks at all are governed by the site's delivery mode, which the user configures (you do not set it). In the default smart mode and push-only mode, ask_user blocks for the policy timeout; in notify-only mode it returns immediately with `answered: false` after sending the push. Always check `answered` rather than assuming the call blocked, and pass `timeoutMs` only when you need a shorter wait than the site policy.
|
|
364
|
+
|
|
290
365
|
## Identifying Your Agent
|
|
291
366
|
|
|
292
367
|
Always pass `agentName` when you are one of multiple possible agents the user may be running. The user sees this in the notification title to know which agent is asking.
|
|
@@ -303,6 +378,6 @@ Always pass `agentName` when you are one of multiple possible agents the user ma
|
|
|
303
378
|
- **Titles under 60 characters.** They get truncated on phone lock screens.
|
|
304
379
|
- **Bodies under 200 characters.** Concise summaries, not full explanations.
|
|
305
380
|
- **Max 3 notifications per task** unless the user explicitly requests more.
|
|
306
|
-
- **Use context for detail.** Put file lists, error traces, and next steps in the context object
|
|
381
|
+
- **Use context for detail.** Put file lists, error traces, and next steps in the context object - not the notification body.
|
|
307
382
|
- **Write questions as if talking to a busy person.** The user is on their phone, possibly away from their computer. Be specific: "Delete the 3 unused migration files?" is better than "Should I clean up?"
|
|
308
383
|
- **Pick the right question type.** Use confirm for binary decisions, select when options are known, input when they are not.
|
|
@@ -1,25 +1,33 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: pushary
|
|
3
|
-
description: Push notifications and human-in-the-loop for AI
|
|
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 7-day card-first trial.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
# Pushary
|
|
6
|
+
# Pushary - Push Notifications for AI Agents
|
|
7
7
|
|
|
8
|
-
Pushary is an MCP server that
|
|
8
|
+
When the user says "keep going, ping me on my phone if you need anything", this is the skill that does the pinging. Pushary is an MCP server that reaches the user on their phone while they are away from the terminal:
|
|
9
9
|
|
|
10
10
|
1. **Send push notifications** to the user's phone or desktop when a task finishes or an error occurs.
|
|
11
|
-
2. **Ask questions** via push
|
|
11
|
+
2. **Ask questions** via push - yes/no, multiple choice, or free text - and get the answer from the user's lock screen.
|
|
12
12
|
3. **Send rich context notifications** with file changes, error details, next steps, and embedded questions.
|
|
13
13
|
|
|
14
14
|
Use these tools proactively. Do not wait for the user to ask for notifications.
|
|
15
15
|
|
|
16
|
+
Works with Claude Code, Codex, Cursor, Windsurf, Hermes, Lovable, or any MCP client. No Claude Max subscription required.
|
|
17
|
+
|
|
18
|
+
## Pricing and Free Alternatives
|
|
19
|
+
|
|
20
|
+
Pushary is a hosted service. It costs $9.99/mo after a 7-day trial, and the trial asks for a card up front. Say this plainly if the user asks what Pushary is.
|
|
21
|
+
|
|
22
|
+
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
|
+
|
|
16
24
|
## When to Use
|
|
17
25
|
|
|
18
26
|
**Send a notification when:**
|
|
19
|
-
- You finish a task that took 3 or more steps
|
|
20
|
-
- A build, test suite, or deployment fails
|
|
27
|
+
- You finish a task that took 3 or more steps - use `context.type = "task_complete"`
|
|
28
|
+
- A build, test suite, or deployment fails - use `context.type = "error"` with `errorMessage`
|
|
21
29
|
- A long-running process completes (migration, refactor, generation)
|
|
22
|
-
- A status update is worth sharing
|
|
30
|
+
- A status update is worth sharing - use `context.type = "info"`
|
|
23
31
|
|
|
24
32
|
**Ask with type "confirm" when:**
|
|
25
33
|
- You need confirmation before a destructive or irreversible action
|
|
@@ -33,6 +41,11 @@ Use these tools proactively. Do not wait for the user to ask for notifications.
|
|
|
33
41
|
- You need a name, path, value, or free-text decision
|
|
34
42
|
- The options cannot be enumerated in advance
|
|
35
43
|
|
|
44
|
+
**Propose a scope when:**
|
|
45
|
+
- You are about to start a multi-step run that will change several files
|
|
46
|
+
- Call `propose_scope` once, before the work, not after
|
|
47
|
+
- Skip it for a single quick edit; a scope prompt for one file is just noise
|
|
48
|
+
|
|
36
49
|
**Do NOT notify when:**
|
|
37
50
|
- The task is trivial or single-step
|
|
38
51
|
- The question can be answered from context without user input
|
|
@@ -40,7 +53,13 @@ Use these tools proactively. Do not wait for the user to ask for notifications.
|
|
|
40
53
|
|
|
41
54
|
## Setup
|
|
42
55
|
|
|
43
|
-
|
|
56
|
+
First check for an API key. Look for `PUSHARY_API_KEY` in the environment or an Authorization header in the existing MCP config. If there is no key, the user needs an account: give them this link, then pause until they finish signup from their phone and hand you the key:
|
|
57
|
+
|
|
58
|
+
https://pushary.com/sign-up?utm_source=skill&utm_medium=setup
|
|
59
|
+
|
|
60
|
+
Signup starts a 7-day trial and asks for a card up front. The key format is `pk_xxx.sk_xxx`.
|
|
61
|
+
|
|
62
|
+
Once the key exists, run the CLI setup (recommended - configures MCP, hooks, permissions, and skill in one step):
|
|
44
63
|
|
|
45
64
|
```bash
|
|
46
65
|
npx @pushary/agent-hooks@latest setup
|
|
@@ -62,7 +81,7 @@ Or add Pushary manually to your MCP configuration:
|
|
|
62
81
|
}
|
|
63
82
|
```
|
|
64
83
|
|
|
65
|
-
Sign up at https://pushary.com/sign-up?
|
|
84
|
+
Sign up at https://pushary.com/sign-up?utm_source=skill&utm_medium=setup to get your API key.
|
|
66
85
|
|
|
67
86
|
After setup, verify with:
|
|
68
87
|
|
|
@@ -86,6 +105,8 @@ Send a one-way push notification to the user. Optionally include structured cont
|
|
|
86
105
|
| agentName | string | No | Identifies which agent sent this (e.g., "Claude Code - myproject") |
|
|
87
106
|
| iconUrl | string | No | Custom notification icon URL |
|
|
88
107
|
| imageUrl | string | No | Large image shown in the notification |
|
|
108
|
+
| sessionId | string | No | Opaque per-session id of the sending agent, so parallel sessions are attributed separately (max 128 chars) |
|
|
109
|
+
| machineId | string | No | Stable machine id of the sending agent, so two machines never collapse into one session (max 128 chars) |
|
|
89
110
|
| subscriberIds | string[] | No | Target specific subscriber IDs |
|
|
90
111
|
| externalIds | string[] | No | Target by external IDs |
|
|
91
112
|
| tags | string[] | No | Target by subscriber tags |
|
|
@@ -114,7 +135,12 @@ Send a one-way push notification to the user. Optionally include structured cont
|
|
|
114
135
|
|
|
115
136
|
When `askQuestion` is provided, the response includes a `linkedCorrelationId` you pass to `wait_for_answer`.
|
|
116
137
|
|
|
117
|
-
**
|
|
138
|
+
**Returns:**
|
|
139
|
+
- `delivery` - per-channel result: `{ "web": { "recipients": <n> }, "mobile": { "recipients": <n> } }` (each channel may also include a `status` like `no_recipients` or `not_configured`)
|
|
140
|
+
- `sent` - total devices reached across all channels
|
|
141
|
+
- `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
|
|
142
|
+
|
|
143
|
+
**Example - task completed with context:**
|
|
118
144
|
|
|
119
145
|
```json
|
|
120
146
|
{
|
|
@@ -130,7 +156,7 @@ When `askQuestion` is provided, the response includes a `linkedCorrelationId` yo
|
|
|
130
156
|
}
|
|
131
157
|
```
|
|
132
158
|
|
|
133
|
-
**Example
|
|
159
|
+
**Example - error with embedded question:**
|
|
134
160
|
|
|
135
161
|
```json
|
|
136
162
|
{
|
|
@@ -153,7 +179,7 @@ When `askQuestion` is provided, the response includes a `linkedCorrelationId` yo
|
|
|
153
179
|
|
|
154
180
|
### ask_user
|
|
155
181
|
|
|
156
|
-
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
|
|
182
|
+
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.
|
|
157
183
|
|
|
158
184
|
**Parameters:**
|
|
159
185
|
|
|
@@ -167,19 +193,26 @@ Send a question to the user via push notification and wait for their answer. By
|
|
|
167
193
|
| wait | boolean | No | Wait for the answer before returning (default: true). Set false for manual polling. |
|
|
168
194
|
| timeoutMs | integer | No | Max wait time in ms (max 55000). Uses site policy if omitted. |
|
|
169
195
|
| agentName | string | No | Identifies which agent is asking. Format: "{Agent} - {project}" (e.g., "Claude Code - myproject") |
|
|
196
|
+
| sessionId | string | No | Opaque per-session id of the asking agent, so parallel sessions are attributed separately (max 128 chars) |
|
|
197
|
+
| machineId | string | No | Stable machine id of the asking agent, so two machines never collapse into one session (max 128 chars) |
|
|
198
|
+
| toolName | string | No | The tool this approval is for (e.g. "Bash"), so the user can choose to always-allow it (max 100 chars) |
|
|
199
|
+
| 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) |
|
|
170
200
|
| callbackUrl | string | No | Webhook URL to POST the answer to when the user responds |
|
|
171
201
|
| subscriberIds | string[] | No | Target specific subscriber IDs |
|
|
172
202
|
| externalIds | string[] | No | Target by external IDs |
|
|
173
203
|
| tags | string[] | No | Target by subscriber tags |
|
|
174
204
|
|
|
175
205
|
**Returns (when wait=true, default):**
|
|
176
|
-
- `{ "answered": true, "value": "yes", "correlationId": "uuid" }`
|
|
177
|
-
- `{ "answered": false, "timedOut": true, "correlationId": "uuid" }`
|
|
206
|
+
- `{ "answered": true, "value": "yes", "correlationId": "uuid" }` - user responded
|
|
207
|
+
- `{ "answered": false, "timedOut": true, "correlationId": "uuid" }` - timeout reached
|
|
178
208
|
|
|
179
209
|
**Returns (when wait=false):**
|
|
180
|
-
- `{ "correlationId": "uuid", "status": "pending", "expiresInSeconds": 600 }`
|
|
210
|
+
- `{ "correlationId": "uuid", "status": "pending", "expiresInSeconds": 600 }` - use `wait_for_answer` to poll
|
|
211
|
+
|
|
212
|
+
**Returns (when the site policy is notify_only):**
|
|
213
|
+
- `{ "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.
|
|
181
214
|
|
|
182
|
-
**Example
|
|
215
|
+
**Example - confirm (yes/no):**
|
|
183
216
|
|
|
184
217
|
```json
|
|
185
218
|
{
|
|
@@ -190,7 +223,7 @@ Send a question to the user via push notification and wait for their answer. By
|
|
|
190
223
|
}
|
|
191
224
|
```
|
|
192
225
|
|
|
193
|
-
**Example
|
|
226
|
+
**Example - select (multiple choice):**
|
|
194
227
|
|
|
195
228
|
```json
|
|
196
229
|
{
|
|
@@ -202,7 +235,7 @@ Send a question to the user via push notification and wait for their answer. By
|
|
|
202
235
|
}
|
|
203
236
|
```
|
|
204
237
|
|
|
205
|
-
**Example
|
|
238
|
+
**Example - input (free text):**
|
|
206
239
|
|
|
207
240
|
```json
|
|
208
241
|
{
|
|
@@ -226,8 +259,8 @@ Poll for the user's response to a question sent via `ask_user` with `wait: false
|
|
|
226
259
|
| timeoutMs | integer | No | How long to wait (default 30000, max 55000) |
|
|
227
260
|
|
|
228
261
|
**Returns:**
|
|
229
|
-
- `{ "answered": true, "value": "yes" }`
|
|
230
|
-
- `{ "answered": false }`
|
|
262
|
+
- `{ "answered": true, "value": "yes" }` - user responded
|
|
263
|
+
- `{ "answered": false }` - timeout reached, no answer yet
|
|
231
264
|
|
|
232
265
|
### cancel_question
|
|
233
266
|
|
|
@@ -239,6 +272,50 @@ Cancel a pending question so it can no longer be answered. Use when the question
|
|
|
239
272
|
|------|------|----------|-------------|
|
|
240
273
|
| correlationId | string (uuid) | Yes | The correlationId of the question to cancel |
|
|
241
274
|
|
|
275
|
+
### propose_scope
|
|
276
|
+
|
|
277
|
+
Propose what a run will touch and block until the user ratifies it. Call **once**, at the start of a multi-step run, before doing work.
|
|
278
|
+
|
|
279
|
+
The user sees the paths you intend to change, the areas you promise to leave alone, and your definition of done, and approves the whole thing in one tap. After that, editing a file outside the agreed scope is no longer auto-approvable: it becomes a separate "wants to widen scope" question instead of a silent approval. Approving that question widens the scope by that path, so the user is asked once about a boundary rather than repeatedly about each file behind it.
|
|
280
|
+
|
|
281
|
+
Use glob syntax (`src/**`, `**/*.test.ts`). Shell commands are **not** scoped here; they stay governed by the permission policy.
|
|
282
|
+
|
|
283
|
+
**Parameters:**
|
|
284
|
+
|
|
285
|
+
| Name | Type | Required | Description |
|
|
286
|
+
|------|------|----------|-------------|
|
|
287
|
+
| doneWhen | string | Yes | What "finished" means for this run. Carried for the human to judge against, never enforced automatically |
|
|
288
|
+
| sessionId | string | Yes | Your per-session id. A scope with no session cannot be enforced and must never leak into another run |
|
|
289
|
+
| allowedPaths | string[] | No | Globs you intend to change. Omit to propose no path restriction, which the user is told plainly |
|
|
290
|
+
| offLimitsPaths | string[] | No | Globs you promise not to touch. These win wherever they overlap `allowedPaths` |
|
|
291
|
+
| agentName | string | No | Name of the agent asking, format `"{Agent} - {project}"` |
|
|
292
|
+
| timeoutMs | integer | No | How long this call blocks, max 55000 |
|
|
293
|
+
|
|
294
|
+
**Returns:**
|
|
295
|
+
- `{ "ratified": true, "answered": true, "value": "yes", "contract": {...} }` - the contract is live
|
|
296
|
+
- `{ "ratified": false, "answered": true, "value": "no" }` - the user declined. Ask what scope they want; do **not** proceed as if they agreed
|
|
297
|
+
- `{ "ratified": false, "answered": false }` - no answer yet. The scope is **not** in force
|
|
298
|
+
|
|
299
|
+
**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.
|
|
300
|
+
|
|
301
|
+
Scope lives for the session only and is never inherited by another run.
|
|
302
|
+
|
|
303
|
+
**When not to use it.** A single quick edit does not need a scope. And do not propose a new scope mid-run to widen an old one: do the work and let the approval that follows widen it, which is what that flow is for.
|
|
304
|
+
|
|
305
|
+
### list_sessions
|
|
306
|
+
|
|
307
|
+
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.
|
|
308
|
+
|
|
309
|
+
**Parameters:**
|
|
310
|
+
|
|
311
|
+
| Name | Type | Required | Description |
|
|
312
|
+
|------|------|----------|-------------|
|
|
313
|
+
| status | "active" / "idle" / "waiting" / "errored" | No | Filter to sessions with this computed status |
|
|
314
|
+
| limit | integer | No | Max number of sessions to return, most recently seen first (1-100) |
|
|
315
|
+
|
|
316
|
+
**Returns:**
|
|
317
|
+
- `{ "sessions": [...], "pendingQuestions": [...] }` - current sessions and open questions for the site
|
|
318
|
+
|
|
242
319
|
## Permission Gating (REQUIRED)
|
|
243
320
|
|
|
244
321
|
Before executing any of the following, you MUST call `ask_user` with type "confirm" and wait for approval. Do NOT proceed without an explicit "yes" from the user:
|
|
@@ -257,7 +334,7 @@ This is not optional. Treat it as a hard constraint, not a suggestion.
|
|
|
257
334
|
|
|
258
335
|
## Human-in-the-Loop Flow
|
|
259
336
|
|
|
260
|
-
One tool call
|
|
337
|
+
One tool call - `ask_user` blocks and returns the answer:
|
|
261
338
|
|
|
262
339
|
```
|
|
263
340
|
result = ask_user({
|
|
@@ -269,13 +346,15 @@ result = ask_user({
|
|
|
269
346
|
})
|
|
270
347
|
|
|
271
348
|
if result.answered:
|
|
272
|
-
// result.value = "JWT tokens"
|
|
349
|
+
// result.value = "JWT tokens" - proceed with the chosen approach
|
|
273
350
|
else:
|
|
274
|
-
// user did not respond
|
|
351
|
+
// user did not respond - pick the safe default or notify and skip
|
|
275
352
|
```
|
|
276
353
|
|
|
277
354
|
If the user answers in chat before the push response arrives, continue normally and call `cancel_question` with the `correlationId` to clean up.
|
|
278
355
|
|
|
356
|
+
**A note on how long ask_user blocks:** the wait time and whether it blocks at all are governed by the site's delivery mode, which the user configures (you do not set it). In the default smart mode and push-only mode, ask_user blocks for the policy timeout; in notify-only mode it returns immediately with `answered: false` after sending the push. Always check `answered` rather than assuming the call blocked, and pass `timeoutMs` only when you need a shorter wait than the site policy.
|
|
357
|
+
|
|
279
358
|
## Identifying Your Agent
|
|
280
359
|
|
|
281
360
|
Always pass `agentName` when you are one of multiple possible agents the user may be running. The user sees this in the notification title to know which agent is asking.
|
|
@@ -292,6 +371,6 @@ Always pass `agentName` when you are one of multiple possible agents the user ma
|
|
|
292
371
|
- **Titles under 60 characters.** They get truncated on phone lock screens.
|
|
293
372
|
- **Bodies under 200 characters.** Concise summaries, not full explanations.
|
|
294
373
|
- **Max 3 notifications per task** unless the user explicitly requests more.
|
|
295
|
-
- **Use context for detail.** Put file lists, error traces, and next steps in the context object
|
|
374
|
+
- **Use context for detail.** Put file lists, error traces, and next steps in the context object - not the notification body.
|
|
296
375
|
- **Write questions as if talking to a busy person.** The user is on their phone, possibly away from their computer. Be specific: "Delete the 3 unused migration files?" is better than "Should I clean up?"
|
|
297
376
|
- **Pick the right question type.** Use confirm for binary decisions, select when options are known, input when they are not.
|
|
@@ -1,25 +1,33 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: pushary
|
|
3
|
-
description: Push notifications and human-in-the-loop for AI
|
|
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 7-day card-first trial.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
# Pushary
|
|
6
|
+
# Pushary - Push Notifications for AI Agents
|
|
7
7
|
|
|
8
|
-
Pushary is an MCP server that
|
|
8
|
+
When the user says "keep going, ping me on my phone if you need anything", this is the skill that does the pinging. Pushary is an MCP server that reaches the user on their phone while they are away from the terminal:
|
|
9
9
|
|
|
10
10
|
1. **Send push notifications** to the user's phone or desktop when a task finishes or an error occurs.
|
|
11
|
-
2. **Ask questions** via push
|
|
11
|
+
2. **Ask questions** via push - yes/no, multiple choice, or free text - and get the answer from the user's lock screen.
|
|
12
12
|
3. **Send rich context notifications** with file changes, error details, next steps, and embedded questions.
|
|
13
13
|
|
|
14
14
|
Use these tools proactively. Do not wait for the user to ask for notifications.
|
|
15
15
|
|
|
16
|
+
Works with Claude Code, Codex, Cursor, Windsurf, Hermes, Lovable, or any MCP client. No Claude Max subscription required.
|
|
17
|
+
|
|
18
|
+
## Pricing and Free Alternatives
|
|
19
|
+
|
|
20
|
+
Pushary is a hosted service. It costs $9.99/mo after a 7-day trial, and the trial asks for a card up front. Say this plainly if the user asks what Pushary is.
|
|
21
|
+
|
|
22
|
+
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
|
+
|
|
16
24
|
## When to Use
|
|
17
25
|
|
|
18
26
|
**Send a notification when:**
|
|
19
|
-
- You finish a task that took 3 or more steps
|
|
20
|
-
- A build, test suite, or deployment fails
|
|
27
|
+
- You finish a task that took 3 or more steps - use `context.type = "task_complete"`
|
|
28
|
+
- A build, test suite, or deployment fails - use `context.type = "error"` with `errorMessage`
|
|
21
29
|
- A long-running process completes (migration, refactor, generation)
|
|
22
|
-
- A status update is worth sharing
|
|
30
|
+
- A status update is worth sharing - use `context.type = "info"`
|
|
23
31
|
|
|
24
32
|
**Ask with type "confirm" when:**
|
|
25
33
|
- You need confirmation before a destructive or irreversible action
|
|
@@ -33,6 +41,11 @@ Use these tools proactively. Do not wait for the user to ask for notifications.
|
|
|
33
41
|
- You need a name, path, value, or free-text decision
|
|
34
42
|
- The options cannot be enumerated in advance
|
|
35
43
|
|
|
44
|
+
**Propose a scope when:**
|
|
45
|
+
- You are about to start a multi-step run that will change several files
|
|
46
|
+
- Call `propose_scope` once, before the work, not after
|
|
47
|
+
- Skip it for a single quick edit; a scope prompt for one file is just noise
|
|
48
|
+
|
|
36
49
|
**Do NOT notify when:**
|
|
37
50
|
- The task is trivial or single-step
|
|
38
51
|
- The question can be answered from context without user input
|
|
@@ -40,7 +53,13 @@ Use these tools proactively. Do not wait for the user to ask for notifications.
|
|
|
40
53
|
|
|
41
54
|
## Setup
|
|
42
55
|
|
|
43
|
-
|
|
56
|
+
First check for an API key. Look for `PUSHARY_API_KEY` in the environment or an Authorization header in the existing MCP config. If there is no key, the user needs an account: give them this link, then pause until they finish signup from their phone and hand you the key:
|
|
57
|
+
|
|
58
|
+
https://pushary.com/sign-up?utm_source=skill&utm_medium=setup
|
|
59
|
+
|
|
60
|
+
Signup starts a 7-day trial and asks for a card up front. The key format is `pk_xxx.sk_xxx`.
|
|
61
|
+
|
|
62
|
+
Once the key exists, run the CLI setup (recommended - configures MCP, hooks, permissions, and skill in one step):
|
|
44
63
|
|
|
45
64
|
```bash
|
|
46
65
|
npx @pushary/agent-hooks@latest setup
|
|
@@ -62,7 +81,7 @@ Or add Pushary manually to your MCP configuration:
|
|
|
62
81
|
}
|
|
63
82
|
```
|
|
64
83
|
|
|
65
|
-
Sign up at https://pushary.com/sign-up?
|
|
84
|
+
Sign up at https://pushary.com/sign-up?utm_source=skill&utm_medium=setup to get your API key.
|
|
66
85
|
|
|
67
86
|
After setup, verify with:
|
|
68
87
|
|
|
@@ -86,6 +105,8 @@ Send a one-way push notification to the user. Optionally include structured cont
|
|
|
86
105
|
| agentName | string | No | Identifies which agent sent this (e.g., "Claude Code - myproject") |
|
|
87
106
|
| iconUrl | string | No | Custom notification icon URL |
|
|
88
107
|
| imageUrl | string | No | Large image shown in the notification |
|
|
108
|
+
| sessionId | string | No | Opaque per-session id of the sending agent, so parallel sessions are attributed separately (max 128 chars) |
|
|
109
|
+
| machineId | string | No | Stable machine id of the sending agent, so two machines never collapse into one session (max 128 chars) |
|
|
89
110
|
| subscriberIds | string[] | No | Target specific subscriber IDs |
|
|
90
111
|
| externalIds | string[] | No | Target by external IDs |
|
|
91
112
|
| tags | string[] | No | Target by subscriber tags |
|
|
@@ -114,7 +135,12 @@ Send a one-way push notification to the user. Optionally include structured cont
|
|
|
114
135
|
|
|
115
136
|
When `askQuestion` is provided, the response includes a `linkedCorrelationId` you pass to `wait_for_answer`.
|
|
116
137
|
|
|
117
|
-
**
|
|
138
|
+
**Returns:**
|
|
139
|
+
- `delivery` - per-channel result: `{ "web": { "recipients": <n> }, "mobile": { "recipients": <n> } }` (each channel may also include a `status` like `no_recipients` or `not_configured`)
|
|
140
|
+
- `sent` - total devices reached across all channels
|
|
141
|
+
- `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
|
|
142
|
+
|
|
143
|
+
**Example - task completed with context:**
|
|
118
144
|
|
|
119
145
|
```json
|
|
120
146
|
{
|
|
@@ -130,7 +156,7 @@ When `askQuestion` is provided, the response includes a `linkedCorrelationId` yo
|
|
|
130
156
|
}
|
|
131
157
|
```
|
|
132
158
|
|
|
133
|
-
**Example
|
|
159
|
+
**Example - error with embedded question:**
|
|
134
160
|
|
|
135
161
|
```json
|
|
136
162
|
{
|
|
@@ -153,7 +179,7 @@ When `askQuestion` is provided, the response includes a `linkedCorrelationId` yo
|
|
|
153
179
|
|
|
154
180
|
### ask_user
|
|
155
181
|
|
|
156
|
-
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
|
|
182
|
+
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.
|
|
157
183
|
|
|
158
184
|
**Parameters:**
|
|
159
185
|
|
|
@@ -167,19 +193,26 @@ Send a question to the user via push notification and wait for their answer. By
|
|
|
167
193
|
| wait | boolean | No | Wait for the answer before returning (default: true). Set false for manual polling. |
|
|
168
194
|
| timeoutMs | integer | No | Max wait time in ms (max 55000). Uses site policy if omitted. |
|
|
169
195
|
| agentName | string | No | Identifies which agent is asking. Format: "{Agent} - {project}" (e.g., "Claude Code - myproject") |
|
|
196
|
+
| sessionId | string | No | Opaque per-session id of the asking agent, so parallel sessions are attributed separately (max 128 chars) |
|
|
197
|
+
| machineId | string | No | Stable machine id of the asking agent, so two machines never collapse into one session (max 128 chars) |
|
|
198
|
+
| toolName | string | No | The tool this approval is for (e.g. "Bash"), so the user can choose to always-allow it (max 100 chars) |
|
|
199
|
+
| 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) |
|
|
170
200
|
| callbackUrl | string | No | Webhook URL to POST the answer to when the user responds |
|
|
171
201
|
| subscriberIds | string[] | No | Target specific subscriber IDs |
|
|
172
202
|
| externalIds | string[] | No | Target by external IDs |
|
|
173
203
|
| tags | string[] | No | Target by subscriber tags |
|
|
174
204
|
|
|
175
205
|
**Returns (when wait=true, default):**
|
|
176
|
-
- `{ "answered": true, "value": "yes", "correlationId": "uuid" }`
|
|
177
|
-
- `{ "answered": false, "timedOut": true, "correlationId": "uuid" }`
|
|
206
|
+
- `{ "answered": true, "value": "yes", "correlationId": "uuid" }` - user responded
|
|
207
|
+
- `{ "answered": false, "timedOut": true, "correlationId": "uuid" }` - timeout reached
|
|
178
208
|
|
|
179
209
|
**Returns (when wait=false):**
|
|
180
|
-
- `{ "correlationId": "uuid", "status": "pending", "expiresInSeconds": 600 }`
|
|
210
|
+
- `{ "correlationId": "uuid", "status": "pending", "expiresInSeconds": 600 }` - use `wait_for_answer` to poll
|
|
211
|
+
|
|
212
|
+
**Returns (when the site policy is notify_only):**
|
|
213
|
+
- `{ "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.
|
|
181
214
|
|
|
182
|
-
**Example
|
|
215
|
+
**Example - confirm (yes/no):**
|
|
183
216
|
|
|
184
217
|
```json
|
|
185
218
|
{
|
|
@@ -190,7 +223,7 @@ Send a question to the user via push notification and wait for their answer. By
|
|
|
190
223
|
}
|
|
191
224
|
```
|
|
192
225
|
|
|
193
|
-
**Example
|
|
226
|
+
**Example - select (multiple choice):**
|
|
194
227
|
|
|
195
228
|
```json
|
|
196
229
|
{
|
|
@@ -202,7 +235,7 @@ Send a question to the user via push notification and wait for their answer. By
|
|
|
202
235
|
}
|
|
203
236
|
```
|
|
204
237
|
|
|
205
|
-
**Example
|
|
238
|
+
**Example - input (free text):**
|
|
206
239
|
|
|
207
240
|
```json
|
|
208
241
|
{
|
|
@@ -226,8 +259,8 @@ Poll for the user's response to a question sent via `ask_user` with `wait: false
|
|
|
226
259
|
| timeoutMs | integer | No | How long to wait (default 30000, max 55000) |
|
|
227
260
|
|
|
228
261
|
**Returns:**
|
|
229
|
-
- `{ "answered": true, "value": "yes" }`
|
|
230
|
-
- `{ "answered": false }`
|
|
262
|
+
- `{ "answered": true, "value": "yes" }` - user responded
|
|
263
|
+
- `{ "answered": false }` - timeout reached, no answer yet
|
|
231
264
|
|
|
232
265
|
### cancel_question
|
|
233
266
|
|
|
@@ -239,6 +272,50 @@ Cancel a pending question so it can no longer be answered. Use when the question
|
|
|
239
272
|
|------|------|----------|-------------|
|
|
240
273
|
| correlationId | string (uuid) | Yes | The correlationId of the question to cancel |
|
|
241
274
|
|
|
275
|
+
### propose_scope
|
|
276
|
+
|
|
277
|
+
Propose what a run will touch and block until the user ratifies it. Call **once**, at the start of a multi-step run, before doing work.
|
|
278
|
+
|
|
279
|
+
The user sees the paths you intend to change, the areas you promise to leave alone, and your definition of done, and approves the whole thing in one tap. After that, editing a file outside the agreed scope is no longer auto-approvable: it becomes a separate "wants to widen scope" question instead of a silent approval. Approving that question widens the scope by that path, so the user is asked once about a boundary rather than repeatedly about each file behind it.
|
|
280
|
+
|
|
281
|
+
Use glob syntax (`src/**`, `**/*.test.ts`). Shell commands are **not** scoped here; they stay governed by the permission policy.
|
|
282
|
+
|
|
283
|
+
**Parameters:**
|
|
284
|
+
|
|
285
|
+
| Name | Type | Required | Description |
|
|
286
|
+
|------|------|----------|-------------|
|
|
287
|
+
| doneWhen | string | Yes | What "finished" means for this run. Carried for the human to judge against, never enforced automatically |
|
|
288
|
+
| sessionId | string | Yes | Your per-session id. A scope with no session cannot be enforced and must never leak into another run |
|
|
289
|
+
| allowedPaths | string[] | No | Globs you intend to change. Omit to propose no path restriction, which the user is told plainly |
|
|
290
|
+
| offLimitsPaths | string[] | No | Globs you promise not to touch. These win wherever they overlap `allowedPaths` |
|
|
291
|
+
| agentName | string | No | Name of the agent asking, format `"{Agent} - {project}"` |
|
|
292
|
+
| timeoutMs | integer | No | How long this call blocks, max 55000 |
|
|
293
|
+
|
|
294
|
+
**Returns:**
|
|
295
|
+
- `{ "ratified": true, "answered": true, "value": "yes", "contract": {...} }` - the contract is live
|
|
296
|
+
- `{ "ratified": false, "answered": true, "value": "no" }` - the user declined. Ask what scope they want; do **not** proceed as if they agreed
|
|
297
|
+
- `{ "ratified": false, "answered": false }` - no answer yet. The scope is **not** in force
|
|
298
|
+
|
|
299
|
+
**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.
|
|
300
|
+
|
|
301
|
+
Scope lives for the session only and is never inherited by another run.
|
|
302
|
+
|
|
303
|
+
**When not to use it.** A single quick edit does not need a scope. And do not propose a new scope mid-run to widen an old one: do the work and let the approval that follows widen it, which is what that flow is for.
|
|
304
|
+
|
|
305
|
+
### list_sessions
|
|
306
|
+
|
|
307
|
+
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.
|
|
308
|
+
|
|
309
|
+
**Parameters:**
|
|
310
|
+
|
|
311
|
+
| Name | Type | Required | Description |
|
|
312
|
+
|------|------|----------|-------------|
|
|
313
|
+
| status | "active" / "idle" / "waiting" / "errored" | No | Filter to sessions with this computed status |
|
|
314
|
+
| limit | integer | No | Max number of sessions to return, most recently seen first (1-100) |
|
|
315
|
+
|
|
316
|
+
**Returns:**
|
|
317
|
+
- `{ "sessions": [...], "pendingQuestions": [...] }` - current sessions and open questions for the site
|
|
318
|
+
|
|
242
319
|
## Permission Gating (REQUIRED)
|
|
243
320
|
|
|
244
321
|
Before executing any of the following, you MUST call `ask_user` with type "confirm" and wait for approval. Do NOT proceed without an explicit "yes" from the user:
|
|
@@ -257,7 +334,7 @@ This is not optional. Treat it as a hard constraint, not a suggestion.
|
|
|
257
334
|
|
|
258
335
|
## Human-in-the-Loop Flow
|
|
259
336
|
|
|
260
|
-
One tool call
|
|
337
|
+
One tool call - `ask_user` blocks and returns the answer:
|
|
261
338
|
|
|
262
339
|
```
|
|
263
340
|
result = ask_user({
|
|
@@ -269,13 +346,15 @@ result = ask_user({
|
|
|
269
346
|
})
|
|
270
347
|
|
|
271
348
|
if result.answered:
|
|
272
|
-
// result.value = "JWT tokens"
|
|
349
|
+
// result.value = "JWT tokens" - proceed with the chosen approach
|
|
273
350
|
else:
|
|
274
|
-
// user did not respond
|
|
351
|
+
// user did not respond - pick the safe default or notify and skip
|
|
275
352
|
```
|
|
276
353
|
|
|
277
354
|
If the user answers in chat before the push response arrives, continue normally and call `cancel_question` with the `correlationId` to clean up.
|
|
278
355
|
|
|
356
|
+
**A note on how long ask_user blocks:** the wait time and whether it blocks at all are governed by the site's delivery mode, which the user configures (you do not set it). In the default smart mode and push-only mode, ask_user blocks for the policy timeout; in notify-only mode it returns immediately with `answered: false` after sending the push. Always check `answered` rather than assuming the call blocked, and pass `timeoutMs` only when you need a shorter wait than the site policy.
|
|
357
|
+
|
|
279
358
|
## Identifying Your Agent
|
|
280
359
|
|
|
281
360
|
Always pass `agentName` when you are one of multiple possible agents the user may be running. The user sees this in the notification title to know which agent is asking.
|
|
@@ -292,6 +371,6 @@ Always pass `agentName` when you are one of multiple possible agents the user ma
|
|
|
292
371
|
- **Titles under 60 characters.** They get truncated on phone lock screens.
|
|
293
372
|
- **Bodies under 200 characters.** Concise summaries, not full explanations.
|
|
294
373
|
- **Max 3 notifications per task** unless the user explicitly requests more.
|
|
295
|
-
- **Use context for detail.** Put file lists, error traces, and next steps in the context object
|
|
374
|
+
- **Use context for detail.** Put file lists, error traces, and next steps in the context object - not the notification body.
|
|
296
375
|
- **Write questions as if talking to a busy person.** The user is on their phone, possibly away from their computer. Be specific: "Delete the 3 unused migration files?" is better than "Should I clean up?"
|
|
297
376
|
- **Pick the right question type.** Use confirm for binary decisions, select when options are known, input when they are not.
|
|
@@ -54,6 +54,7 @@ var setHumanStream = (target) => {
|
|
|
54
54
|
var writeHuman = (text) => {
|
|
55
55
|
stream.write(text);
|
|
56
56
|
};
|
|
57
|
+
var humanIsTty = () => stream === process.stdout && process.stdout.isTTY === true;
|
|
57
58
|
|
|
58
59
|
// src/onboarding.ts
|
|
59
60
|
import qrcodeTerminal from "qrcode-terminal";
|
|
@@ -258,16 +259,59 @@ var fetchChannels = async (apiKey) => {
|
|
|
258
259
|
};
|
|
259
260
|
};
|
|
260
261
|
var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
262
|
+
var SPINNER_HEARTBEAT_MS = 3e4;
|
|
263
|
+
var SPINNER_FRAME_MS = 200;
|
|
264
|
+
var writeStatusLine = (text) => {
|
|
265
|
+
writeHuman(humanIsTty() ? `\r ${text}\x1B[K
|
|
266
|
+
` : ` ${text}
|
|
267
|
+
`);
|
|
268
|
+
};
|
|
269
|
+
var visibleLength = (text) => text.replace(/\x1b\[[0-9;]*m/g, "").length;
|
|
270
|
+
var clipToRow = (text, columns) => {
|
|
271
|
+
const budget = columns - 4;
|
|
272
|
+
if (budget <= 1 || visibleLength(text) <= budget) return text;
|
|
273
|
+
let visible = 0;
|
|
274
|
+
let out = "";
|
|
275
|
+
for (let i = 0; i < text.length; i += 1) {
|
|
276
|
+
const escape = /^\x1b\[[0-9;]*m/.exec(text.slice(i));
|
|
277
|
+
if (escape) {
|
|
278
|
+
out += escape[0];
|
|
279
|
+
i += escape[0].length - 1;
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
if (visible >= budget - 1) break;
|
|
283
|
+
out += text[i];
|
|
284
|
+
visible += 1;
|
|
285
|
+
}
|
|
286
|
+
return `${out}\u2026`;
|
|
287
|
+
};
|
|
288
|
+
var terminalColumns = () => {
|
|
289
|
+
const columns = process.stdout.columns;
|
|
290
|
+
return typeof columns === "number" && columns > 0 ? columns : 80;
|
|
291
|
+
};
|
|
261
292
|
var startSpinner = (getLabel) => {
|
|
293
|
+
if (!humanIsTty()) {
|
|
294
|
+
writeHuman(` ${getLabel()}
|
|
295
|
+
`);
|
|
296
|
+
const interval2 = setInterval(() => {
|
|
297
|
+
writeHuman(` ${getLabel()}
|
|
298
|
+
`);
|
|
299
|
+
}, SPINNER_HEARTBEAT_MS);
|
|
300
|
+
interval2.unref?.();
|
|
301
|
+
return (finalGlyph, finalLabel) => {
|
|
302
|
+
clearInterval(interval2);
|
|
303
|
+
writeStatusLine(`${finalGlyph} ${finalLabel}`);
|
|
304
|
+
};
|
|
305
|
+
}
|
|
262
306
|
const frames = [" ", ". ", ".. ", "..."];
|
|
263
307
|
let i = 0;
|
|
264
308
|
const interval = setInterval(() => {
|
|
265
|
-
|
|
266
|
-
|
|
309
|
+
const line = `${dim(frames[i++ % frames.length])} ${getLabel()}`;
|
|
310
|
+
writeHuman(`\r ${clipToRow(line, terminalColumns())}\x1B[K`);
|
|
311
|
+
}, SPINNER_FRAME_MS);
|
|
267
312
|
return (finalGlyph, finalLabel) => {
|
|
268
313
|
clearInterval(interval);
|
|
269
|
-
|
|
270
|
-
`);
|
|
314
|
+
writeStatusLine(`${finalGlyph} ${finalLabel}`);
|
|
271
315
|
};
|
|
272
316
|
};
|
|
273
317
|
var printQr = (url) => new Promise((resolve, reject) => {
|
|
@@ -304,8 +348,7 @@ var waitForDevice = async (apiKey, baseline) => {
|
|
|
304
348
|
consecutiveFailures += 1;
|
|
305
349
|
if (consecutiveFailures >= 3 && !warned) {
|
|
306
350
|
warned = true;
|
|
307
|
-
|
|
308
|
-
`);
|
|
351
|
+
writeStatusLine(`${yellow("!")} Still trying. pushary.com is not answering ${dim(`(${probe.detail})`)}`);
|
|
309
352
|
}
|
|
310
353
|
await sleep(CONNECT_POLL_INTERVAL_MS);
|
|
311
354
|
continue;
|
|
@@ -319,6 +362,8 @@ var waitForDevice = async (apiKey, baseline) => {
|
|
|
319
362
|
await sleep(CONNECT_POLL_INTERVAL_MS);
|
|
320
363
|
}
|
|
321
364
|
stop(yellow("!"), "Didn't detect a connection yet");
|
|
365
|
+
console.log(` ${dim("Signed in on the app already? Open it and allow notifications.")}`);
|
|
366
|
+
console.log(` ${dim("Signing in alone does not connect the phone. Then run")} ${cyan("pushary doctor")}${dim(".")}`);
|
|
322
367
|
return { kind: "timeout" };
|
|
323
368
|
} catch (err) {
|
|
324
369
|
stop(yellow("!"), `Couldn't check connection ${dim(`(${err instanceof Error ? err.message : "network error"})`)}`);
|
|
@@ -465,6 +510,18 @@ var startPairing = async (cliPublicKey) => {
|
|
|
465
510
|
return null;
|
|
466
511
|
}
|
|
467
512
|
};
|
|
513
|
+
var describePairFailure = (reason) => {
|
|
514
|
+
switch (reason) {
|
|
515
|
+
case "no_site":
|
|
516
|
+
return "Couldn't set up your workspace. Try again, or finish setup at pushary.com/dashboard";
|
|
517
|
+
case "blocked":
|
|
518
|
+
return "Finish setting up in the Pushary app, then run setup again";
|
|
519
|
+
case "authorize_failed":
|
|
520
|
+
return "The app could not authorize this terminal. Re-run setup to try again";
|
|
521
|
+
default:
|
|
522
|
+
return "Pairing failed. Re-run setup to try again";
|
|
523
|
+
}
|
|
524
|
+
};
|
|
468
525
|
var cancelPairing = async (pairId) => {
|
|
469
526
|
try {
|
|
470
527
|
await fetch(`${apiBase()}/api/mobile/pair/start`, {
|
|
@@ -514,12 +571,19 @@ var connectViaAppPairing = async (options = {}) => {
|
|
|
514
571
|
console.log();
|
|
515
572
|
const startedAt = Date.now();
|
|
516
573
|
const deadline = startedAt + PAIR_TIMEOUT_MS;
|
|
574
|
+
let hintShown = false;
|
|
517
575
|
const stop = startSpinner(() => {
|
|
518
576
|
const left = Math.max(0, Math.ceil((deadline - Date.now()) / 6e4));
|
|
519
577
|
if (Date.now() - startedAt < PAIR_HINT_AFTER_MS) {
|
|
520
578
|
return "Waiting for the app to authorize";
|
|
521
579
|
}
|
|
522
|
-
|
|
580
|
+
if (!hintShown) {
|
|
581
|
+
hintShown = true;
|
|
582
|
+
writeStatusLine(
|
|
583
|
+
`${dim("No app yet? Install it, sign in with this account, then scan. Ctrl-C to stop.")}`
|
|
584
|
+
);
|
|
585
|
+
}
|
|
586
|
+
return `Waiting for the app ${dim(`(${left}m left)`)}`;
|
|
523
587
|
});
|
|
524
588
|
let cancelled = false;
|
|
525
589
|
const onInterrupt = () => {
|
|
@@ -549,6 +613,10 @@ var connectViaAppPairing = async (options = {}) => {
|
|
|
549
613
|
stop(yellow("!"), "Pairing expired \u2014 re-run setup to try again");
|
|
550
614
|
return null;
|
|
551
615
|
}
|
|
616
|
+
if (claim?.status === "failed") {
|
|
617
|
+
stop(yellow("!"), describePairFailure(claim.reason));
|
|
618
|
+
return null;
|
|
619
|
+
}
|
|
552
620
|
await sleep(PAIR_POLL_INTERVAL_MS);
|
|
553
621
|
}
|
|
554
622
|
stop(yellow("!"), "Timed out waiting for the app");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pushary/agent-hooks",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.68.0",
|
|
4
4
|
"description": "Permission hooks for AI coding agents: route tool approvals through Pushary push notifications",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pushary",
|
|
@@ -25,13 +25,8 @@
|
|
|
25
25
|
],
|
|
26
26
|
"author": "Pushary <business@pushary.com>",
|
|
27
27
|
"homepage": "https://pushary.com",
|
|
28
|
-
"repository": {
|
|
29
|
-
"type": "git",
|
|
30
|
-
"url": "git+https://github.com/pushary/pushary.git",
|
|
31
|
-
"directory": "packages/agent-hooks"
|
|
32
|
-
},
|
|
33
28
|
"bugs": {
|
|
34
|
-
"url": "https://
|
|
29
|
+
"url": "https://pushary.com/support"
|
|
35
30
|
},
|
|
36
31
|
"license": "MIT",
|
|
37
32
|
"type": "module",
|