@pushary/agent-hooks 0.68.0 → 0.70.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.
Files changed (45) hide show
  1. package/CHANGELOG.md +138 -0
  2. package/README.md +3 -2
  3. package/data/cursor-plugin/scripts/pushary-gate.mjs +70 -15
  4. package/data/vscode-plugin/scripts/pushary-gate.mjs +59 -11
  5. package/dist/bin/pushary-claude.js +21 -11
  6. package/dist/bin/pushary-clean.js +60 -22
  7. package/dist/bin/pushary-codex-hook.js +38 -23
  8. package/dist/bin/pushary-codex.js +1 -1
  9. package/dist/bin/pushary-connect.js +6 -4
  10. package/dist/bin/pushary-daemon.js +3 -3
  11. package/dist/bin/pushary-doctor.js +120 -28
  12. package/dist/bin/pushary-gemini-hook.js +32 -16
  13. package/dist/bin/pushary-hook.js +5 -5
  14. package/dist/bin/pushary-login.js +2 -2
  15. package/dist/bin/pushary-logout.js +2 -2
  16. package/dist/bin/pushary-mode.js +2 -2
  17. package/dist/bin/pushary-notification-hook.js +1 -1
  18. package/dist/bin/pushary-permission-denied-hook.js +3 -3
  19. package/dist/bin/pushary-permission-hook.js +3 -3
  20. package/dist/bin/pushary-post-hook.js +1 -1
  21. package/dist/bin/pushary-prompt-hook.js +1 -1
  22. package/dist/bin/pushary-session-end-hook.js +1 -1
  23. package/dist/bin/pushary-session-start-hook.js +1 -1
  24. package/dist/bin/pushary-setup.js +90 -13
  25. package/dist/bin/pushary-stats.js +2 -2
  26. package/dist/bin/pushary-status.js +4 -4
  27. package/dist/bin/pushary-stop-hook.js +1 -1
  28. package/dist/bin/pushary-stopfailure-hook.js +1 -1
  29. package/dist/bin/pushary-suggestions.js +2 -2
  30. package/dist/bin/pushary-upgrade.js +2 -2
  31. package/dist/bin/pushary-wait.js +2 -2
  32. package/dist/bin/pushary.js +1 -1
  33. package/dist/{chunk-JGH37QRB.js → chunk-7OYGFJYZ.js} +1 -1
  34. package/dist/chunk-B7R4YSAB.js +38 -0
  35. package/dist/{chunk-ATP6UQPH.js → chunk-BBK3TTU2.js} +1 -1
  36. package/dist/{chunk-XOUYM27W.js → chunk-FZIQSDTQ.js} +207 -113
  37. package/dist/chunk-GD275GEX.js +66 -0
  38. package/dist/{chunk-SONXGCZY.js → chunk-HU43EZ5Q.js} +45 -27
  39. package/dist/{chunk-KB4ODLGB.js → chunk-I4BYZIX4.js} +2 -0
  40. package/dist/{chunk-YMFZUXL5.js → chunk-MGFZXUUR.js} +57 -4
  41. package/dist/{chunk-YZT2CBC3.js → chunk-NQTRA7H5.js} +1 -1
  42. package/dist/src/index.d.ts +1 -0
  43. package/dist/src/index.js +3 -3
  44. package/package.json +1 -1
  45. package/dist/chunk-YHG74UFF.js +0 -12
@@ -1,8 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
+ KILL_REASON,
3
4
  denyReasonFrom,
4
- isDeferAnswer
5
- } from "../chunk-YHG74UFF.js";
5
+ isDeferAnswer,
6
+ resolveGate
7
+ } from "../chunk-GD275GEX.js";
6
8
  import {
7
9
  CODEX_AGENT,
8
10
  DEFAULT_SESSION,
@@ -24,14 +26,16 @@ import {
24
26
  permissionTimeoutDecision,
25
27
  preToolUseTimeoutDecision,
26
28
  readLastPrompt,
29
+ repoKeyFor,
27
30
  reportEvent,
28
- resolvePolicy,
29
31
  savePendingQuestion,
32
+ scopePathFor,
30
33
  sendNotification,
31
34
  toCodexWire,
32
35
  toPolicyLookup,
36
+ toPolicyLookups,
33
37
  waitForAnswer
34
- } from "../chunk-XOUYM27W.js";
38
+ } from "../chunk-FZIQSDTQ.js";
35
39
  import {
36
40
  isGatingMoment,
37
41
  recordKeylessMoment
@@ -57,7 +61,6 @@ import "../chunk-KZERVKTD.js";
57
61
  import { basename, join } from "path";
58
62
  import { tmpdir, userInfo } from "os";
59
63
  import { existsSync, mkdirSync, statSync, unlinkSync, writeFileSync } from "fs";
60
- var KILL_REASON = "Stopped by user: this agent was halted from Pushary";
61
64
  var START_MS = Date.now();
62
65
  var APPROVAL_TTL_MS = 10 * 60 * 1e3;
63
66
  var sanitizeId = (value) => value.replace(/[^A-Za-z0-9_-]/g, "_").slice(0, 128);
@@ -167,18 +170,33 @@ var notifyApprovalNeeded = async (apiKey, input, lookup) => {
167
170
  } catch {
168
171
  }
169
172
  };
173
+ var codexGate = async (apiKey, input) => {
174
+ const modeState = await fetchModeState(apiKey, input.session_id);
175
+ const lookup = toPolicyLookup(input.tool_name ?? "", input.tool_input ?? {}, input.cwd);
176
+ const lookups = toPolicyLookups(input.tool_name ?? "", input.tool_input ?? {}, input.cwd);
177
+ const config = await getPolicy(apiKey, modeState.policyVersion, "codex");
178
+ const verdict = resolveGate({
179
+ modeState,
180
+ config,
181
+ toolName: lookups.tool,
182
+ toolInputs: lookups.inputs,
183
+ // Skipped entirely without a contract, which is the state of nearly every
184
+ // session. A multi-file patch contributes one path per file.
185
+ scopePaths: modeState.scope ? lookups.inputs.map((entry) => scopePathFor(lookups.tool, entry, input.cwd)).filter((path) => typeof path === "string") : [],
186
+ cwd: input.cwd,
187
+ repoKey: repoKeyFor(input.cwd)
188
+ });
189
+ return { verdict, lookup };
190
+ };
170
191
  var decidePermissionRequest = async (input) => {
171
192
  try {
172
193
  const apiKey = getApiKey();
173
- const modeState = await fetchModeState(apiKey, input.session_id);
174
- if (modeState.kill) return codexDeny(KILL_REASON);
175
- const lookup = toPolicyLookup(input.tool_name ?? "", input.tool_input ?? {}, input.cwd);
176
- const policy = await getPolicy(apiKey, modeState.policyVersion, "codex");
177
- const toolPolicy = resolvePolicy(policy, lookup.tool, modeState.mode, lookup.input);
178
- if (toolPolicy.timeoutSeconds === 0 && toolPolicy.timeoutAction === "approve") return codexAllow();
179
- if (toolPolicy.timeoutSeconds === 0 && toolPolicy.timeoutAction === "deny") {
180
- return codexDeny(`Denied by policy for ${toolPolicy.tool}`);
181
- }
194
+ const { verdict, lookup } = await codexGate(apiKey, input);
195
+ if (verdict.kind === "kill") return codexDeny(KILL_REASON);
196
+ if (verdict.kind === "allow") return codexAllow();
197
+ if (verdict.kind === "deny") return codexDeny(verdict.reason);
198
+ if (verdict.kind === "defer") return codexPass();
199
+ const toolPolicy = verdict.policy;
182
200
  if (toolPolicy.mode === "terminal_only") return codexPass();
183
201
  if (toolPolicy.mode === "notify_only") {
184
202
  if (claimNotify(input.tool_use_id)) await notifyApprovalNeeded(apiKey, input, lookup);
@@ -229,15 +247,12 @@ var decidePreToolUse = async (input) => {
229
247
  return codexPass();
230
248
  }
231
249
  try {
232
- const modeState = await fetchModeState(apiKey, input.session_id);
233
- if (modeState.kill) return codexDeny(KILL_REASON);
234
- const lookup = toPolicyLookup(input.tool_name ?? "", input.tool_input ?? {}, input.cwd);
235
- const policy = await getPolicy(apiKey, modeState.policyVersion, "codex");
236
- const toolPolicy = resolvePolicy(policy, lookup.tool, modeState.mode, lookup.input);
237
- if (toolPolicy.timeoutSeconds === 0 && toolPolicy.timeoutAction === "approve") return codexPass();
238
- if (toolPolicy.timeoutSeconds === 0 && toolPolicy.timeoutAction === "deny") {
239
- return codexDeny(`Denied by policy for ${toolPolicy.tool}`);
240
- }
250
+ const { verdict, lookup } = await codexGate(apiKey, input);
251
+ if (verdict.kind === "kill") return codexDeny(KILL_REASON);
252
+ if (verdict.kind === "allow") return codexPass();
253
+ if (verdict.kind === "deny") return codexDeny(verdict.reason);
254
+ if (verdict.kind === "defer") return codexPass();
255
+ const toolPolicy = verdict.policy;
241
256
  if (toolPolicy.mode === "push_only") {
242
257
  if (consumeApproval(input.tool_use_id)) return codexPass();
243
258
  let pushed;
@@ -3,7 +3,7 @@ import {
3
3
  askUser,
4
4
  reportEvent,
5
5
  waitForAnswer
6
- } from "../chunk-XOUYM27W.js";
6
+ } from "../chunk-FZIQSDTQ.js";
7
7
  import "../chunk-BSZYIAZL.js";
8
8
  import "../chunk-SAF6HGAA.js";
9
9
  import "../chunk-7QLSKOSU.js";
@@ -6,7 +6,7 @@ import {
6
6
  confirmAppConnection,
7
7
  connectDevice,
8
8
  printConnectInstructions
9
- } from "../chunk-YMFZUXL5.js";
9
+ } from "../chunk-MGFZXUUR.js";
10
10
  import "../chunk-3EGEA4KH.js";
11
11
  import {
12
12
  readKeySource
@@ -24,8 +24,8 @@ import {
24
24
  } from "../chunk-GMXKITVA.js";
25
25
  import {
26
26
  exitOnHelpFlag
27
- } from "../chunk-ATP6UQPH.js";
28
- import "../chunk-JGH37QRB.js";
27
+ } from "../chunk-BBK3TTU2.js";
28
+ import "../chunk-7OYGFJYZ.js";
29
29
  import "../chunk-BSZYIAZL.js";
30
30
  import "../chunk-SAF6HGAA.js";
31
31
  import {
@@ -84,7 +84,9 @@ var main = async () => {
84
84
  console.log();
85
85
  process.exit(EXIT.OK);
86
86
  }
87
- console.log(` ${dim("No device of yours can answer yet. Run")} ${cyan("pushary connect")} ${dim("again when you are ready.")}`);
87
+ console.log(` ${dim("No device of yours can answer yet.")}`);
88
+ for (const line of result.repair ?? []) console.log(` ${dim(line)}`);
89
+ console.log(` ${dim("Then run")} ${cyan("pushary connect")} ${dim("again.")}`);
88
90
  console.log();
89
91
  process.exit(EXIT.NO_DEVICE);
90
92
  };
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  HEARTBEAT_INTERVAL_MS,
4
4
  sendHeartbeat
5
- } from "../chunk-YZT2CBC3.js";
5
+ } from "../chunk-NQTRA7H5.js";
6
6
  import {
7
7
  spawnClaude
8
8
  } from "../chunk-XHKBHWLX.js";
@@ -11,8 +11,8 @@ import {
11
11
  } from "../chunk-MUEW424A.js";
12
12
  import {
13
13
  exitOnHelpFlag
14
- } from "../chunk-ATP6UQPH.js";
15
- import "../chunk-JGH37QRB.js";
14
+ } from "../chunk-BBK3TTU2.js";
15
+ import "../chunk-7OYGFJYZ.js";
16
16
  import {
17
17
  getMachineId
18
18
  } from "../chunk-RN3NOEJF.js";
@@ -3,11 +3,19 @@ import {
3
3
  describeWaitLadder
4
4
  } from "../chunk-IIENZE2J.js";
5
5
  import {
6
+ claudeWired,
7
+ codexWired,
8
+ geminiWired
9
+ } from "../chunk-B7R4YSAB.js";
10
+ import {
11
+ agentProbes,
12
+ detectAgent,
6
13
  detectAllAgents,
7
14
  hasInstructionBlock,
15
+ isDetected,
8
16
  isPluginRegistered,
9
17
  vscodeSettingsTargets
10
- } from "../chunk-KB4ODLGB.js";
18
+ } from "../chunk-I4BYZIX4.js";
11
19
  import {
12
20
  GEMINI_HOOK_BINARY,
13
21
  hasGeminiHooks,
@@ -27,6 +35,7 @@ import {
27
35
  geminiSettings,
28
36
  hasCodexHooks,
29
37
  missingCodexHookEvents,
38
+ pusharyConfigFile,
30
39
  readCodexMcpAuth,
31
40
  untrustedCodexHookEvents,
32
41
  vscodePluginDir
@@ -35,7 +44,7 @@ import {
35
44
  describeReach,
36
45
  fetchChannels,
37
46
  reachVerdict
38
- } from "../chunk-YMFZUXL5.js";
47
+ } from "../chunk-MGFZXUUR.js";
39
48
  import "../chunk-3EGEA4KH.js";
40
49
  import {
41
50
  keyPrefix,
@@ -58,10 +67,10 @@ import {
58
67
  } from "../chunk-MUEW424A.js";
59
68
  import {
60
69
  exitOnHelpFlag
61
- } from "../chunk-ATP6UQPH.js";
70
+ } from "../chunk-BBK3TTU2.js";
62
71
  import {
63
72
  getPackageVersion
64
- } from "../chunk-JGH37QRB.js";
73
+ } from "../chunk-7OYGFJYZ.js";
65
74
  import {
66
75
  readLedgerSummary
67
76
  } from "../chunk-WLGEY4UX.js";
@@ -135,6 +144,16 @@ var redactText = (text) => text.replace(KEY_PATTERN, (match) => `${match.split("
135
144
  var redact = (bundle) => JSON.parse(redactText(JSON.stringify(bundle)));
136
145
  var bundleFileName = (bundle) => `pushary-diagnostics-${bundle.generatedAt.replace(/[:.]/g, "-")}.json`;
137
146
 
147
+ // src/diagnostics/exit-code.ts
148
+ var resolveDoctorExit = ({ keyExit, failures }) => {
149
+ if (keyExit !== null) return keyExit;
150
+ if (failures.length === 0) return EXIT.OK;
151
+ const every = (kind) => failures.every((f) => f.kind === kind);
152
+ if (every("unreachable")) return EXIT.UNREACHABLE;
153
+ if (every("reachability")) return EXIT.NO_DEVICE;
154
+ return EXIT.PROBLEMS_FOUND;
155
+ };
156
+
138
157
  // bin/pushary-doctor.ts
139
158
  exitOnHelpFlag("doctor");
140
159
  var parseDoctorOptions = (argv) => {
@@ -215,12 +234,21 @@ var extractHookTimeout = (entries, needle) => {
215
234
  return null;
216
235
  };
217
236
  var results = [];
218
- var check = (passed, label, detail) => {
219
- results.push({ passed, label, detail });
237
+ var check = (passed, label, detail, kind = "config") => {
238
+ results.push({ passed, label, detail, kind });
220
239
  const icon = passed ? pass : fail;
221
240
  const suffix = detail ? ` ${dim(`(${detail})`)}` : "";
222
241
  console.log(` ${icon} ${label}${suffix}`);
223
242
  };
243
+ var probes = agentProbes();
244
+ var installed = (agent) => isDetected(detectAgent(probes[agent]));
245
+ var agentsChecked = [];
246
+ var skipped = (label, why) => {
247
+ console.log(` ${dim("\u2013")} ${label} ${dim(`(${why})`)}`);
248
+ };
249
+ var checking = (label) => {
250
+ agentsChecked.push(label);
251
+ };
224
252
  var main = async () => {
225
253
  console.log();
226
254
  console.log(` ${bold("Pushary Doctor")}`);
@@ -247,8 +275,12 @@ var main = async () => {
247
275
  if (keySource === "config-file") {
248
276
  console.log(` ${pass} API key in ${dim(source.origin ?? "the config file")} ${dim(`(${maskKey(apiKey)})`)}`);
249
277
  } else if (keySource === "profile") {
250
- console.log(` ${pass} API key in shell profile ${dim(`(${maskKey(apiKey)})`)}`);
251
- console.log(` ${warn} Re-run ${cyan("npx @pushary/agent-hooks setup")} so hooks read it from ${dim("~/.pushary/config.json")} without a shell reload`);
278
+ check(
279
+ false,
280
+ "API key readable by hooks",
281
+ `found only in a shell profile (${maskKey(apiKey)}), which hooks do not read \u2014 re-run setup, or open a new terminal`
282
+ );
283
+ console.log(` ${dim("Hooks read $PUSHARY_API_KEY, then")} ${dim(pusharyConfigFile())}`);
252
284
  } else {
253
285
  check(!!apiKey, "API key in environment", apiKey ? maskKey(apiKey) : "PUSHARY_API_KEY not set");
254
286
  }
@@ -282,19 +314,34 @@ var main = async () => {
282
314
  check(false, "Key format", "expected pk_xxx.xxx");
283
315
  break;
284
316
  case "unreachable":
285
- console.log(` ${warn} Could not verify the key with pushary.com ${dim(`(${keyVerdict.detail})`)}`);
317
+ check(false, "Key verified with pushary.com", `could not reach pushary.com (${keyVerdict.detail})`, "unreachable");
286
318
  break;
287
319
  }
288
320
  }
289
321
  const CLAUDE_JSON = claudeJson();
290
- const claudeJson2 = readJson(CLAUDE_JSON);
322
+ const claudeInPlay = claudeWired({
323
+ claudeJson: readJson(CLAUDE_JSON),
324
+ settings: readJson(CLAUDE_SETTINGS),
325
+ skillExists: existsSync(SKILL_PATH)
326
+ });
327
+ const claudeJson2 = claudeInPlay ? readJson(CLAUDE_JSON) : null;
291
328
  const mcpServers = claudeJson2?.mcpServers ?? {};
292
329
  const pusharyServer = mcpServers?.pushary;
293
- check(!!pusharyServer, "Claude Code: MCP server configured");
294
- if (pusharyServer) {
295
- check(pusharyServer.type === "http", "Claude Code: MCP server type", pusharyServer.type ? String(pusharyServer.type) : 'missing \u2014 add type: "http"');
330
+ if (!claudeInPlay) {
331
+ skipped("Claude Code", installed("claude_code") ? "installed, but Pushary is not set up for it" : "not installed");
332
+ } else {
333
+ checking("Claude Code");
334
+ check(!!pusharyServer, "Claude Code: MCP server configured");
335
+ if (pusharyServer) {
336
+ check(pusharyServer.type === "http", "Claude Code: MCP server type", pusharyServer.type ? String(pusharyServer.type) : 'missing \u2014 add type: "http"');
337
+ const claudeAuth = pusharyServer.headers?.Authorization;
338
+ if (typeof claudeAuth === "string" && apiKey) {
339
+ const matches = claudeAuth === `Bearer ${apiKey}`;
340
+ check(matches, "Claude Code: MCP key matches active key", matches ? void 0 : "the key in ~/.claude.json is not the key hooks use (rotated?) \u2014 re-run setup");
341
+ }
342
+ }
296
343
  }
297
- const settings = readJson(CLAUDE_SETTINGS);
344
+ const settings = claudeInPlay ? readJson(CLAUDE_SETTINGS) : null;
298
345
  if (settings) {
299
346
  const hooks = settings.hooks;
300
347
  const CLAUDE_HOOK_CHECKS = [
@@ -325,19 +372,24 @@ var main = async () => {
325
372
  if (hasLegacyPerms) {
326
373
  console.log(` ${warn} Legacy individual permissions detected ${dim("(run pushary clean, then setup again)")}`);
327
374
  }
328
- } else {
375
+ } else if (claudeInPlay) {
329
376
  check(false, "Claude Code: settings.json", "not found");
330
377
  }
331
378
  const codexConfigPath = codexConfigToml();
332
379
  const codexHooksPath = codexHooksJson();
333
380
  const codexHooksConfig = readJson(codexHooksPath);
334
- if (existsSync(codexConfigPath) || codexHooksConfig) {
335
- const codexConfig = existsSync(codexConfigPath) ? readFileSync(codexConfigPath, "utf-8") : "";
336
- let codexParsed = {};
337
- try {
338
- codexParsed = parseTOML(codexConfig);
339
- } catch {
340
- }
381
+ const codexConfig = existsSync(codexConfigPath) ? readFileSync(codexConfigPath, "utf-8") : "";
382
+ let codexParsed = {};
383
+ try {
384
+ codexParsed = parseTOML(codexConfig);
385
+ } catch {
386
+ }
387
+ if (codexWired({
388
+ config: codexParsed,
389
+ hooks: codexHooksConfig,
390
+ skillExists: existsSync(join(codexSkillDir(), "SKILL.md"))
391
+ })) {
392
+ checking("Codex");
341
393
  const codexMcp = codexParsed.mcp_servers?.pushary;
342
394
  const codexTransport = !!codexMcp && (typeof codexMcp.url === "string" || typeof codexMcp.command === "string");
343
395
  const codexAuth = readCodexMcpAuth(codexParsed);
@@ -401,10 +453,13 @@ var main = async () => {
401
453
  const codexAgentsMd2 = codexAgentsMd();
402
454
  const hasCodexInstructions = hasInstructionBlock(codexAgentsMd2);
403
455
  check(hasCodexInstructions, "Codex: proactive instructions (AGENTS.md)", hasCodexInstructions ? codexAgentsMd2 : "missing \u2014 re-run setup so Codex asks via push, not the terminal");
456
+ } else {
457
+ skipped("Codex", installed("codex") ? "installed, but Pushary is not set up for it" : "not installed");
404
458
  }
405
459
  const geminiSettingsPath = geminiSettings();
406
460
  const geminiSettings2 = readJson(geminiSettingsPath);
407
- if (geminiSettings2) {
461
+ if (geminiSettings2 && geminiWired({ settings: geminiSettings2 })) {
462
+ checking("Gemini CLI");
408
463
  const geminiServers = geminiSettings2.mcpServers ?? {};
409
464
  const geminiPushary = geminiServers.pushary;
410
465
  check(!!geminiPushary, "Gemini CLI: MCP server configured");
@@ -443,12 +498,15 @@ var main = async () => {
443
498
  const geminiMd2 = geminiMd();
444
499
  const hasGeminiInstructions = hasInstructionBlock(geminiMd2);
445
500
  check(hasGeminiInstructions, "Gemini CLI: proactive instructions (GEMINI.md)", hasGeminiInstructions ? geminiMd2 : "missing \u2014 re-run setup so Gemini asks via push, not the terminal");
501
+ } else {
502
+ skipped("Gemini CLI", installed("gemini_cli") ? "installed, but Pushary is not set up for it" : "not installed");
446
503
  }
447
504
  const cursorPluginDir2 = cursorPluginDir();
448
505
  if (existsSync(cursorPluginDir2)) {
506
+ checking("Cursor");
449
507
  const cursorUserHooks2 = cursorUserHooks();
450
508
  const userHooks = readJson(cursorUserHooks2);
451
- const gateEntry = userHooks.hooks?.beforeShellExecution?.find(
509
+ const gateEntry = userHooks?.hooks?.beforeShellExecution?.find(
452
510
  (h) => String(h.command ?? "").includes("pushary-gate")
453
511
  );
454
512
  if (!gateEntry) {
@@ -465,12 +523,17 @@ var main = async () => {
465
523
  check(false, "Cursor: API key linked", "no Authorization in plugin mcp.json \u2014 re-run setup");
466
524
  } else if (cursorAuth.includes("PUSHARY_API_KEY")) {
467
525
  check(false, "Cursor: API key linked", "plugin relies on $PUSHARY_API_KEY; re-run setup to embed the key so the gate works when Cursor is launched from the GUI");
526
+ } else if (apiKey && cursorAuth !== `Bearer ${apiKey}`) {
527
+ check(false, "Cursor: API key matches active key", "the embedded key is not the key hooks use (rotated?) \u2014 re-run setup");
468
528
  } else {
469
529
  check(true, "Cursor: API key linked", "embedded in plugin mcp.json");
470
530
  }
531
+ } else {
532
+ skipped("Cursor", installed("cursor") ? "installed, but Pushary is not set up for it" : "not installed");
471
533
  }
472
534
  const vscodePluginDir2 = vscodePluginDir();
473
535
  if (existsSync(vscodePluginDir2)) {
536
+ checking("VS Code");
474
537
  const registeredIn = vscodeSettingsTargets(existsSync).filter(
475
538
  (settingsPath) => existsSync(settingsPath) && isPluginRegistered(readFileSync(settingsPath, "utf-8"), vscodePluginDir2)
476
539
  );
@@ -499,11 +562,22 @@ var main = async () => {
499
562
  check(false, "VS Code: API key linked", "no Authorization in plugin .mcp.json \u2014 re-run setup");
500
563
  } else if (vscodeAuth.includes("PUSHARY_API_KEY")) {
501
564
  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");
565
+ } else if (apiKey && vscodeAuth !== `Bearer ${apiKey}`) {
566
+ check(false, "VS Code: API key matches active key", "the embedded key is not the key hooks use (rotated?) \u2014 re-run setup");
502
567
  } else {
503
568
  check(true, "VS Code: API key linked", "embedded in plugin .mcp.json");
504
569
  }
570
+ } else {
571
+ skipped("VS Code", installed("vscode") ? "installed, but Pushary is not set up for it" : "not installed");
572
+ }
573
+ check(
574
+ agentsChecked.length > 0,
575
+ "Wired to at least one agent",
576
+ agentsChecked.length > 0 ? agentsChecked.join(", ") : "Pushary is installed but configured for no agent \u2014 run npx @pushary/agent-hooks@latest setup"
577
+ );
578
+ if (claudeInPlay) {
579
+ check(existsSync(SKILL_PATH), "Skill installed", existsSync(SKILL_PATH) ? SKILL_PATH : "not found");
505
580
  }
506
- check(existsSync(SKILL_PATH), "Skill installed", existsSync(SKILL_PATH) ? SKILL_PATH : "not found");
507
581
  let globalVersion2 = "";
508
582
  try {
509
583
  globalVersion2 = execNpm("list -g --no-workspaces @pushary/agent-hooks --depth=0", { timeout: 1e4 }).toString().match(/@pushary\/agent-hooks@([\d.]+)/)?.[1] ?? "";
@@ -613,11 +687,21 @@ var main = async () => {
613
687
  console.log(` ${dim("Notification Delivery")}`);
614
688
  const probe = await fetchChannels(apiKey);
615
689
  if (probe.kind !== "ok") {
616
- check(false, "Phone reachable", probe.kind === "unreachable" ? `could not read your devices (${probe.detail})` : probe.kind === "locked" ? "no plan active, so nothing can be delivered" : "the server rejected this key");
690
+ check(
691
+ false,
692
+ "Phone reachable",
693
+ probe.kind === "unreachable" ? `could not read your devices (${probe.detail})` : probe.kind === "locked" ? "no plan active, so nothing can be delivered" : "the server rejected this key",
694
+ probe.kind === "unreachable" ? "unreachable" : "config"
695
+ );
617
696
  } else if (probe.status.precise) {
618
697
  const channels = probe.status;
619
698
  const verdict = reachVerdict(channels);
620
- check(verdict.kind === "yours", "Phone reachable", verdict.kind === "yours" ? describeReach(channels) : verdict.kind === "nobody" ? "nothing connected, run pushary connect or get the app at https://pushary.com/download" : `${describeReach(channels)}; run pushary connect to add your own`);
699
+ check(
700
+ verdict.kind === "yours",
701
+ "Phone reachable",
702
+ verdict.kind === "yours" ? describeReach(channels) : verdict.kind === "nobody" ? "nothing connected, run pushary connect or get the app at https://pushary.com/download" : `${describeReach(channels)}; run pushary connect to add your own`,
703
+ "reachability"
704
+ );
621
705
  }
622
706
  const pushSuppressed = !options.roundtrip && (options.noPush || options.json || !process.stdout.isTTY);
623
707
  if (pushSuppressed) {
@@ -662,6 +746,14 @@ var main = async () => {
662
746
  });
663
747
  const elapsed = ((Date.now() - start) / 1e3).toFixed(1);
664
748
  check(waitResult.answered === true, "Answer received", waitResult.answered ? `"${waitResult.value}" (${elapsed}s roundtrip)` : "no response within timeout");
749
+ if (waitResult.answered !== true) {
750
+ await callMcpTool(apiKey, "cancel_question", { correlationId: askResult.correlationId }, {
751
+ id: 5,
752
+ sessionId,
753
+ timeoutMs: 1e4
754
+ }).catch(() => {
755
+ });
756
+ }
665
757
  } else {
666
758
  check(false, "Question sent", "failed to get correlationId");
667
759
  }
@@ -710,7 +802,7 @@ var main = async () => {
710
802
  }
711
803
  console.log();
712
804
  const keyExit = keyVerdict && (keyVerdict.kind === "rejected" || keyVerdict.kind === "locked" || keyVerdict.kind === "malformed") ? describeKeyCheck(keyVerdict).exitCode : null;
713
- const exitCode = keyExit ?? (failed.length > 0 ? EXIT.PROBLEMS_FOUND : EXIT.OK);
805
+ const exitCode = resolveDoctorExit({ keyExit, failures: failed });
714
806
  if (options.bundle) {
715
807
  try {
716
808
  const bundle = redact(
@@ -1,8 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
+ KILL_REASON,
3
4
  denyReasonFrom,
4
- isDeferAnswer
5
- } from "../chunk-YHG74UFF.js";
5
+ isDeferAnswer,
6
+ resolveGate
7
+ } from "../chunk-GD275GEX.js";
6
8
  import {
7
9
  DEFAULT_SESSION,
8
10
  askUser,
@@ -18,12 +20,13 @@ import {
18
20
  handleUserPrompt,
19
21
  readLastPrompt,
20
22
  readLastUserPrompt,
23
+ repoKeyFor,
21
24
  reportEvent,
22
- resolvePolicy,
23
25
  savePendingQuestion,
26
+ scopePathFor,
24
27
  sendNotification,
25
28
  waitForAnswer
26
- } from "../chunk-XOUYM27W.js";
29
+ } from "../chunk-FZIQSDTQ.js";
27
30
  import {
28
31
  isGatingMoment,
29
32
  recordKeylessMoment
@@ -49,6 +52,7 @@ import "../chunk-KZERVKTD.js";
49
52
  import { basename } from "path";
50
53
 
51
54
  // src/gemini-adapter.ts
55
+ import { isAbsolute, resolve } from "path";
52
56
  var GEMINI_AGENT = { type: "gemini_cli", label: "Gemini CLI" };
53
57
  var GEMINI_TOOL_MAP = {
54
58
  run_shell_command: "Bash",
@@ -57,7 +61,7 @@ var GEMINI_TOOL_MAP = {
57
61
  read_file: "Read"
58
62
  };
59
63
  var firstString = (...values) => values.find((v) => typeof v === "string" && v.length > 0);
60
- var toGeminiPolicyLookup = (toolName, toolInput) => {
64
+ var toGeminiPolicyLookup = (toolName, toolInput, cwd) => {
61
65
  const canonical = GEMINI_TOOL_MAP[toolName];
62
66
  if (!canonical) return { tool: toolName, input: toolInput };
63
67
  if (canonical === "Bash") {
@@ -65,7 +69,11 @@ var toGeminiPolicyLookup = (toolName, toolInput) => {
65
69
  return { tool: "Bash", input: command ? { command } : {} };
66
70
  }
67
71
  const filePath = firstString(toolInput.file_path, toolInput.absolute_path, toolInput.path);
68
- return { tool: canonical, input: filePath ? { file_path: filePath } : {} };
72
+ if (!filePath) return { tool: canonical, input: {} };
73
+ return {
74
+ tool: canonical,
75
+ input: { file_path: cwd && !isAbsolute(filePath) ? resolve(cwd, filePath) : filePath }
76
+ };
69
77
  };
70
78
  var geminiAllow = () => ({ kind: "allow" });
71
79
  var geminiDeny = (reason) => ({ kind: "deny", reason });
@@ -82,9 +90,8 @@ var geminiTimeoutDecision = (timeoutAction, denyReason = "No response within tim
82
90
  };
83
91
 
84
92
  // bin/pushary-gemini-hook.ts
85
- var KILL_REASON = "Stopped by user: this agent was halted from Pushary";
86
93
  var START_MS = Date.now();
87
- var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
94
+ var sleep = (ms) => new Promise((resolve2) => setTimeout(resolve2, ms));
88
95
  var pollForAnswer = async (apiKey, correlationId, deadlineMs, pollInterval = 2e3) => {
89
96
  while (Date.now() < deadlineMs) {
90
97
  const remaining = Math.min(Math.max(deadlineMs - Date.now(), 1e3), 3e4);
@@ -210,14 +217,23 @@ var decideBeforeTool = async (input) => {
210
217
  }
211
218
  try {
212
219
  const modeState = await fetchModeState(apiKey, input.session_id);
213
- if (modeState.kill) return geminiDeny(KILL_REASON);
214
- const lookup = toGeminiPolicyLookup(input.tool_name ?? "", input.tool_input ?? {});
215
- const policy = await getPolicy(apiKey, modeState.policyVersion, "gemini_cli");
216
- const toolPolicy = resolvePolicy(policy, lookup.tool, modeState.mode, lookup.input);
217
- if (toolPolicy.timeoutSeconds === 0 && toolPolicy.timeoutAction === "approve") return geminiAllow();
218
- if (toolPolicy.timeoutSeconds === 0 && toolPolicy.timeoutAction === "deny") {
219
- return geminiDeny(`Denied by policy for ${toolPolicy.tool}`);
220
- }
220
+ const lookup = toGeminiPolicyLookup(input.tool_name ?? "", input.tool_input ?? {}, input.cwd);
221
+ const config = await getPolicy(apiKey, modeState.policyVersion, "gemini_cli");
222
+ const scopePath = modeState.scope ? scopePathFor(lookup.tool, lookup.input, input.cwd) : void 0;
223
+ const verdict = resolveGate({
224
+ modeState,
225
+ config,
226
+ toolName: lookup.tool,
227
+ toolInputs: [lookup.input],
228
+ scopePaths: scopePath ? [scopePath] : [],
229
+ cwd: input.cwd,
230
+ repoKey: repoKeyFor(input.cwd)
231
+ });
232
+ if (verdict.kind === "kill") return geminiDeny(KILL_REASON);
233
+ if (verdict.kind === "allow") return geminiAllow();
234
+ if (verdict.kind === "deny") return geminiDeny(verdict.reason);
235
+ if (verdict.kind === "defer") return geminiPass();
236
+ const toolPolicy = verdict.policy;
221
237
  switch (toolPolicy.mode) {
222
238
  case "push_only":
223
239
  return handlePushOnly(apiKey, input, lookup, toolPolicy.timeoutSeconds, toolPolicy.timeoutAction);
@@ -1,14 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  handlePreToolUse
4
- } from "../chunk-SONXGCZY.js";
4
+ } from "../chunk-HU43EZ5Q.js";
5
5
  import "../chunk-CAJZAFVS.js";
6
6
  import {
7
7
  exitOnHelpFlag
8
- } from "../chunk-ATP6UQPH.js";
9
- import "../chunk-JGH37QRB.js";
10
- import "../chunk-YHG74UFF.js";
11
- import "../chunk-XOUYM27W.js";
8
+ } from "../chunk-BBK3TTU2.js";
9
+ import "../chunk-7OYGFJYZ.js";
10
+ import "../chunk-GD275GEX.js";
11
+ import "../chunk-FZIQSDTQ.js";
12
12
  import "../chunk-WLGEY4UX.js";
13
13
  import "../chunk-BSZYIAZL.js";
14
14
  import "../chunk-SAF6HGAA.js";
@@ -21,10 +21,10 @@ import {
21
21
  } from "../chunk-GMXKITVA.js";
22
22
  import {
23
23
  exitOnHelpFlag
24
- } from "../chunk-ATP6UQPH.js";
24
+ } from "../chunk-BBK3TTU2.js";
25
25
  import {
26
26
  getPackageVersion
27
- } from "../chunk-JGH37QRB.js";
27
+ } from "../chunk-7OYGFJYZ.js";
28
28
  import "../chunk-SAF6HGAA.js";
29
29
  import {
30
30
  UsageError
@@ -19,8 +19,8 @@ import {
19
19
  } from "../chunk-GMXKITVA.js";
20
20
  import {
21
21
  exitOnHelpFlag
22
- } from "../chunk-ATP6UQPH.js";
23
- import "../chunk-JGH37QRB.js";
22
+ } from "../chunk-BBK3TTU2.js";
23
+ import "../chunk-7OYGFJYZ.js";
24
24
  import {
25
25
  UsageError
26
26
  } from "../chunk-7QLSKOSU.js";
@@ -11,8 +11,8 @@ import {
11
11
  } from "../chunk-GMXKITVA.js";
12
12
  import {
13
13
  exitOnHelpFlag
14
- } from "../chunk-ATP6UQPH.js";
15
- import "../chunk-JGH37QRB.js";
14
+ } from "../chunk-BBK3TTU2.js";
15
+ import "../chunk-7OYGFJYZ.js";
16
16
  import "../chunk-SAF6HGAA.js";
17
17
  import {
18
18
  UsageError
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  handleNotification
4
- } from "../chunk-XOUYM27W.js";
4
+ } from "../chunk-FZIQSDTQ.js";
5
5
  import "../chunk-BSZYIAZL.js";
6
6
  import "../chunk-SAF6HGAA.js";
7
7
  import "../chunk-7QLSKOSU.js";
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  handlePermissionDenied
4
- } from "../chunk-SONXGCZY.js";
4
+ } from "../chunk-HU43EZ5Q.js";
5
5
  import "../chunk-CAJZAFVS.js";
6
- import "../chunk-YHG74UFF.js";
7
- import "../chunk-XOUYM27W.js";
6
+ import "../chunk-GD275GEX.js";
7
+ import "../chunk-FZIQSDTQ.js";
8
8
  import "../chunk-WLGEY4UX.js";
9
9
  import "../chunk-BSZYIAZL.js";
10
10
  import "../chunk-SAF6HGAA.js";