@pushary/agent-hooks 0.65.0 → 0.67.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.
@@ -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 VS Code, 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.
@@ -4,8 +4,11 @@ import {
4
4
  removePusharySettings
5
5
  } from "../chunk-CAJZAFVS.js";
6
6
  import {
7
- removeInstructionBlock
8
- } from "../chunk-N7XJ4L2W.js";
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
- } from "../chunk-RU3CIBXY.js";
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"))}`);
@@ -6,7 +6,7 @@ import {
6
6
  confirmAppConnection,
7
7
  connectDevice,
8
8
  printConnectInstructions
9
- } from "../chunk-LZLXTM7P.js";
9
+ } from "../chunk-QTC7SR6A.js";
10
10
  import "../chunk-3EGEA4KH.js";
11
11
  import {
12
12
  readKeySource
@@ -3,11 +3,11 @@ import {
3
3
  describeWaitLadder
4
4
  } from "../chunk-IIENZE2J.js";
5
5
  import {
6
- detectAllAgents
7
- } from "../chunk-6G2BC4ET.js";
8
- import {
9
- hasInstructionBlock
10
- } from "../chunk-N7XJ4L2W.js";
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
- } from "../chunk-RU3CIBXY.js";
31
+ untrustedCodexHookEvents,
32
+ vscodePluginDir
33
+ } from "../chunk-7HG4WUIE.js";
33
34
  import {
34
35
  describeReach,
35
36
  fetchChannels,
36
37
  reachVerdict
37
- } from "../chunk-LZLXTM7P.js";
38
+ } from "../chunk-QTC7SR6A.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 {
@@ -4,7 +4,7 @@ import {
4
4
  codexConfigToml,
5
5
  cursorUserMcp,
6
6
  geminiSettings
7
- } from "../chunk-RU3CIBXY.js";
7
+ } from "../chunk-7HG4WUIE.js";
8
8
  import {
9
9
  clearKey,
10
10
  readKeySource
@@ -10,13 +10,15 @@ import {
10
10
  import {
11
11
  describeDetection,
12
12
  detectAllAgents,
13
- isDetected
14
- } from "../chunk-6G2BC4ET.js";
15
- import {
13
+ isDetected,
14
+ pluginLocationSnippet,
15
+ registerPluginLocation,
16
16
  renderAgentInstructions,
17
17
  renderProjectAgentInstructions,
18
+ shortenHome,
19
+ vscodeSettingsTargets,
18
20
  writeInstructionBlock
19
- } from "../chunk-N7XJ4L2W.js";
21
+ } from "../chunk-KB4ODLGB.js";
20
22
  import {
21
23
  GEMINI_HOOK_BINARY,
22
24
  addGeminiHooks,
@@ -37,15 +39,16 @@ import {
37
39
  cursorUserHooks,
38
40
  geminiMd,
39
41
  geminiSettings,
40
- pusharyConfigFile
41
- } from "../chunk-RU3CIBXY.js";
42
+ pusharyConfigFile,
43
+ vscodePluginDir
44
+ } from "../chunk-7HG4WUIE.js";
42
45
  import {
43
46
  confirmAppConnection,
44
47
  connectDevice,
45
48
  connectViaAppPairing,
46
49
  printConnectInstructions,
47
50
  setHumanStream
48
- } from "../chunk-LZLXTM7P.js";
51
+ } from "../chunk-QTC7SR6A.js";
49
52
  import "../chunk-3EGEA4KH.js";
50
53
  import {
51
54
  KEY_FILE_MODE,
@@ -262,7 +265,7 @@ var reportSetupAbortWith = async (deps, reason, exitCode, keyCheck = "unknown")
262
265
  var reportSetupAbort = (reason, exitCode, keyCheck) => reportSetupAbortWith(defaultDeps, reason, exitCode, keyCheck);
263
266
 
264
267
  // src/setup/options.ts
265
- var AGENT_CHOICES = ["claude_code", "codex", "gemini_cli", "hermes", "cursor", "custom"];
268
+ var AGENT_CHOICES = ["claude_code", "codex", "gemini_cli", "hermes", "cursor", "vscode", "custom"];
266
269
  var CONNECT_MODES = ["app", "browser", "web", "none"];
267
270
  var CONNECT_ALIASES = { auto: "app", pwa: "web" };
268
271
  var parseConnectMode = (raw) => {
@@ -340,6 +343,7 @@ var CLAUDE_SETTINGS = claudeSettings();
340
343
  var CLAUDE_JSON = claudeJson();
341
344
  var CURSOR_PLUGIN_DIR = cursorPluginDir();
342
345
  var CURSOR_USER_HOOKS = cursorUserHooks();
346
+ var VSCODE_PLUGIN_DIR = vscodePluginDir();
343
347
  var CLAUDE_SKILL_DIR = claudeSkillDir();
344
348
  var CODEX_HOME = codexHome();
345
349
  var CODEX_SKILL_DIR = codexSkillDir();
@@ -849,6 +853,120 @@ var setupCursor = async (apiKey) => {
849
853
  console.log(` ${dim("\u2022")} Fully quit and reopen Cursor to load it (a Reload Window may not be enough)`);
850
854
  noteManual("Fully quit and reopen Cursor. A Reload Window may not be enough.");
851
855
  };
856
+ var resolveBundledVsCodePlugin = () => {
857
+ const dir = dirname(fileURLToPath(import.meta.url));
858
+ const candidates = [
859
+ join(dir, "..", "..", "data", "vscode-plugin"),
860
+ join(dir, "..", "data", "vscode-plugin"),
861
+ join(dir, "..", "..", "..", "vscode-plugin"),
862
+ join(dir, "..", "..", "vscode-plugin")
863
+ ];
864
+ return candidates.find((p) => existsSync(join(p, ".claude-plugin", "plugin.json"))) ?? null;
865
+ };
866
+ var pinVsCodeGatePath = (pluginDir) => {
867
+ const hooksPath = join(pluginDir, "hooks", "hooks.json");
868
+ const data = readJson(hooksPath);
869
+ const entries = data.hooks?.PreToolUse;
870
+ if (!Array.isArray(entries) || entries.length === 0) {
871
+ throw new Error("bundled VS Code hooks.json is missing a PreToolUse entry");
872
+ }
873
+ const gate = join(pluginDir, "scripts", "pushary-gate.mjs");
874
+ data.hooks.PreToolUse = entries.map((entry) => ({ ...entry, command: `node "${gate}"` }));
875
+ writeJson(hooksPath, data);
876
+ };
877
+ var registerVsCodePlugin = (pluginDir) => {
878
+ const written = [];
879
+ const manual = [];
880
+ for (const settingsPath of vscodeSettingsTargets(existsSync)) {
881
+ const current = existsSync(settingsPath) ? readFileSync(settingsPath, "utf-8") : null;
882
+ const result = registerPluginLocation(current, pluginDir);
883
+ if (result.kind === "already") continue;
884
+ if (result.kind === "manual") {
885
+ manual.push(settingsPath);
886
+ continue;
887
+ }
888
+ if (current !== null) backupFile(settingsPath);
889
+ mkdirSync(dirname(settingsPath), { recursive: true });
890
+ writeFileAtomic(settingsPath, result.content);
891
+ written.push(settingsPath);
892
+ }
893
+ return { written, manual };
894
+ };
895
+ var setupVsCode = async (apiKey) => {
896
+ console.log(`
897
+ ${bold("Setting up VS Code")}
898
+ `);
899
+ const source = resolveBundledVsCodePlugin();
900
+ if (!source) throw new Error("bundled VS Code plugin not found in this package");
901
+ await spinner("Installing Pushary plugin", async () => {
902
+ const staging = join(dirname(VSCODE_PLUGIN_DIR), `.pushary-staging-vscode-${process.pid}`);
903
+ const backup = `${VSCODE_PLUGIN_DIR}.pushary-backup-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}`;
904
+ mkdirSync(dirname(VSCODE_PLUGIN_DIR), { recursive: true });
905
+ rmSync(staging, { recursive: true, force: true });
906
+ try {
907
+ cpSync(source, staging, {
908
+ recursive: true,
909
+ filter: (p) => !["tools", "node_modules", ".git", ".DS_Store"].includes(basename(p))
910
+ });
911
+ const staged = readJsonSafe(join(staging, ".claude-plugin", "plugin.json"));
912
+ if (staged.kind !== "ok") {
913
+ throw new Error("staged VS Code plugin is missing or has an unreadable plugin.json");
914
+ }
915
+ const hadExisting = existsSync(VSCODE_PLUGIN_DIR);
916
+ if (hadExisting) renameSync(VSCODE_PLUGIN_DIR, backup);
917
+ try {
918
+ renameSync(staging, VSCODE_PLUGIN_DIR);
919
+ } catch (err) {
920
+ if (hadExisting) renameSync(backup, VSCODE_PLUGIN_DIR);
921
+ throw err;
922
+ }
923
+ if (hadExisting) rmSync(backup, { recursive: true, force: true });
924
+ } finally {
925
+ rmSync(staging, { recursive: true, force: true });
926
+ }
927
+ });
928
+ await spinner("Linking your API key", async () => {
929
+ const mcpPath = join(VSCODE_PLUGIN_DIR, ".mcp.json");
930
+ const mcp = readAgentJson(mcpPath);
931
+ const servers = mcp.mcpServers ?? {};
932
+ if (servers.pushary) {
933
+ servers.pushary.headers = { ...servers.pushary.headers, Authorization: `Bearer ${apiKey}` };
934
+ mcp.mcpServers = servers;
935
+ writeJsonAtomic(mcpPath, mcp, KEY_FILE_MODE);
936
+ }
937
+ pinVsCodeGatePath(VSCODE_PLUGIN_DIR);
938
+ });
939
+ let registration = { written: [], manual: [] };
940
+ await spinner("Registering the plugin with VS Code", async () => {
941
+ registration = registerVsCodePlugin(VSCODE_PLUGIN_DIR);
942
+ });
943
+ console.log();
944
+ console.log(` ${dim("What this configured:")}`);
945
+ console.log(` ${dim("\u2022")} Plugin installed to ${shortenHome(VSCODE_PLUGIN_DIR)} (MCP tools, skill, commands)`);
946
+ for (const path of registration.written) {
947
+ console.log(` ${dim("\u2022")} Registered in ${shortenHome(path)} under chat.pluginLocations`);
948
+ }
949
+ console.log(` ${dim("\u2022")} Risky shell commands route to push approval before they run`);
950
+ if (registration.manual.length > 0) {
951
+ console.log();
952
+ console.log(` ${yellow("!")} ${bold("One step left, by hand")}`);
953
+ console.log(` ${dim("These settings files already have a chat.pluginLocations block, and")}`);
954
+ console.log(` ${dim("they contain comments, so they were left untouched rather than rewritten.")}`);
955
+ console.log(` ${dim("Add this entry inside that block:")}`);
956
+ console.log();
957
+ console.log(` ${cyan(`${JSON.stringify(VSCODE_PLUGIN_DIR)}: true`)}`);
958
+ console.log();
959
+ for (const path of registration.manual) {
960
+ console.log(` ${dim("in")} ${path}`);
961
+ }
962
+ noteManual(
963
+ `Add ${JSON.stringify(VSCODE_PLUGIN_DIR)}: true to the chat.pluginLocations block in ${registration.manual.join(" and ")}. Full snippet:
964
+ ${pluginLocationSnippet(VSCODE_PLUGIN_DIR)}`
965
+ );
966
+ }
967
+ console.log(` ${dim("\u2022")} Fully quit and reopen VS Code to load the plugin`);
968
+ noteManual("Fully quit and reopen VS Code. A Reload Window may not pick up a new plugin location.");
969
+ };
852
970
  var saveApiKey = async (apiKey) => {
853
971
  let result;
854
972
  await spinner("Saving your API key", async () => {
@@ -941,6 +1059,7 @@ var AGENT_SETUP = {
941
1059
  gemini_cli: setupGemini,
942
1060
  hermes: setupHermes,
943
1061
  cursor: setupCursor,
1062
+ vscode: setupVsCode,
944
1063
  custom: setupCustom
945
1064
  };
946
1065
  var PROJECT_INSTRUCTION_TARGETS = {
@@ -1042,6 +1161,7 @@ var AGENT_NAMES = {
1042
1161
  gemini_cli: "Gemini CLI",
1043
1162
  hermes: "Hermes",
1044
1163
  cursor: "Cursor",
1164
+ vscode: "VS Code",
1045
1165
  custom: "Other"
1046
1166
  };
1047
1167
  var AGENT_CAPABILITIES = {
@@ -1050,6 +1170,7 @@ var AGENT_CAPABILITIES = {
1050
1170
  gemini_cli: "MCP + native hooks + auto-allowed tools",
1051
1171
  hermes: "native plugin + auto-error notifications",
1052
1172
  cursor: "plugin + permission gate",
1173
+ vscode: "agent plugin + permission gate",
1053
1174
  custom: "any MCP or HTTP agent (Windsurf, n8n, custom)"
1054
1175
  };
1055
1176
  var NAME_COLUMN = Math.max(...Object.values(AGENT_NAMES).map((name) => name.length)) + 2;
@@ -1111,6 +1232,7 @@ var AGENT_TARGETS = {
1111
1232
  gemini_cli: [GEMINI_SETTINGS, GEMINI_MD],
1112
1233
  hermes: ["the Hermes virtualenv (pip install pushary-hermes)"],
1113
1234
  cursor: [join(CURSOR_PLUGIN_DIR, "mcp.json"), CURSOR_USER_HOOKS],
1235
+ vscode: [join(VSCODE_PLUGIN_DIR, ".mcp.json"), ...vscodeSettingsTargets(existsSync)],
1114
1236
  custom: ["nothing (prints connection details only)"]
1115
1237
  };
1116
1238
  var reportDryRun = (apiKey, agents, keyCheck) => {
@@ -7,7 +7,7 @@ import {
7
7
  describeReach,
8
8
  fetchChannels,
9
9
  reachVerdict
10
- } from "../chunk-LZLXTM7P.js";
10
+ } from "../chunk-QTC7SR6A.js";
11
11
  import "../chunk-3EGEA4KH.js";
12
12
  import {
13
13
  readKeySource