@pushary/agent-hooks 0.66.0 → 0.67.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 +34 -0
- package/data/SKILL.md +105 -30
- package/data/cursor-plugin/skills/pushary/SKILL.md +103 -24
- package/data/vscode-plugin/.claude-plugin/plugin.json +28 -0
- package/data/vscode-plugin/.mcp.json +11 -0
- package/data/vscode-plugin/CHANGELOG.md +11 -0
- package/data/vscode-plugin/LICENSE +21 -0
- package/data/vscode-plugin/README.md +136 -0
- package/data/vscode-plugin/SECURITY.md +56 -0
- package/data/vscode-plugin/assets/logo.png +0 -0
- package/data/vscode-plugin/commands/notify-when-done.md +14 -0
- package/data/vscode-plugin/commands/pushary-test.md +13 -0
- package/data/vscode-plugin/hooks/hooks.json +11 -0
- package/data/vscode-plugin/scripts/pushary-gate.mjs +508 -0
- package/data/vscode-plugin/skills/pushary/SKILL.md +376 -0
- package/dist/bin/pushary-clean.js +29 -4
- package/dist/bin/pushary-connect.js +1 -1
- package/dist/bin/pushary-doctor.js +43 -8
- package/dist/bin/pushary-logout.js +1 -1
- package/dist/bin/pushary-setup.js +130 -8
- package/dist/bin/pushary-status.js +1 -1
- package/dist/bin/pushary-upgrade.js +17 -5
- package/dist/{chunk-RU3CIBXY.js → chunk-7HG4WUIE.js} +4 -0
- package/dist/chunk-KB4ODLGB.js +332 -0
- package/dist/{chunk-QTC7SR6A.js → chunk-V6OA4VPU.js} +24 -5
- package/package.json +2 -7
- package/dist/chunk-6G2BC4ET.js +0 -93
- package/dist/chunk-N7XJ4L2W.js +0 -79
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: pushary
|
|
3
|
+
description: Push notifications and human-in-the-loop for AI agents. Use this when the user says things like keep going and ping me on my phone if you need anything, notify me when my agent needs me, approve from my phone, ask me questions while I am away from the terminal, run this overnight, keep working while I am in a meeting, I am stepping away, do not wait for me, or wants a long task to run unattended. Send alerts when tasks finish or fail, ask questions (yes/no, multiple choice, or free text) via push, and get answers from the user's lock screen. Use these tools proactively - do not wait for the user to ask for notifications. Works with Claude Code, Codex, Cursor, VS Code, Windsurf, Hermes, Lovable, or any MCP client; no Claude Max subscription required. Pushary is a hosted service, $9.99/mo after a 7-day card-first trial.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Pushary - Push Notifications for AI Agents
|
|
7
|
+
|
|
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
|
+
|
|
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 - yes/no, multiple choice, or free text - and get the answer from the user's lock screen.
|
|
12
|
+
3. **Send rich context notifications** with file changes, error details, next steps, and embedded questions.
|
|
13
|
+
|
|
14
|
+
Use these tools proactively. Do not wait for the user to ask for notifications.
|
|
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
|
+
|
|
24
|
+
## When to Use
|
|
25
|
+
|
|
26
|
+
**Send a notification when:**
|
|
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`
|
|
29
|
+
- A long-running process completes (migration, refactor, generation)
|
|
30
|
+
- A status update is worth sharing - use `context.type = "info"`
|
|
31
|
+
|
|
32
|
+
**Ask with type "confirm" when:**
|
|
33
|
+
- You need confirmation before a destructive or irreversible action
|
|
34
|
+
- Binary decision: proceed or abort
|
|
35
|
+
|
|
36
|
+
**Ask with type "select" when:**
|
|
37
|
+
- Multiple implementation approaches exist (2-6 options)
|
|
38
|
+
- The user needs to pick from a known set
|
|
39
|
+
|
|
40
|
+
**Ask with type "input" when:**
|
|
41
|
+
- You need a name, path, value, or free-text decision
|
|
42
|
+
- The options cannot be enumerated in advance
|
|
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
|
+
|
|
49
|
+
**Do NOT notify when:**
|
|
50
|
+
- The task is trivial or single-step
|
|
51
|
+
- The question can be answered from context without user input
|
|
52
|
+
- You already sent 3 notifications for the current task (unless the user explicitly asked for more)
|
|
53
|
+
|
|
54
|
+
## Setup
|
|
55
|
+
|
|
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):
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
npx @pushary/agent-hooks@latest setup
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Or add Pushary manually to your MCP configuration:
|
|
69
|
+
|
|
70
|
+
```json
|
|
71
|
+
{
|
|
72
|
+
"mcpServers": {
|
|
73
|
+
"pushary": {
|
|
74
|
+
"type": "http",
|
|
75
|
+
"url": "https://pushary.com/api/mcp/mcp",
|
|
76
|
+
"headers": {
|
|
77
|
+
"Authorization": "Bearer YOUR_API_KEY"
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Sign up at https://pushary.com/sign-up?utm_source=skill&utm_medium=setup to get your API key.
|
|
85
|
+
|
|
86
|
+
After setup, verify with:
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
npx @pushary/agent-hooks@latest doctor
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Tools
|
|
93
|
+
|
|
94
|
+
### send_notification
|
|
95
|
+
|
|
96
|
+
Send a one-way push notification to the user. Optionally include structured context for a rich detail page.
|
|
97
|
+
|
|
98
|
+
**Parameters:**
|
|
99
|
+
|
|
100
|
+
| Name | Type | Required | Description |
|
|
101
|
+
|------|------|----------|-------------|
|
|
102
|
+
| title | string | Yes | Notification title (max 100 chars, aim for under 60) |
|
|
103
|
+
| body | string | Yes | Notification body (max 500 chars, aim for under 200) |
|
|
104
|
+
| url | string | No | URL opened when tapped. Ignored if context is provided. |
|
|
105
|
+
| agentName | string | No | Identifies which agent sent this (e.g., "Claude Code - myproject") |
|
|
106
|
+
| iconUrl | string | No | Custom notification icon URL |
|
|
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) |
|
|
110
|
+
| subscriberIds | string[] | No | Target specific subscriber IDs |
|
|
111
|
+
| externalIds | string[] | No | Target by external IDs |
|
|
112
|
+
| tags | string[] | No | Target by subscriber tags |
|
|
113
|
+
| context | object | No | Structured context for a rich detail page (see below) |
|
|
114
|
+
|
|
115
|
+
**Context object:**
|
|
116
|
+
|
|
117
|
+
| Name | Type | Description |
|
|
118
|
+
|------|------|-------------|
|
|
119
|
+
| type | "task_complete" / "error" / "info" | The kind of notification |
|
|
120
|
+
| summary | string | Short summary of what happened |
|
|
121
|
+
| details | string[] | Bullet-point details |
|
|
122
|
+
| filesChanged | string[] | List of files that were changed |
|
|
123
|
+
| errorMessage | string | Error message (for error type) |
|
|
124
|
+
| errorFile | string | File path where the error occurred |
|
|
125
|
+
| nextSteps | string | Suggested next steps for the user |
|
|
126
|
+
| askQuestion | object | Embed a decision prompt in the notification (see below) |
|
|
127
|
+
|
|
128
|
+
**Embedded askQuestion:**
|
|
129
|
+
|
|
130
|
+
| Name | Type | Description |
|
|
131
|
+
|------|------|-------------|
|
|
132
|
+
| question | string | A follow-up question shown below the context |
|
|
133
|
+
| type | "confirm" / "select" / "input" | Question type (default: confirm) |
|
|
134
|
+
| options | string[] | Options for select type (2-6 items) |
|
|
135
|
+
|
|
136
|
+
When `askQuestion` is provided, the response includes a `linkedCorrelationId` you pass to `wait_for_answer`.
|
|
137
|
+
|
|
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:**
|
|
144
|
+
|
|
145
|
+
```json
|
|
146
|
+
{
|
|
147
|
+
"title": "Refactoring complete",
|
|
148
|
+
"body": "Extracted 3 shared components across 12 files",
|
|
149
|
+
"agentName": "Claude Code - pushary repo",
|
|
150
|
+
"context": {
|
|
151
|
+
"type": "task_complete",
|
|
152
|
+
"summary": "Extracted shared Button, Modal, and Card components from 12 files",
|
|
153
|
+
"filesChanged": ["src/components/Button.tsx", "src/components/Modal.tsx", "src/components/Card.tsx"],
|
|
154
|
+
"nextSteps": "Run the test suite to verify no regressions"
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
**Example - error with embedded question:**
|
|
160
|
+
|
|
161
|
+
```json
|
|
162
|
+
{
|
|
163
|
+
"title": "Build failed",
|
|
164
|
+
"body": "TypeScript error in auth.ts:42",
|
|
165
|
+
"agentName": "Claude Code - api-server",
|
|
166
|
+
"context": {
|
|
167
|
+
"type": "error",
|
|
168
|
+
"errorMessage": "Type 'string' is not assignable to type 'AuthToken'",
|
|
169
|
+
"errorFile": "src/auth.ts:42",
|
|
170
|
+
"summary": "The auth token type changed upstream and this file needs updating",
|
|
171
|
+
"askQuestion": {
|
|
172
|
+
"question": "Should I update the type or revert the upstream change?",
|
|
173
|
+
"type": "select",
|
|
174
|
+
"options": ["Update the type in auth.ts", "Revert the upstream change", "Skip for now"]
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
### ask_user
|
|
181
|
+
|
|
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.
|
|
183
|
+
|
|
184
|
+
**Parameters:**
|
|
185
|
+
|
|
186
|
+
| Name | Type | Required | Description |
|
|
187
|
+
|------|------|----------|-------------|
|
|
188
|
+
| question | string | Yes | The question to ask (max 500 chars) |
|
|
189
|
+
| type | "confirm" / "select" / "input" | No | Question type (default: confirm) |
|
|
190
|
+
| options | string[] | No | Choices for select type (2-6 options). Required when type is select. |
|
|
191
|
+
| placeholder | string | No | Placeholder text for input type (max 200 chars) |
|
|
192
|
+
| context | string | No | What the agent is working on, shown above the question (max 500 chars) |
|
|
193
|
+
| wait | boolean | No | Wait for the answer before returning (default: true). Set false for manual polling. |
|
|
194
|
+
| timeoutMs | integer | No | Max wait time in ms (max 55000). Uses site policy if omitted. |
|
|
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) |
|
|
200
|
+
| callbackUrl | string | No | Webhook URL to POST the answer to when the user responds |
|
|
201
|
+
| subscriberIds | string[] | No | Target specific subscriber IDs |
|
|
202
|
+
| externalIds | string[] | No | Target by external IDs |
|
|
203
|
+
| tags | string[] | No | Target by subscriber tags |
|
|
204
|
+
|
|
205
|
+
**Returns (when wait=true, default):**
|
|
206
|
+
- `{ "answered": true, "value": "yes", "correlationId": "uuid" }` - user responded
|
|
207
|
+
- `{ "answered": false, "timedOut": true, "correlationId": "uuid" }` - timeout reached
|
|
208
|
+
|
|
209
|
+
**Returns (when wait=false):**
|
|
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.
|
|
214
|
+
|
|
215
|
+
**Example - confirm (yes/no):**
|
|
216
|
+
|
|
217
|
+
```json
|
|
218
|
+
{
|
|
219
|
+
"question": "Delete the 3 unused migration files?",
|
|
220
|
+
"type": "confirm",
|
|
221
|
+
"context": "Cleaning up old database migrations in db/migrate/",
|
|
222
|
+
"agentName": "Claude Code - myproject"
|
|
223
|
+
}
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
**Example - select (multiple choice):**
|
|
227
|
+
|
|
228
|
+
```json
|
|
229
|
+
{
|
|
230
|
+
"question": "Which auth strategy should I use?",
|
|
231
|
+
"type": "select",
|
|
232
|
+
"options": ["JWT tokens", "Session cookies", "OAuth2 + PKCE"],
|
|
233
|
+
"context": "Setting up authentication for the new API endpoints",
|
|
234
|
+
"agentName": "Claude Code - api-server"
|
|
235
|
+
}
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
**Example - input (free text):**
|
|
239
|
+
|
|
240
|
+
```json
|
|
241
|
+
{
|
|
242
|
+
"question": "What should the new API endpoint path be?",
|
|
243
|
+
"type": "input",
|
|
244
|
+
"placeholder": "/api/v2/...",
|
|
245
|
+
"context": "Creating a new REST endpoint for user preferences",
|
|
246
|
+
"agentName": "Cursor - frontend"
|
|
247
|
+
}
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
### wait_for_answer
|
|
251
|
+
|
|
252
|
+
Poll for the user's response to a question sent via `ask_user` with `wait: false`. Not needed when using the default blocking mode.
|
|
253
|
+
|
|
254
|
+
**Parameters:**
|
|
255
|
+
|
|
256
|
+
| Name | Type | Required | Description |
|
|
257
|
+
|------|------|----------|-------------|
|
|
258
|
+
| correlationId | string (uuid) | Yes | The correlationId from ask_user |
|
|
259
|
+
| timeoutMs | integer | No | How long to wait (default 30000, max 55000) |
|
|
260
|
+
|
|
261
|
+
**Returns:**
|
|
262
|
+
- `{ "answered": true, "value": "yes" }` - user responded
|
|
263
|
+
- `{ "answered": false }` - timeout reached, no answer yet
|
|
264
|
+
|
|
265
|
+
### cancel_question
|
|
266
|
+
|
|
267
|
+
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).
|
|
268
|
+
|
|
269
|
+
**Parameters:**
|
|
270
|
+
|
|
271
|
+
| Name | Type | Required | Description |
|
|
272
|
+
|------|------|----------|-------------|
|
|
273
|
+
| correlationId | string (uuid) | Yes | The correlationId of the question to cancel |
|
|
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
|
+
|
|
319
|
+
## Permission Gating (REQUIRED)
|
|
320
|
+
|
|
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:
|
|
322
|
+
|
|
323
|
+
- File deletion (`rm`, `unlink`, any destructive file operation)
|
|
324
|
+
- Database mutations (`DROP`, `DELETE`, `TRUNCATE`, migrations)
|
|
325
|
+
- Deployment commands (`deploy`, `push`, `publish`, `release`)
|
|
326
|
+
- System administration (`systemctl`, `service`, package install/remove)
|
|
327
|
+
- Git operations that rewrite history (`reset --hard`, `push --force`, `rebase`)
|
|
328
|
+
- Network configuration changes (firewall, DNS, proxy)
|
|
329
|
+
- Any command the user has flagged as dangerous
|
|
330
|
+
|
|
331
|
+
If `ask_user` returns `answered: false`, do NOT execute the command. Send a notification that the operation was skipped due to no response.
|
|
332
|
+
|
|
333
|
+
This is not optional. Treat it as a hard constraint, not a suggestion.
|
|
334
|
+
|
|
335
|
+
## Human-in-the-Loop Flow
|
|
336
|
+
|
|
337
|
+
One tool call - `ask_user` blocks and returns the answer:
|
|
338
|
+
|
|
339
|
+
```
|
|
340
|
+
result = ask_user({
|
|
341
|
+
question: "Which auth strategy should I use?",
|
|
342
|
+
type: "select",
|
|
343
|
+
options: ["JWT tokens", "Session cookies", "OAuth2 + PKCE"],
|
|
344
|
+
context: "Setting up authentication for the new API",
|
|
345
|
+
agentName: "Claude Code - myproject"
|
|
346
|
+
})
|
|
347
|
+
|
|
348
|
+
if result.answered:
|
|
349
|
+
// result.value = "JWT tokens" - proceed with the chosen approach
|
|
350
|
+
else:
|
|
351
|
+
// user did not respond - pick the safe default or notify and skip
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
If the user answers in chat before the push response arrives, continue normally and call `cancel_question` with the `correlationId` to clean up.
|
|
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
|
+
|
|
358
|
+
## Identifying Your Agent
|
|
359
|
+
|
|
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.
|
|
361
|
+
|
|
362
|
+
**Format:** `{Agent Type} - {project or context}`
|
|
363
|
+
|
|
364
|
+
**Examples:**
|
|
365
|
+
- `"Claude Code - pushary repo"`
|
|
366
|
+
- `"Hermes - daily-briefing"`
|
|
367
|
+
- `"Cursor - frontend refactor"`
|
|
368
|
+
|
|
369
|
+
## Notification Etiquette
|
|
370
|
+
|
|
371
|
+
- **Titles under 60 characters.** They get truncated on phone lock screens.
|
|
372
|
+
- **Bodies under 200 characters.** Concise summaries, not full explanations.
|
|
373
|
+
- **Max 3 notifications per task** unless the user explicitly requests more.
|
|
374
|
+
- **Use context for detail.** Put file lists, error traces, and next steps in the context object - not the notification body.
|
|
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?"
|
|
376
|
+
- **Pick the right question type.** Use confirm for binary decisions, select when options are known, input when they are not.
|
|
@@ -4,8 +4,11 @@ import {
|
|
|
4
4
|
removePusharySettings
|
|
5
5
|
} from "../chunk-CAJZAFVS.js";
|
|
6
6
|
import {
|
|
7
|
-
removeInstructionBlock
|
|
8
|
-
|
|
7
|
+
removeInstructionBlock,
|
|
8
|
+
shortenHome,
|
|
9
|
+
unregisterPluginLocation,
|
|
10
|
+
vscodeSettingsTargets
|
|
11
|
+
} from "../chunk-KB4ODLGB.js";
|
|
9
12
|
import {
|
|
10
13
|
removeGeminiSettings
|
|
11
14
|
} from "../chunk-E2U35RLD.js";
|
|
@@ -24,8 +27,9 @@ import {
|
|
|
24
27
|
geminiMd,
|
|
25
28
|
geminiSettings,
|
|
26
29
|
pusharyDir,
|
|
27
|
-
removeCodexHooks
|
|
28
|
-
|
|
30
|
+
removeCodexHooks,
|
|
31
|
+
vscodePluginDir
|
|
32
|
+
} from "../chunk-7HG4WUIE.js";
|
|
29
33
|
import {
|
|
30
34
|
removeClaudeAlias
|
|
31
35
|
} from "../chunk-BC3VCZ3E.js";
|
|
@@ -86,6 +90,7 @@ var SKILL_DIR = claudeSkillDir();
|
|
|
86
90
|
var CURSOR_MCP = cursorUserMcp();
|
|
87
91
|
var CURSOR_PLUGIN_DIR = cursorPluginDir();
|
|
88
92
|
var CURSOR_USER_HOOKS = cursorUserHooks();
|
|
93
|
+
var VSCODE_PLUGIN_DIR = vscodePluginDir();
|
|
89
94
|
var PUSHARY_DIR = pusharyDir();
|
|
90
95
|
var SHELL_FILES = [".zshrc", ".zprofile", ".bashrc", ".bash_profile"].map((f) => join(homedir(), f));
|
|
91
96
|
var resolveHermesPython = () => {
|
|
@@ -230,6 +235,26 @@ var main = async () => {
|
|
|
230
235
|
} else {
|
|
231
236
|
console.log(` ${skip} Cursor gate ${dim("(no hooks.json)")}`);
|
|
232
237
|
}
|
|
238
|
+
for (const settingsPath of vscodeSettingsTargets(existsSync)) {
|
|
239
|
+
const label = `VS Code plugin ${dim(`(${shortenHome(settingsPath)})`)}`;
|
|
240
|
+
if (!existsSync(settingsPath)) {
|
|
241
|
+
console.log(` ${skip} ${label} ${dim("(no settings.json)")}`);
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
const result = unregisterPluginLocation(readFileSync(settingsPath, "utf-8"), VSCODE_PLUGIN_DIR);
|
|
245
|
+
if (result.kind === "removed") {
|
|
246
|
+
guardedWrite(settingsPath, result.content);
|
|
247
|
+
console.log(` ${check} ${label} ${dim(did("unregistered"))}`);
|
|
248
|
+
} else if (result.kind === "manual") {
|
|
249
|
+
console.log(` ${skip} ${label} ${yellow("(remove the chat.pluginLocations entry by hand)")}`);
|
|
250
|
+
} else {
|
|
251
|
+
console.log(` ${skip} ${label} ${dim("(no pushary entry)")}`);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
if (existsSync(VSCODE_PLUGIN_DIR)) {
|
|
255
|
+
guardedRemove(VSCODE_PLUGIN_DIR);
|
|
256
|
+
console.log(` ${check} VS Code plugin files ${dim(did("removed"))}`);
|
|
257
|
+
}
|
|
233
258
|
if (existsSync(SKILL_DIR)) {
|
|
234
259
|
guardedRemove(SKILL_DIR);
|
|
235
260
|
console.log(` ${check} Skill directory ${dim(did("removed"))}`);
|
|
@@ -3,11 +3,11 @@ import {
|
|
|
3
3
|
describeWaitLadder
|
|
4
4
|
} from "../chunk-IIENZE2J.js";
|
|
5
5
|
import {
|
|
6
|
-
detectAllAgents
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
} from "../chunk-
|
|
6
|
+
detectAllAgents,
|
|
7
|
+
hasInstructionBlock,
|
|
8
|
+
isPluginRegistered,
|
|
9
|
+
vscodeSettingsTargets
|
|
10
|
+
} from "../chunk-KB4ODLGB.js";
|
|
11
11
|
import {
|
|
12
12
|
GEMINI_HOOK_BINARY,
|
|
13
13
|
hasGeminiHooks,
|
|
@@ -28,13 +28,14 @@ import {
|
|
|
28
28
|
hasCodexHooks,
|
|
29
29
|
missingCodexHookEvents,
|
|
30
30
|
readCodexMcpAuth,
|
|
31
|
-
untrustedCodexHookEvents
|
|
32
|
-
|
|
31
|
+
untrustedCodexHookEvents,
|
|
32
|
+
vscodePluginDir
|
|
33
|
+
} from "../chunk-7HG4WUIE.js";
|
|
33
34
|
import {
|
|
34
35
|
describeReach,
|
|
35
36
|
fetchChannels,
|
|
36
37
|
reachVerdict
|
|
37
|
-
} from "../chunk-
|
|
38
|
+
} from "../chunk-V6OA4VPU.js";
|
|
38
39
|
import "../chunk-3EGEA4KH.js";
|
|
39
40
|
import {
|
|
40
41
|
keyPrefix,
|
|
@@ -468,6 +469,40 @@ var main = async () => {
|
|
|
468
469
|
check(true, "Cursor: API key linked", "embedded in plugin mcp.json");
|
|
469
470
|
}
|
|
470
471
|
}
|
|
472
|
+
const vscodePluginDir2 = vscodePluginDir();
|
|
473
|
+
if (existsSync(vscodePluginDir2)) {
|
|
474
|
+
const registeredIn = vscodeSettingsTargets(existsSync).filter(
|
|
475
|
+
(settingsPath) => existsSync(settingsPath) && isPluginRegistered(readFileSync(settingsPath, "utf-8"), vscodePluginDir2)
|
|
476
|
+
);
|
|
477
|
+
check(
|
|
478
|
+
registeredIn.length > 0,
|
|
479
|
+
"VS Code: plugin registered",
|
|
480
|
+
registeredIn.length > 0 ? registeredIn.join(", ") : "not listed in chat.pluginLocations \u2014 re-run setup (an unregistered plugin directory is never loaded)"
|
|
481
|
+
);
|
|
482
|
+
const vscodeHooks = readJson(join(vscodePluginDir2, "hooks", "hooks.json"));
|
|
483
|
+
const gateEntry = vscodeHooks?.hooks?.PreToolUse?.find((h) => String(h.command ?? "").includes("pushary-gate"));
|
|
484
|
+
if (!gateEntry) {
|
|
485
|
+
check(false, "VS Code: permission gate present", "no Pushary gate in the plugin hooks.json \u2014 re-run setup");
|
|
486
|
+
} else {
|
|
487
|
+
const scriptPath = String(gateEntry.command).match(/"([^"]+)"/)?.[1] ?? "";
|
|
488
|
+
const resolves = scriptPath ? existsSync(scriptPath) : false;
|
|
489
|
+
check(
|
|
490
|
+
resolves,
|
|
491
|
+
"VS Code: permission gate present",
|
|
492
|
+
resolves ? scriptPath : `gate script not found: ${scriptPath || gateEntry.command} \u2014 re-run setup`
|
|
493
|
+
);
|
|
494
|
+
}
|
|
495
|
+
const vscodeMcp = readJson(join(vscodePluginDir2, ".mcp.json"));
|
|
496
|
+
const vscodeServers = vscodeMcp?.mcpServers ?? {};
|
|
497
|
+
const vscodeAuth = vscodeServers.pushary?.headers?.Authorization;
|
|
498
|
+
if (!vscodeAuth) {
|
|
499
|
+
check(false, "VS Code: API key linked", "no Authorization in plugin .mcp.json \u2014 re-run setup");
|
|
500
|
+
} else if (vscodeAuth.includes("PUSHARY_API_KEY")) {
|
|
501
|
+
check(false, "VS Code: API key linked", "plugin relies on $PUSHARY_API_KEY; re-run setup to embed the key so the gate works when VS Code is launched from the GUI");
|
|
502
|
+
} else {
|
|
503
|
+
check(true, "VS Code: API key linked", "embedded in plugin .mcp.json");
|
|
504
|
+
}
|
|
505
|
+
}
|
|
471
506
|
check(existsSync(SKILL_PATH), "Skill installed", existsSync(SKILL_PATH) ? SKILL_PATH : "not found");
|
|
472
507
|
let globalVersion2 = "";
|
|
473
508
|
try {
|