@pushary/agent-hooks 0.11.1 → 0.13.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/cursor-plugin/.cursor-plugin/plugin.json +25 -0
- package/data/cursor-plugin/CHANGELOG.md +10 -0
- package/data/cursor-plugin/LICENSE +21 -0
- package/data/cursor-plugin/README.md +111 -0
- package/data/cursor-plugin/SECURITY.md +33 -0
- package/data/cursor-plugin/assets/logo.png +0 -0
- package/data/cursor-plugin/commands/notify-when-done.md +14 -0
- package/data/cursor-plugin/commands/pushary-test.md +13 -0
- package/data/cursor-plugin/hooks/hooks.json +13 -0
- package/data/cursor-plugin/mcp.json +11 -0
- package/data/cursor-plugin/rules/pushary.mdc +40 -0
- package/data/cursor-plugin/scripts/pushary-gate.mjs +349 -0
- package/data/cursor-plugin/skills/pushary/SKILL.md +297 -0
- package/dist/bin/pushary-clean.js +8 -1
- package/dist/bin/pushary-codex.js +3 -2
- package/dist/bin/pushary-doctor.js +5 -3
- package/dist/bin/pushary-hook.js +3 -3
- package/dist/bin/pushary-post-hook.js +3 -2
- package/dist/bin/pushary-prompt-hook.d.ts +1 -0
- package/dist/bin/pushary-prompt-hook.js +25 -0
- package/dist/bin/pushary-setup.js +40 -31
- package/dist/bin/pushary-stop-hook.js +3 -2
- package/dist/chunk-22CV7V7A.js +38 -0
- package/dist/chunk-5MA3CPZB.js +141 -0
- package/dist/chunk-CH53PBQN.js +265 -0
- package/dist/chunk-RNWPCELY.js +176 -0
- package/dist/chunk-WCGKLHCL.js +154 -0
- package/dist/src/index.d.ts +10 -2
- package/dist/src/index.js +9 -9
- package/package.json +4 -3
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: pushary
|
|
3
|
+
description: Push notifications and human-in-the-loop for AI coding agents. Send alerts when tasks finish, 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 Cursor, Claude Code, Windsurf, Hermes, and any MCP client.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Pushary — Push Notifications for AI Agents
|
|
7
|
+
|
|
8
|
+
Pushary is an MCP server that gives you three capabilities:
|
|
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 wait for the user's answer.
|
|
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
|
+
## When to Use
|
|
17
|
+
|
|
18
|
+
**Send a notification when:**
|
|
19
|
+
- You finish a task that took 3 or more steps — use `context.type = "task_complete"`
|
|
20
|
+
- A build, test suite, or deployment fails — use `context.type = "error"` with `errorMessage`
|
|
21
|
+
- A long-running process completes (migration, refactor, generation)
|
|
22
|
+
- A status update is worth sharing — use `context.type = "info"`
|
|
23
|
+
|
|
24
|
+
**Ask with type "confirm" when:**
|
|
25
|
+
- You need confirmation before a destructive or irreversible action
|
|
26
|
+
- Binary decision: proceed or abort
|
|
27
|
+
|
|
28
|
+
**Ask with type "select" when:**
|
|
29
|
+
- Multiple implementation approaches exist (2-6 options)
|
|
30
|
+
- The user needs to pick from a known set
|
|
31
|
+
|
|
32
|
+
**Ask with type "input" when:**
|
|
33
|
+
- You need a name, path, value, or free-text decision
|
|
34
|
+
- The options cannot be enumerated in advance
|
|
35
|
+
|
|
36
|
+
**Do NOT notify when:**
|
|
37
|
+
- The task is trivial or single-step
|
|
38
|
+
- The question can be answered from context without user input
|
|
39
|
+
- You already sent 3 notifications for the current task (unless the user explicitly asked for more)
|
|
40
|
+
|
|
41
|
+
## Setup
|
|
42
|
+
|
|
43
|
+
Run the CLI setup (recommended — configures MCP, hooks, permissions, and skill in one step):
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
npx @pushary/agent-hooks@latest setup
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Or add Pushary manually to your MCP configuration:
|
|
50
|
+
|
|
51
|
+
```json
|
|
52
|
+
{
|
|
53
|
+
"mcpServers": {
|
|
54
|
+
"pushary": {
|
|
55
|
+
"type": "http",
|
|
56
|
+
"url": "https://pushary.com/api/mcp/mcp",
|
|
57
|
+
"headers": {
|
|
58
|
+
"Authorization": "Bearer YOUR_API_KEY"
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Sign up at https://pushary.com/sign-up?from=ai-coding to get your API key.
|
|
66
|
+
|
|
67
|
+
After setup, verify with:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
npx @pushary/agent-hooks@latest doctor
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Tools
|
|
74
|
+
|
|
75
|
+
### send_notification
|
|
76
|
+
|
|
77
|
+
Send a one-way push notification to the user. Optionally include structured context for a rich detail page.
|
|
78
|
+
|
|
79
|
+
**Parameters:**
|
|
80
|
+
|
|
81
|
+
| Name | Type | Required | Description |
|
|
82
|
+
|------|------|----------|-------------|
|
|
83
|
+
| title | string | Yes | Notification title (max 100 chars, aim for under 60) |
|
|
84
|
+
| body | string | Yes | Notification body (max 500 chars, aim for under 200) |
|
|
85
|
+
| url | string | No | URL opened when tapped. Ignored if context is provided. |
|
|
86
|
+
| agentName | string | No | Identifies which agent sent this (e.g., "Claude Code - myproject") |
|
|
87
|
+
| iconUrl | string | No | Custom notification icon URL |
|
|
88
|
+
| imageUrl | string | No | Large image shown in the notification |
|
|
89
|
+
| subscriberIds | string[] | No | Target specific subscriber IDs |
|
|
90
|
+
| externalIds | string[] | No | Target by external IDs |
|
|
91
|
+
| tags | string[] | No | Target by subscriber tags |
|
|
92
|
+
| context | object | No | Structured context for a rich detail page (see below) |
|
|
93
|
+
|
|
94
|
+
**Context object:**
|
|
95
|
+
|
|
96
|
+
| Name | Type | Description |
|
|
97
|
+
|------|------|-------------|
|
|
98
|
+
| type | "task_complete" / "error" / "info" | The kind of notification |
|
|
99
|
+
| summary | string | Short summary of what happened |
|
|
100
|
+
| details | string[] | Bullet-point details |
|
|
101
|
+
| filesChanged | string[] | List of files that were changed |
|
|
102
|
+
| errorMessage | string | Error message (for error type) |
|
|
103
|
+
| errorFile | string | File path where the error occurred |
|
|
104
|
+
| nextSteps | string | Suggested next steps for the user |
|
|
105
|
+
| askQuestion | object | Embed a decision prompt in the notification (see below) |
|
|
106
|
+
|
|
107
|
+
**Embedded askQuestion:**
|
|
108
|
+
|
|
109
|
+
| Name | Type | Description |
|
|
110
|
+
|------|------|-------------|
|
|
111
|
+
| question | string | A follow-up question shown below the context |
|
|
112
|
+
| type | "confirm" / "select" / "input" | Question type (default: confirm) |
|
|
113
|
+
| options | string[] | Options for select type (2-6 items) |
|
|
114
|
+
|
|
115
|
+
When `askQuestion` is provided, the response includes a `linkedCorrelationId` you pass to `wait_for_answer`.
|
|
116
|
+
|
|
117
|
+
**Example — task completed with context:**
|
|
118
|
+
|
|
119
|
+
```json
|
|
120
|
+
{
|
|
121
|
+
"title": "Refactoring complete",
|
|
122
|
+
"body": "Extracted 3 shared components across 12 files",
|
|
123
|
+
"agentName": "Claude Code - pushary repo",
|
|
124
|
+
"context": {
|
|
125
|
+
"type": "task_complete",
|
|
126
|
+
"summary": "Extracted shared Button, Modal, and Card components from 12 files",
|
|
127
|
+
"filesChanged": ["src/components/Button.tsx", "src/components/Modal.tsx", "src/components/Card.tsx"],
|
|
128
|
+
"nextSteps": "Run the test suite to verify no regressions"
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
**Example — error with embedded question:**
|
|
134
|
+
|
|
135
|
+
```json
|
|
136
|
+
{
|
|
137
|
+
"title": "Build failed",
|
|
138
|
+
"body": "TypeScript error in auth.ts:42",
|
|
139
|
+
"agentName": "Claude Code - api-server",
|
|
140
|
+
"context": {
|
|
141
|
+
"type": "error",
|
|
142
|
+
"errorMessage": "Type 'string' is not assignable to type 'AuthToken'",
|
|
143
|
+
"errorFile": "src/auth.ts:42",
|
|
144
|
+
"summary": "The auth token type changed upstream and this file needs updating",
|
|
145
|
+
"askQuestion": {
|
|
146
|
+
"question": "Should I update the type or revert the upstream change?",
|
|
147
|
+
"type": "select",
|
|
148
|
+
"options": ["Update the type in auth.ts", "Revert the upstream change", "Skip for now"]
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### ask_user
|
|
155
|
+
|
|
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 — no need to call `wait_for_answer` separately.
|
|
157
|
+
|
|
158
|
+
**Parameters:**
|
|
159
|
+
|
|
160
|
+
| Name | Type | Required | Description |
|
|
161
|
+
|------|------|----------|-------------|
|
|
162
|
+
| question | string | Yes | The question to ask (max 500 chars) |
|
|
163
|
+
| type | "confirm" / "select" / "input" | No | Question type (default: confirm) |
|
|
164
|
+
| options | string[] | No | Choices for select type (2-6 options). Required when type is select. |
|
|
165
|
+
| placeholder | string | No | Placeholder text for input type (max 200 chars) |
|
|
166
|
+
| context | string | No | What the agent is working on, shown above the question (max 500 chars) |
|
|
167
|
+
| wait | boolean | No | Wait for the answer before returning (default: true). Set false for manual polling. |
|
|
168
|
+
| timeoutMs | integer | No | Max wait time in ms (max 55000). Uses site policy if omitted. |
|
|
169
|
+
| agentName | string | No | Identifies which agent is asking. Format: "{Agent} - {project}" (e.g., "Claude Code - myproject") |
|
|
170
|
+
| callbackUrl | string | No | Webhook URL to POST the answer to when the user responds |
|
|
171
|
+
| subscriberIds | string[] | No | Target specific subscriber IDs |
|
|
172
|
+
| externalIds | string[] | No | Target by external IDs |
|
|
173
|
+
| tags | string[] | No | Target by subscriber tags |
|
|
174
|
+
|
|
175
|
+
**Returns (when wait=true, default):**
|
|
176
|
+
- `{ "answered": true, "value": "yes", "correlationId": "uuid" }` — user responded
|
|
177
|
+
- `{ "answered": false, "timedOut": true, "correlationId": "uuid" }` — timeout reached
|
|
178
|
+
|
|
179
|
+
**Returns (when wait=false):**
|
|
180
|
+
- `{ "correlationId": "uuid", "status": "pending", "expiresInSeconds": 600 }` — use `wait_for_answer` to poll
|
|
181
|
+
|
|
182
|
+
**Example — confirm (yes/no):**
|
|
183
|
+
|
|
184
|
+
```json
|
|
185
|
+
{
|
|
186
|
+
"question": "Delete the 3 unused migration files?",
|
|
187
|
+
"type": "confirm",
|
|
188
|
+
"context": "Cleaning up old database migrations in db/migrate/",
|
|
189
|
+
"agentName": "Claude Code - myproject"
|
|
190
|
+
}
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
**Example — select (multiple choice):**
|
|
194
|
+
|
|
195
|
+
```json
|
|
196
|
+
{
|
|
197
|
+
"question": "Which auth strategy should I use?",
|
|
198
|
+
"type": "select",
|
|
199
|
+
"options": ["JWT tokens", "Session cookies", "OAuth2 + PKCE"],
|
|
200
|
+
"context": "Setting up authentication for the new API endpoints",
|
|
201
|
+
"agentName": "Claude Code - api-server"
|
|
202
|
+
}
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
**Example — input (free text):**
|
|
206
|
+
|
|
207
|
+
```json
|
|
208
|
+
{
|
|
209
|
+
"question": "What should the new API endpoint path be?",
|
|
210
|
+
"type": "input",
|
|
211
|
+
"placeholder": "/api/v2/...",
|
|
212
|
+
"context": "Creating a new REST endpoint for user preferences",
|
|
213
|
+
"agentName": "Cursor - frontend"
|
|
214
|
+
}
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
### wait_for_answer
|
|
218
|
+
|
|
219
|
+
Poll for the user's response to a question sent via `ask_user` with `wait: false`. Not needed when using the default blocking mode.
|
|
220
|
+
|
|
221
|
+
**Parameters:**
|
|
222
|
+
|
|
223
|
+
| Name | Type | Required | Description |
|
|
224
|
+
|------|------|----------|-------------|
|
|
225
|
+
| correlationId | string (uuid) | Yes | The correlationId from ask_user |
|
|
226
|
+
| timeoutMs | integer | No | How long to wait (default 30000, max 55000) |
|
|
227
|
+
|
|
228
|
+
**Returns:**
|
|
229
|
+
- `{ "answered": true, "value": "yes" }` — user responded
|
|
230
|
+
- `{ "answered": false }` — timeout reached, no answer yet
|
|
231
|
+
|
|
232
|
+
### cancel_question
|
|
233
|
+
|
|
234
|
+
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).
|
|
235
|
+
|
|
236
|
+
**Parameters:**
|
|
237
|
+
|
|
238
|
+
| Name | Type | Required | Description |
|
|
239
|
+
|------|------|----------|-------------|
|
|
240
|
+
| correlationId | string (uuid) | Yes | The correlationId of the question to cancel |
|
|
241
|
+
|
|
242
|
+
## Permission Gating (REQUIRED)
|
|
243
|
+
|
|
244
|
+
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:
|
|
245
|
+
|
|
246
|
+
- File deletion (`rm`, `unlink`, any destructive file operation)
|
|
247
|
+
- Database mutations (`DROP`, `DELETE`, `TRUNCATE`, migrations)
|
|
248
|
+
- Deployment commands (`deploy`, `push`, `publish`, `release`)
|
|
249
|
+
- System administration (`systemctl`, `service`, package install/remove)
|
|
250
|
+
- Git operations that rewrite history (`reset --hard`, `push --force`, `rebase`)
|
|
251
|
+
- Network configuration changes (firewall, DNS, proxy)
|
|
252
|
+
- Any command the user has flagged as dangerous
|
|
253
|
+
|
|
254
|
+
If `ask_user` returns `answered: false`, do NOT execute the command. Send a notification that the operation was skipped due to no response.
|
|
255
|
+
|
|
256
|
+
This is not optional. Treat it as a hard constraint, not a suggestion.
|
|
257
|
+
|
|
258
|
+
## Human-in-the-Loop Flow
|
|
259
|
+
|
|
260
|
+
One tool call — `ask_user` blocks and returns the answer:
|
|
261
|
+
|
|
262
|
+
```
|
|
263
|
+
result = ask_user({
|
|
264
|
+
question: "Which auth strategy should I use?",
|
|
265
|
+
type: "select",
|
|
266
|
+
options: ["JWT tokens", "Session cookies", "OAuth2 + PKCE"],
|
|
267
|
+
context: "Setting up authentication for the new API",
|
|
268
|
+
agentName: "Claude Code - myproject"
|
|
269
|
+
})
|
|
270
|
+
|
|
271
|
+
if result.answered:
|
|
272
|
+
// result.value = "JWT tokens" — proceed with the chosen approach
|
|
273
|
+
else:
|
|
274
|
+
// user did not respond — pick the safe default or notify and skip
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
If the user answers in chat before the push response arrives, continue normally and call `cancel_question` with the `correlationId` to clean up.
|
|
278
|
+
|
|
279
|
+
## Identifying Your Agent
|
|
280
|
+
|
|
281
|
+
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.
|
|
282
|
+
|
|
283
|
+
**Format:** `{Agent Type} - {project or context}`
|
|
284
|
+
|
|
285
|
+
**Examples:**
|
|
286
|
+
- `"Claude Code - pushary repo"`
|
|
287
|
+
- `"Hermes - daily-briefing"`
|
|
288
|
+
- `"Cursor - frontend refactor"`
|
|
289
|
+
|
|
290
|
+
## Notification Etiquette
|
|
291
|
+
|
|
292
|
+
- **Titles under 60 characters.** They get truncated on phone lock screens.
|
|
293
|
+
- **Bodies under 200 characters.** Concise summaries, not full explanations.
|
|
294
|
+
- **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 — not the notification body.
|
|
296
|
+
- **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
|
+
- **Pick the right question type.** Use confirm for binary decisions, select when options are known, input when they are not.
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import {
|
|
3
3
|
removeClaudeMcpServers,
|
|
4
4
|
removePusharySettings
|
|
5
|
-
} from "../chunk-
|
|
5
|
+
} from "../chunk-5MA3CPZB.js";
|
|
6
6
|
import {
|
|
7
7
|
execNpm
|
|
8
8
|
} from "../chunk-RSHN2AQ7.js";
|
|
@@ -24,6 +24,7 @@ var CLAUDE_SETTINGS_LOCAL = join(homedir(), ".claude", "settings.local.json");
|
|
|
24
24
|
var CLAUDE_JSON = join(homedir(), ".claude.json");
|
|
25
25
|
var SKILL_DIR = join(homedir(), ".claude", "skills", "pushary");
|
|
26
26
|
var CURSOR_MCP = join(".cursor", "mcp.json");
|
|
27
|
+
var CURSOR_PLUGIN_DIR = join(homedir(), ".cursor", "plugins", "local", "pushary");
|
|
27
28
|
var SHELL_FILES = [".zshrc", ".zprofile", ".bashrc", ".bash_profile"].map((f) => join(homedir(), f));
|
|
28
29
|
var readJson = (path) => {
|
|
29
30
|
try {
|
|
@@ -86,6 +87,12 @@ var main = async () => {
|
|
|
86
87
|
} else {
|
|
87
88
|
console.log(` ${skip} Cursor MCP config ${dim("(not found)")}`);
|
|
88
89
|
}
|
|
90
|
+
if (existsSync(CURSOR_PLUGIN_DIR)) {
|
|
91
|
+
rmSync(CURSOR_PLUGIN_DIR, { recursive: true });
|
|
92
|
+
console.log(` ${check} Cursor plugin ${dim("(removed from ~/.cursor/plugins/local)")}`);
|
|
93
|
+
} else {
|
|
94
|
+
console.log(` ${skip} Cursor plugin ${dim("(not installed)")}`);
|
|
95
|
+
}
|
|
89
96
|
if (existsSync(SKILL_DIR)) {
|
|
90
97
|
rmSync(SKILL_DIR, { recursive: true });
|
|
91
98
|
console.log(` ${check} Skill directory ${dim("(removed)")}`);
|
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
reportEvent
|
|
4
|
-
} from "../chunk-
|
|
4
|
+
} from "../chunk-WCGKLHCL.js";
|
|
5
5
|
import {
|
|
6
6
|
askUser,
|
|
7
7
|
getMachineId,
|
|
8
8
|
waitForAnswer
|
|
9
|
-
} from "../chunk-
|
|
9
|
+
} from "../chunk-CH53PBQN.js";
|
|
10
10
|
import "../chunk-3MIR7ODJ.js";
|
|
11
11
|
import {
|
|
12
12
|
getApiKey
|
|
13
13
|
} from "../chunk-VUNL35KE.js";
|
|
14
|
+
import "../chunk-22CV7V7A.js";
|
|
14
15
|
|
|
15
16
|
// bin/pushary-codex.ts
|
|
16
17
|
import { basename } from "path";
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
execNpm
|
|
4
|
-
} from "../chunk-RSHN2AQ7.js";
|
|
5
2
|
import {
|
|
6
3
|
callMcpTool,
|
|
7
4
|
sendMcpRequest
|
|
8
5
|
} from "../chunk-3MIR7ODJ.js";
|
|
9
6
|
import "../chunk-VUNL35KE.js";
|
|
7
|
+
import {
|
|
8
|
+
execNpm
|
|
9
|
+
} from "../chunk-RSHN2AQ7.js";
|
|
10
10
|
|
|
11
11
|
// bin/pushary-doctor.ts
|
|
12
12
|
import { existsSync, readFileSync } from "fs";
|
|
@@ -103,9 +103,11 @@ var main = async () => {
|
|
|
103
103
|
const hasPreHook = JSON.stringify(hooks?.PreToolUse ?? []).includes("pushary-hook");
|
|
104
104
|
const hasPostHook = JSON.stringify(hooks?.PostToolUse ?? []).includes("pushary-post-hook");
|
|
105
105
|
const hasStopHook = JSON.stringify(hooks?.Stop ?? []).includes("pushary-stop-hook");
|
|
106
|
+
const hasPromptHook = JSON.stringify(hooks?.UserPromptSubmit ?? []).includes("pushary-prompt-hook");
|
|
106
107
|
check(hasPreHook, "Claude Code: PreToolUse hook");
|
|
107
108
|
check(hasPostHook, "Claude Code: PostToolUse hook");
|
|
108
109
|
check(hasStopHook, "Claude Code: Stop hook");
|
|
110
|
+
check(hasPromptHook, "Claude Code: UserPromptSubmit hook", hasPromptHook ? void 0 : "missing, re-run setup to register it");
|
|
109
111
|
const preHookCommand = extractHookCommand(hooks?.PreToolUse, "pushary-hook");
|
|
110
112
|
if (preHookCommand) {
|
|
111
113
|
const resolves = commandResolves(preHookCommand);
|
package/dist/bin/pushary-hook.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
handlePreToolUse
|
|
4
|
-
} from "../chunk-
|
|
5
|
-
import "../chunk-
|
|
6
|
-
import "../chunk-OF5WIOYS.js";
|
|
4
|
+
} from "../chunk-RNWPCELY.js";
|
|
5
|
+
import "../chunk-CH53PBQN.js";
|
|
7
6
|
import "../chunk-3MIR7ODJ.js";
|
|
8
7
|
import "../chunk-VUNL35KE.js";
|
|
8
|
+
import "../chunk-22CV7V7A.js";
|
|
9
9
|
|
|
10
10
|
// bin/pushary-hook.ts
|
|
11
11
|
var main = async () => {
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
handlePostToolUse
|
|
4
|
-
} from "../chunk-
|
|
5
|
-
import "../chunk-
|
|
4
|
+
} from "../chunk-WCGKLHCL.js";
|
|
5
|
+
import "../chunk-CH53PBQN.js";
|
|
6
6
|
import "../chunk-3MIR7ODJ.js";
|
|
7
7
|
import "../chunk-VUNL35KE.js";
|
|
8
|
+
import "../chunk-22CV7V7A.js";
|
|
8
9
|
|
|
9
10
|
// bin/pushary-post-hook.ts
|
|
10
11
|
var main = async () => {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
handleUserPrompt
|
|
4
|
+
} from "../chunk-WCGKLHCL.js";
|
|
5
|
+
import "../chunk-CH53PBQN.js";
|
|
6
|
+
import "../chunk-3MIR7ODJ.js";
|
|
7
|
+
import "../chunk-VUNL35KE.js";
|
|
8
|
+
import "../chunk-22CV7V7A.js";
|
|
9
|
+
|
|
10
|
+
// bin/pushary-prompt-hook.ts
|
|
11
|
+
var main = async () => {
|
|
12
|
+
let rawInput = "";
|
|
13
|
+
for await (const chunk of process.stdin) {
|
|
14
|
+
rawInput += chunk;
|
|
15
|
+
}
|
|
16
|
+
if (!rawInput.trim()) {
|
|
17
|
+
process.exit(0);
|
|
18
|
+
}
|
|
19
|
+
try {
|
|
20
|
+
const input = JSON.parse(rawInput);
|
|
21
|
+
await handleUserPrompt(input);
|
|
22
|
+
} catch {
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
main();
|
|
@@ -3,18 +3,18 @@ import {
|
|
|
3
3
|
addClaudeMcpServer,
|
|
4
4
|
addPusharyHooks,
|
|
5
5
|
addPusharyToolPermissions
|
|
6
|
-
} from "../chunk-
|
|
6
|
+
} from "../chunk-5MA3CPZB.js";
|
|
7
7
|
import {
|
|
8
8
|
execNpm,
|
|
9
9
|
npmErrorMessage
|
|
10
10
|
} from "../chunk-RSHN2AQ7.js";
|
|
11
11
|
import {
|
|
12
12
|
isValidApiKey
|
|
13
|
-
} from "../chunk-
|
|
13
|
+
} from "../chunk-22CV7V7A.js";
|
|
14
14
|
|
|
15
15
|
// bin/pushary-setup.ts
|
|
16
|
-
import { existsSync, readFileSync, writeFileSync, appendFileSync, mkdirSync } from "fs";
|
|
17
|
-
import { join, dirname } from "path";
|
|
16
|
+
import { existsSync, readFileSync, writeFileSync, appendFileSync, mkdirSync, cpSync, rmSync } from "fs";
|
|
17
|
+
import { join, dirname, basename } from "path";
|
|
18
18
|
import { homedir } from "os";
|
|
19
19
|
import { execSync } from "child_process";
|
|
20
20
|
import { checkbox, input, confirm } from "@inquirer/prompts";
|
|
@@ -176,8 +176,7 @@ var printConnectInstructions = async (apiKey) => {
|
|
|
176
176
|
// bin/pushary-setup.ts
|
|
177
177
|
var CLAUDE_SETTINGS = join(homedir(), ".claude", "settings.json");
|
|
178
178
|
var CLAUDE_JSON = join(homedir(), ".claude.json");
|
|
179
|
-
var
|
|
180
|
-
var CURSOR_RULES_DIR = join(".cursor", "rules");
|
|
179
|
+
var CURSOR_PLUGIN_DIR = join(homedir(), ".cursor", "plugins", "local", "pushary");
|
|
181
180
|
var CLAUDE_SKILL_DIR = join(homedir(), ".claude", "skills", "pushary");
|
|
182
181
|
var CODEX_SKILL_DIR = join(homedir(), ".codex", "skills", "pushary");
|
|
183
182
|
var SHELL_FILES = [".zshrc", ".zprofile", ".bashrc", ".bash_profile"].map((f) => join(homedir(), f));
|
|
@@ -297,19 +296,6 @@ var installSkillToDir = async (dir, label) => {
|
|
|
297
296
|
writeFileSync(join(dir, "SKILL.md"), content, "utf-8");
|
|
298
297
|
});
|
|
299
298
|
};
|
|
300
|
-
var installCursorRule = async () => {
|
|
301
|
-
await spinner("Installing Pushary rules", async () => {
|
|
302
|
-
const content = await fetchSkillContent();
|
|
303
|
-
const body = content.replace(/^---[\s\S]*?---\n*/m, "");
|
|
304
|
-
const mdc = `---
|
|
305
|
-
alwaysApply: true
|
|
306
|
-
---
|
|
307
|
-
|
|
308
|
-
${body}`;
|
|
309
|
-
if (!existsSync(CURSOR_RULES_DIR)) mkdirSync(CURSOR_RULES_DIR, { recursive: true });
|
|
310
|
-
writeFileSync(join(CURSOR_RULES_DIR, "pushary.mdc"), mdc, "utf-8");
|
|
311
|
-
});
|
|
312
|
-
};
|
|
313
299
|
var setupClaudeCode = async (apiKey) => {
|
|
314
300
|
console.log(`
|
|
315
301
|
${bold2("Setting up Claude Code")}
|
|
@@ -324,7 +310,7 @@ var setupClaudeCode = async (apiKey) => {
|
|
|
324
310
|
addPusharyToolPermissions(settings);
|
|
325
311
|
});
|
|
326
312
|
await installGlobally();
|
|
327
|
-
await spinner("Adding hooks (PreToolUse, PostToolUse, Stop)", async () => {
|
|
313
|
+
await spinner("Adding hooks (PreToolUse, PostToolUse, UserPromptSubmit, Stop)", async () => {
|
|
328
314
|
let binDir;
|
|
329
315
|
try {
|
|
330
316
|
binDir = join(execNpm("prefix -g --no-workspaces", { timeout: 5e3 }).toString().trim(), "bin");
|
|
@@ -469,22 +455,45 @@ var setupCodex = async (_apiKey) => {
|
|
|
469
455
|
console.log(` ${dim2("\u2022")} Auto-allowed tools: no permission prompts for Pushary MCP calls`);
|
|
470
456
|
console.log(` ${dim2("\u2022")} Notify handler: captures turn completions and approval requests`);
|
|
471
457
|
};
|
|
458
|
+
var resolveBundledPlugin = () => {
|
|
459
|
+
const dir = dirname(fileURLToPath(import.meta.url));
|
|
460
|
+
const candidates = [
|
|
461
|
+
join(dir, "..", "..", "data", "cursor-plugin"),
|
|
462
|
+
join(dir, "..", "data", "cursor-plugin"),
|
|
463
|
+
join(dir, "..", "..", "..", "cursor-plugin"),
|
|
464
|
+
join(dir, "..", "..", "cursor-plugin")
|
|
465
|
+
];
|
|
466
|
+
return candidates.find((p) => existsSync(join(p, ".cursor-plugin", "plugin.json"))) ?? null;
|
|
467
|
+
};
|
|
472
468
|
var setupCursor = async (apiKey) => {
|
|
473
469
|
console.log(`
|
|
474
470
|
${bold2("Setting up Cursor")}
|
|
475
471
|
`);
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
};
|
|
484
|
-
|
|
485
|
-
|
|
472
|
+
const source = resolveBundledPlugin();
|
|
473
|
+
if (!source) throw new Error("bundled Cursor plugin not found in this package");
|
|
474
|
+
await spinner("Installing Pushary plugin", async () => {
|
|
475
|
+
rmSync(CURSOR_PLUGIN_DIR, { recursive: true, force: true });
|
|
476
|
+
cpSync(source, CURSOR_PLUGIN_DIR, {
|
|
477
|
+
recursive: true,
|
|
478
|
+
filter: (p) => !["tools", "node_modules", ".git", ".DS_Store"].includes(basename(p))
|
|
479
|
+
});
|
|
480
|
+
});
|
|
481
|
+
await spinner("Linking your API key", async () => {
|
|
482
|
+
const mcpPath = join(CURSOR_PLUGIN_DIR, "mcp.json");
|
|
483
|
+
const mcp = readJson(mcpPath);
|
|
484
|
+
const servers = mcp.mcpServers ?? {};
|
|
485
|
+
if (servers.pushary) {
|
|
486
|
+
servers.pushary.headers = { ...servers.pushary.headers, Authorization: `Bearer ${apiKey}` };
|
|
487
|
+
mcp.mcpServers = servers;
|
|
488
|
+
writeJson(mcpPath, mcp);
|
|
489
|
+
}
|
|
486
490
|
});
|
|
487
|
-
|
|
491
|
+
console.log();
|
|
492
|
+
console.log(` ${dim2("What this configured:")}`);
|
|
493
|
+
console.log(` ${dim2("\u2022")} Plugin installed to ~/.cursor/plugins/local/pushary`);
|
|
494
|
+
console.log(` ${dim2("\u2022")} MCP tools, the always-on rule, the skill, and the permission gate`);
|
|
495
|
+
console.log(` ${dim2("\u2022")} Risky shell commands route to push approval before they run`);
|
|
496
|
+
console.log(` ${dim2("\u2022")} Restart Cursor (or run Developer: Reload Window) to load it`);
|
|
488
497
|
};
|
|
489
498
|
var saveApiKey = async (apiKey) => {
|
|
490
499
|
await spinner("Saving API key to shell profile", async () => {
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
handleStop
|
|
4
|
-
} from "../chunk-
|
|
5
|
-
import "../chunk-
|
|
4
|
+
} from "../chunk-WCGKLHCL.js";
|
|
5
|
+
import "../chunk-CH53PBQN.js";
|
|
6
6
|
import "../chunk-3MIR7ODJ.js";
|
|
7
7
|
import "../chunk-VUNL35KE.js";
|
|
8
|
+
import "../chunk-22CV7V7A.js";
|
|
8
9
|
|
|
9
10
|
// bin/pushary-stop-hook.ts
|
|
10
11
|
var main = async () => {
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// ../contracts/src/index.ts
|
|
2
|
+
var APPROVAL_MODES = ["push_only", "terminal_only", "push_first", "notify_only"];
|
|
3
|
+
var isApprovalMode = (value) => typeof value === "string" && APPROVAL_MODES.includes(value);
|
|
4
|
+
var MATCH_RANKS = ["none", "tool", "prefix", "exact"];
|
|
5
|
+
var matchRankWeight = (rank) => MATCH_RANKS.indexOf(rank);
|
|
6
|
+
var matchToolPattern = (pattern, toolName, arg) => {
|
|
7
|
+
const open = pattern.indexOf("(");
|
|
8
|
+
if (open === -1 || !pattern.endsWith(")")) {
|
|
9
|
+
return pattern === toolName ? "tool" : "none";
|
|
10
|
+
}
|
|
11
|
+
if (pattern.slice(0, open) !== toolName || arg === void 0) return "none";
|
|
12
|
+
const inner = pattern.slice(open + 1, -1);
|
|
13
|
+
if (inner.endsWith(":*")) {
|
|
14
|
+
return arg.startsWith(inner.slice(0, -2)) ? "prefix" : "none";
|
|
15
|
+
}
|
|
16
|
+
return arg === inner ? "exact" : "none";
|
|
17
|
+
};
|
|
18
|
+
var POLICY_ARG_KEYS = {
|
|
19
|
+
Bash: "command",
|
|
20
|
+
Edit: "file_path",
|
|
21
|
+
Write: "file_path"
|
|
22
|
+
};
|
|
23
|
+
var extractPolicyArg = (toolName, toolInput) => {
|
|
24
|
+
const key = POLICY_ARG_KEYS[toolName];
|
|
25
|
+
if (!key) return void 0;
|
|
26
|
+
const value = toolInput[key];
|
|
27
|
+
return typeof value === "string" ? value : void 0;
|
|
28
|
+
};
|
|
29
|
+
var API_KEY_PATTERN = /^pk_[a-f0-9]+\.[a-f0-9]+$/;
|
|
30
|
+
var isValidApiKey = (value) => API_KEY_PATTERN.test(value);
|
|
31
|
+
|
|
32
|
+
export {
|
|
33
|
+
isApprovalMode,
|
|
34
|
+
matchRankWeight,
|
|
35
|
+
matchToolPattern,
|
|
36
|
+
extractPolicyArg,
|
|
37
|
+
isValidApiKey
|
|
38
|
+
};
|