pushary 1.8.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 (186) hide show
  1. package/CHANGELOG.md +1943 -0
  2. package/LICENSE +21 -0
  3. package/README.md +330 -0
  4. package/data/SKILL.md +591 -0
  5. package/data/cowork/SKILL.md +77 -0
  6. package/data/cursor-plugin/.cursor-plugin/plugin.json +25 -0
  7. package/data/cursor-plugin/CHANGELOG.md +70 -0
  8. package/data/cursor-plugin/CONTRIBUTING.md +40 -0
  9. package/data/cursor-plugin/LICENSE +21 -0
  10. package/data/cursor-plugin/README.md +113 -0
  11. package/data/cursor-plugin/SECURITY.md +33 -0
  12. package/data/cursor-plugin/assets/logo.png +0 -0
  13. package/data/cursor-plugin/commands/notify-when-done.md +14 -0
  14. package/data/cursor-plugin/commands/pushary-test.md +13 -0
  15. package/data/cursor-plugin/hooks/hooks.json +75 -0
  16. package/data/cursor-plugin/mcp.json +11 -0
  17. package/data/cursor-plugin/rules/pushary.mdc +40 -0
  18. package/data/cursor-plugin/scripts/pushary-gate.mjs +724 -0
  19. package/data/cursor-plugin/scripts/pushary-gate.test.mjs +137 -0
  20. package/data/cursor-plugin/scripts/redaction.mjs +51 -0
  21. package/data/cursor-plugin/skills/pushary/SKILL.md +584 -0
  22. package/data/vscode-plugin/.claude-plugin/plugin.json +28 -0
  23. package/data/vscode-plugin/.mcp.json +11 -0
  24. package/data/vscode-plugin/CHANGELOG.md +49 -0
  25. package/data/vscode-plugin/CONTRIBUTING.md +40 -0
  26. package/data/vscode-plugin/LICENSE +21 -0
  27. package/data/vscode-plugin/README.md +128 -0
  28. package/data/vscode-plugin/SECURITY.md +56 -0
  29. package/data/vscode-plugin/assets/logo.png +0 -0
  30. package/data/vscode-plugin/commands/notify-when-done.md +14 -0
  31. package/data/vscode-plugin/commands/pushary-test.md +13 -0
  32. package/data/vscode-plugin/hooks/hooks.json +60 -0
  33. package/data/vscode-plugin/scripts/pushary-gate.mjs +762 -0
  34. package/data/vscode-plugin/scripts/redaction.mjs +51 -0
  35. package/data/vscode-plugin/skills/pushary/SKILL.md +584 -0
  36. package/dist/bin/pushary-bell-hook.d.ts +1 -0
  37. package/dist/bin/pushary-bell-hook.js +46 -0
  38. package/dist/bin/pushary-bell.d.ts +1 -0
  39. package/dist/bin/pushary-bell.js +146 -0
  40. package/dist/bin/pushary-claude.d.ts +1 -0
  41. package/dist/bin/pushary-claude.js +1916 -0
  42. package/dist/bin/pushary-clean.d.ts +1 -0
  43. package/dist/bin/pushary-clean.js +1300 -0
  44. package/dist/bin/pushary-codex-bridge.d.ts +1 -0
  45. package/dist/bin/pushary-codex-bridge.js +599 -0
  46. package/dist/bin/pushary-codex-hook.d.ts +1 -0
  47. package/dist/bin/pushary-codex-hook.js +886 -0
  48. package/dist/bin/pushary-codex.d.ts +1 -0
  49. package/dist/bin/pushary-codex.js +125 -0
  50. package/dist/bin/pushary-connect.d.ts +1 -0
  51. package/dist/bin/pushary-connect.js +100 -0
  52. package/dist/bin/pushary-cowork.d.ts +1 -0
  53. package/dist/bin/pushary-cowork.js +36 -0
  54. package/dist/bin/pushary-daemon-supervisor.d.ts +1 -0
  55. package/dist/bin/pushary-daemon-supervisor.js +27 -0
  56. package/dist/bin/pushary-daemon.d.ts +1 -0
  57. package/dist/bin/pushary-daemon.js +878 -0
  58. package/dist/bin/pushary-disconnect.d.ts +1 -0
  59. package/dist/bin/pushary-disconnect.js +264 -0
  60. package/dist/bin/pushary-doctor.d.ts +1 -0
  61. package/dist/bin/pushary-doctor.js +1433 -0
  62. package/dist/bin/pushary-elicitation-hook.d.ts +1 -0
  63. package/dist/bin/pushary-elicitation-hook.js +149 -0
  64. package/dist/bin/pushary-gemini-bridge.d.ts +1 -0
  65. package/dist/bin/pushary-gemini-bridge.js +339 -0
  66. package/dist/bin/pushary-gemini-hook.d.ts +1 -0
  67. package/dist/bin/pushary-gemini-hook.js +381 -0
  68. package/dist/bin/pushary-hook.d.ts +1 -0
  69. package/dist/bin/pushary-hook.js +95 -0
  70. package/dist/bin/pushary-login.d.ts +1 -0
  71. package/dist/bin/pushary-login.js +198 -0
  72. package/dist/bin/pushary-logout.d.ts +1 -0
  73. package/dist/bin/pushary-logout.js +147 -0
  74. package/dist/bin/pushary-mcp.d.ts +1 -0
  75. package/dist/bin/pushary-mcp.js +114 -0
  76. package/dist/bin/pushary-mode.d.ts +1 -0
  77. package/dist/bin/pushary-mode.js +130 -0
  78. package/dist/bin/pushary-notification-hook.d.ts +1 -0
  79. package/dist/bin/pushary-notification-hook.js +56 -0
  80. package/dist/bin/pushary-opencode-hook.d.ts +1 -0
  81. package/dist/bin/pushary-opencode-hook.js +338 -0
  82. package/dist/bin/pushary-permission-denied-hook.d.ts +1 -0
  83. package/dist/bin/pushary-permission-denied-hook.js +60 -0
  84. package/dist/bin/pushary-permission-hook.d.ts +1 -0
  85. package/dist/bin/pushary-permission-hook.js +65 -0
  86. package/dist/bin/pushary-post-hook.d.ts +1 -0
  87. package/dist/bin/pushary-post-hook.js +70 -0
  88. package/dist/bin/pushary-prompt-hook.d.ts +1 -0
  89. package/dist/bin/pushary-prompt-hook.js +61 -0
  90. package/dist/bin/pushary-session-end-hook.d.ts +1 -0
  91. package/dist/bin/pushary-session-end-hook.js +59 -0
  92. package/dist/bin/pushary-session-start-hook.d.ts +1 -0
  93. package/dist/bin/pushary-session-start-hook.js +72 -0
  94. package/dist/bin/pushary-setup.d.ts +1 -0
  95. package/dist/bin/pushary-setup.js +2660 -0
  96. package/dist/bin/pushary-stats.d.ts +1 -0
  97. package/dist/bin/pushary-stats.js +44 -0
  98. package/dist/bin/pushary-status.d.ts +1 -0
  99. package/dist/bin/pushary-status.js +262 -0
  100. package/dist/bin/pushary-stop-hook.d.ts +1 -0
  101. package/dist/bin/pushary-stop-hook.js +63 -0
  102. package/dist/bin/pushary-stopfailure-hook.d.ts +1 -0
  103. package/dist/bin/pushary-stopfailure-hook.js +56 -0
  104. package/dist/bin/pushary-suggestions.d.ts +1 -0
  105. package/dist/bin/pushary-suggestions.js +100 -0
  106. package/dist/bin/pushary-transcript-register.d.ts +1 -0
  107. package/dist/bin/pushary-transcript-register.js +40 -0
  108. package/dist/bin/pushary-transcripts.d.ts +1 -0
  109. package/dist/bin/pushary-transcripts.js +77 -0
  110. package/dist/bin/pushary-upgrade.d.ts +1 -0
  111. package/dist/bin/pushary-upgrade.js +599 -0
  112. package/dist/bin/pushary-wait.d.ts +1 -0
  113. package/dist/bin/pushary-wait.js +122 -0
  114. package/dist/bin/pushary.d.ts +1 -0
  115. package/dist/bin/pushary.js +74 -0
  116. package/dist/chunk-2ABTSGFT.js +173 -0
  117. package/dist/chunk-2OY3ZZ5N.js +124 -0
  118. package/dist/chunk-3EGEA4KH.js +44 -0
  119. package/dist/chunk-3EVPNIBE.js +185 -0
  120. package/dist/chunk-5OP5MQI7.js +45 -0
  121. package/dist/chunk-6J2JC2RT.js +149 -0
  122. package/dist/chunk-7QLSKOSU.js +19 -0
  123. package/dist/chunk-7Z7Q4GGS.js +88 -0
  124. package/dist/chunk-A2A2YTMG.js +835 -0
  125. package/dist/chunk-AHV4LNB5.js +377 -0
  126. package/dist/chunk-AXPCESYO.js +197 -0
  127. package/dist/chunk-C2WCPBF7.js +86 -0
  128. package/dist/chunk-CJVVKOSR.js +73 -0
  129. package/dist/chunk-CULUZTWQ.js +2117 -0
  130. package/dist/chunk-CVQYGMSR.js +142 -0
  131. package/dist/chunk-CVWG2N3Y.js +16 -0
  132. package/dist/chunk-D2FX3EPW.js +15 -0
  133. package/dist/chunk-DB3ONZB4.js +44 -0
  134. package/dist/chunk-DE4Y6TCC.js +160 -0
  135. package/dist/chunk-E4FJTUH4.js +249 -0
  136. package/dist/chunk-EPCXUP3P.js +41 -0
  137. package/dist/chunk-EX3GAPA4.js +44 -0
  138. package/dist/chunk-EYK3GSRH.js +143 -0
  139. package/dist/chunk-GMXKITVA.js +70 -0
  140. package/dist/chunk-GPEGOGRJ.js +49 -0
  141. package/dist/chunk-GU3EJCVV.js +43 -0
  142. package/dist/chunk-HQ6G3B5R.js +59 -0
  143. package/dist/chunk-HZVVNGEW.js +22 -0
  144. package/dist/chunk-IPVKH2ST.js +231 -0
  145. package/dist/chunk-J5TIV3J6.js +238 -0
  146. package/dist/chunk-JU6W3XAU.js +64 -0
  147. package/dist/chunk-JYG4B33G.js +227 -0
  148. package/dist/chunk-JZTHGAPA.js +503 -0
  149. package/dist/chunk-KGDWDSNL.js +102 -0
  150. package/dist/chunk-KVQBWI5T.js +99 -0
  151. package/dist/chunk-KZERVKTD.js +16 -0
  152. package/dist/chunk-LXQ5FBBD.js +47 -0
  153. package/dist/chunk-M5ICYGHT.js +138 -0
  154. package/dist/chunk-M5JDZ45W.js +943 -0
  155. package/dist/chunk-MDEPU45E.js +1226 -0
  156. package/dist/chunk-MPWXGONV.js +157 -0
  157. package/dist/chunk-N6IOSZOE.js +185 -0
  158. package/dist/chunk-NGDD6TXY.js +192 -0
  159. package/dist/chunk-NQTHFF4W.js +2830 -0
  160. package/dist/chunk-OKXS7WDZ.js +68 -0
  161. package/dist/chunk-OLQRRFBX.js +21 -0
  162. package/dist/chunk-P7WQEVWA.js +307 -0
  163. package/dist/chunk-POFBE6ZY.js +12 -0
  164. package/dist/chunk-PQ4K74WL.js +30 -0
  165. package/dist/chunk-PUZZELE6.js +37 -0
  166. package/dist/chunk-Q7E4I6UA.js +767 -0
  167. package/dist/chunk-QCGNGEGE.js +15 -0
  168. package/dist/chunk-R7437YI3.js +151 -0
  169. package/dist/chunk-RHR6NARM.js +305 -0
  170. package/dist/chunk-SAXBC4AZ.js +60 -0
  171. package/dist/chunk-SGZJ5LYH.js +76 -0
  172. package/dist/chunk-SIFVFP6T.js +70 -0
  173. package/dist/chunk-SLFABHW3.js +150 -0
  174. package/dist/chunk-T7E75EKA.js +67 -0
  175. package/dist/chunk-TY4JT7W3.js +38 -0
  176. package/dist/chunk-U4TMAANR.js +111 -0
  177. package/dist/chunk-UI36QSBI.js +177 -0
  178. package/dist/chunk-VQKRG3AC.js +339 -0
  179. package/dist/chunk-WE2J62NC.js +1131 -0
  180. package/dist/chunk-XHKBHWLX.js +14 -0
  181. package/dist/chunk-ZSZSSJCV.js +275 -0
  182. package/dist/chunk-ZVSKMPBZ.js +135 -0
  183. package/dist/reapply-CW3MA66V.js +34 -0
  184. package/dist/src/index.d.ts +204 -0
  185. package/dist/src/index.js +64 -0
  186. package/package.json +108 -0
@@ -0,0 +1,1433 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ describeWaitLadder
4
+ } from "../chunk-CVWG2N3Y.js";
5
+ import {
6
+ readServiceExecPath,
7
+ versionManagerOf
8
+ } from "../chunk-GU3EJCVV.js";
9
+ import {
10
+ assessCodexHookTrust,
11
+ readCodexTrustForPusharyHooks
12
+ } from "../chunk-RHR6NARM.js";
13
+ import {
14
+ readMachineReadiness,
15
+ readManagedControlEvidence
16
+ } from "../chunk-DE4Y6TCC.js";
17
+ import {
18
+ FX_PROFILE_REFUSALS,
19
+ FX_PUSHARY_TOOLS_RULE,
20
+ FX_QUESTION_TOOL,
21
+ HERMES_PIP_PACKAGE,
22
+ HERMES_PLUGIN_NAME,
23
+ agentProbes,
24
+ detectAgent,
25
+ detectAllAgents,
26
+ hasInstructionBlock,
27
+ hermesWiredByPushary,
28
+ inspectFxInstallation,
29
+ isDetected,
30
+ isPluginRegistered,
31
+ readHermesStatus,
32
+ resolveHermesPython,
33
+ retryAgentCommand,
34
+ shortenHome,
35
+ vscodeSettingsTargets
36
+ } from "../chunk-MDEPU45E.js";
37
+ import "../chunk-SLFABHW3.js";
38
+ import {
39
+ OPENCODE_PLUGIN_PLACEHOLDER,
40
+ openCodePluginDir,
41
+ openCodePluginFile,
42
+ openCodeWired
43
+ } from "../chunk-AHV4LNB5.js";
44
+ import {
45
+ compatibleConfigFile,
46
+ compatibleInstructionsFile,
47
+ compatibleMcpFile,
48
+ hasCompatibleHooks,
49
+ missingCompatibleHookEvents
50
+ } from "../chunk-J5TIV3J6.js";
51
+ import "../chunk-AXPCESYO.js";
52
+ import {
53
+ detectProviderLiveCapabilities,
54
+ hasSpawnProvider,
55
+ resolveControlPolicy
56
+ } from "../chunk-HQ6G3B5R.js";
57
+ import {
58
+ AUTO_UPDATE_DISABLE_ENV,
59
+ autoUpdateOffReason,
60
+ describeAutoUpdateAnnouncement,
61
+ describeAutoUpdateCheck,
62
+ readAutoUpdateState,
63
+ takeAutoUpdateAnnouncement
64
+ } from "../chunk-UI36QSBI.js";
65
+ import {
66
+ readLedgerSummary
67
+ } from "../chunk-JU6W3XAU.js";
68
+ import {
69
+ describeBridgeResolution,
70
+ isBridgeLocator,
71
+ liveBridgeProbe,
72
+ resolveBridgeStub
73
+ } from "../chunk-C2WCPBF7.js";
74
+ import {
75
+ claudeOtherHolders,
76
+ claudeUnprovenHolds,
77
+ claudeWired,
78
+ codexOtherHolders,
79
+ codexUnprovenHolds,
80
+ codexWired,
81
+ cursorMcpAuthorization,
82
+ cursorOtherHolders,
83
+ cursorWired,
84
+ describeHoldSentence,
85
+ describeOtherHolder,
86
+ geminiOtherHolders,
87
+ geminiWired,
88
+ holdsLong,
89
+ pusharyHookExecutable
90
+ } from "../chunk-6J2JC2RT.js";
91
+ import {
92
+ daemonHasRemoteProof,
93
+ daemonServiceManagerAvailable,
94
+ describeDaemonLoginBlock,
95
+ hasDaemonServiceDefinition,
96
+ readDaemonNeedsLogin,
97
+ readDaemonStatus,
98
+ readInstalledDaemonService
99
+ } from "../chunk-WE2J62NC.js";
100
+ import {
101
+ GEMINI_HOOK_BINARY,
102
+ hasGeminiHooks,
103
+ missingGeminiHookEvents
104
+ } from "../chunk-3EVPNIBE.js";
105
+ import "../chunk-XHKBHWLX.js";
106
+ import "../chunk-CULUZTWQ.js";
107
+ import "../chunk-N6IOSZOE.js";
108
+ import "../chunk-EPCXUP3P.js";
109
+ import "../chunk-NGDD6TXY.js";
110
+ import "../chunk-CVQYGMSR.js";
111
+ import "../chunk-POFBE6ZY.js";
112
+ import {
113
+ describeReach,
114
+ describeRoundTrip,
115
+ fetchChannels,
116
+ reachVerdict,
117
+ roundTripEvidence,
118
+ verifyRoundTrip
119
+ } from "../chunk-M5JDZ45W.js";
120
+ import "../chunk-3EGEA4KH.js";
121
+ import "../chunk-QCGNGEGE.js";
122
+ import {
123
+ createIo
124
+ } from "../chunk-U4TMAANR.js";
125
+ import {
126
+ exitOnHelpFlag
127
+ } from "../chunk-HZVVNGEW.js";
128
+ import {
129
+ parseFlags
130
+ } from "../chunk-GMXKITVA.js";
131
+ import {
132
+ keyPrefix,
133
+ readControlMode,
134
+ readKeySource
135
+ } from "../chunk-P7WQEVWA.js";
136
+ import {
137
+ callMcpTool,
138
+ sendMcpRequest
139
+ } from "../chunk-M5ICYGHT.js";
140
+ import {
141
+ checkApiKey,
142
+ describeKeyCheck
143
+ } from "../chunk-2ABTSGFT.js";
144
+ import "../chunk-2OY3ZZ5N.js";
145
+ import {
146
+ getMachineId
147
+ } from "../chunk-LXQ5FBBD.js";
148
+ import {
149
+ UsageError
150
+ } from "../chunk-7QLSKOSU.js";
151
+ import {
152
+ EXIT
153
+ } from "../chunk-KZERVKTD.js";
154
+ import "../chunk-DB3ONZB4.js";
155
+ import {
156
+ CODEX_HOOK_EVENTS,
157
+ autoUpdateState,
158
+ claudeJson,
159
+ claudeSettings,
160
+ claudeSkillDir,
161
+ codexAgentsMd,
162
+ codexConfigToml,
163
+ codexHookPositions,
164
+ codexHooksJson,
165
+ codexSkillDir,
166
+ codexTrustGaps,
167
+ cursorPluginDir,
168
+ cursorUserHooks,
169
+ cursorUserMcp,
170
+ fetchLatestPublishedVersion,
171
+ fxMcpProfile,
172
+ fxSettings,
173
+ geminiMd,
174
+ geminiSettings,
175
+ hasCodexHooks,
176
+ hermesConfig,
177
+ hermesConfigReceipt,
178
+ missingCodexHookEvents,
179
+ pusharyConfigFile,
180
+ readCodexMcpAuth,
181
+ readGlobalCliInstall,
182
+ resolvePathBinary,
183
+ vscodePluginDir
184
+ } from "../chunk-A2A2YTMG.js";
185
+ import "../chunk-T7E75EKA.js";
186
+ import {
187
+ getBaseUrl
188
+ } from "../chunk-GPEGOGRJ.js";
189
+ import {
190
+ NPX_LATEST_COMMAND
191
+ } from "../chunk-E4FJTUH4.js";
192
+ import {
193
+ AGENTS,
194
+ CURSOR_GATE_EVENTS,
195
+ HOOK_COMPATIBLE_PROFILES,
196
+ HOOK_LOCATOR_BINARY,
197
+ HOOK_SOURCES,
198
+ SUPPORT_DISCORD,
199
+ SUPPORT_EMAIL,
200
+ commandsPackage,
201
+ getPackageVersion,
202
+ hookActivationResult,
203
+ hookCompatibleProfile,
204
+ isHookActivationReport,
205
+ isNewerVersion
206
+ } from "../chunk-NQTHFF4W.js";
207
+
208
+ // bin/pushary-doctor.ts
209
+ import { existsSync, readFileSync, writeFileSync } from "fs";
210
+ import { join, resolve } from "path";
211
+ import { homedir as homedir2 } from "os";
212
+ import { execFileSync } from "child_process";
213
+ import { confirm } from "@inquirer/prompts";
214
+ import { parse as parseTOML } from "smol-toml";
215
+
216
+ // src/diagnostics/global-package.ts
217
+ var NOT_FOUND = "not found";
218
+ var NPM_PREFIX_UNREADABLE = "not found (npm could not report its global prefix)";
219
+ var BOTH_INSTALLED_WARNING = "npm uninstall -g of either name removes every Pushary command";
220
+ var describeInstalledPackage = (installed2) => installed2.version === void 0 ? `${installed2.name}, version unreadable` : `${installed2.name} ${installed2.version}`;
221
+ var describeCommandOwner = (owner) => owner.kind === "owned" ? `the commands run from ${owner.name}` : "which one owns the commands could not be read";
222
+ var globalPackageCheck = (install) => {
223
+ switch (install.kind) {
224
+ case "unavailable":
225
+ return { ok: false, detail: NPM_PREFIX_UNREADABLE, commands: void 0 };
226
+ case "absent":
227
+ return { ok: false, detail: NOT_FOUND, commands: void 0 };
228
+ case "single":
229
+ return {
230
+ ok: install.installed.version !== void 0,
231
+ detail: describeInstalledPackage(install.installed),
232
+ commands: install.installed
233
+ };
234
+ case "both":
235
+ return {
236
+ ok: false,
237
+ detail: `both installed: ${install.installed.map(describeInstalledPackage).join(" and ")}; ${describeCommandOwner(install.owner)}; ${BOTH_INSTALLED_WARNING}`,
238
+ commands: commandsPackage(install)
239
+ };
240
+ }
241
+ };
242
+
243
+ // src/diagnostics/auto-update.ts
244
+ var globalPackageFreshness = (installed2, latest) => isNewerVersion(latest, installed2) ? { upToDate: false, detail: `${installed2} installed, ${latest} available \u2014 run ${NPX_LATEST_COMMAND} upgrade` } : { upToDate: true, detail: installed2 };
245
+ var AUTO_UPDATE_ON = "on: once a day at session start, within the same major version";
246
+ var AUTO_UPDATE_OFF = {
247
+ disabled_by_env: `off: ${AUTO_UPDATE_DISABLE_ENV} is set`,
248
+ disabled_by_user: "off: turn it back on with pushary upgrade --auto=on",
249
+ ci: "off in CI",
250
+ windows: "not available on Windows yet",
251
+ linux: "paused on Linux to protect running sessions; upgrade manually after sessions finish"
252
+ };
253
+ var needsAttention = (input) => input.state.lastResult === "failed" || input.state.lastResult === "needs_manual" || Boolean(input.state.detail);
254
+ var LAST_CHECK_LABEL = "Last automatic update check";
255
+ var describeAutoUpdateStatus = (input) => {
256
+ const offReason = autoUpdateOffReason(input);
257
+ const status = {
258
+ tone: "info",
259
+ label: "Automatic updates",
260
+ detail: offReason === null ? AUTO_UPDATE_ON : AUTO_UPDATE_OFF[offReason]
261
+ };
262
+ const checkedAt = input.state.at ?? input.state.lastCheckAt;
263
+ if (checkedAt === void 0) return [status];
264
+ return [
265
+ status,
266
+ {
267
+ tone: needsAttention(input) ? "warn" : "info",
268
+ label: LAST_CHECK_LABEL,
269
+ detail: `${checkedAt}: ${describeAutoUpdateCheck(input.state)}`
270
+ }
271
+ ];
272
+ };
273
+
274
+ // src/diagnostics/bundle.ts
275
+ import { homedir, hostname } from "os";
276
+ var installedGlobalPackages = (install) => {
277
+ if (install.kind === "single") return [install.installed];
278
+ if (install.kind === "both") return install.installed;
279
+ return [];
280
+ };
281
+ var collectBundle = async (options2 = {}) => {
282
+ const now = options2.now ?? (() => /* @__PURE__ */ new Date());
283
+ const globalInstall = (options2.readGlobalInstall ?? readGlobalCliInstall)();
284
+ const source = readKeySource();
285
+ const identity = options2.identity ?? null;
286
+ return {
287
+ generatedAt: now().toISOString(),
288
+ cliVersion: getPackageVersion(),
289
+ globalVersion: commandsPackage(globalInstall)?.version ?? null,
290
+ globalPackages: installedGlobalPackages(globalInstall),
291
+ node: process.version,
292
+ platform: process.platform,
293
+ arch: process.arch,
294
+ shell: process.env.SHELL ?? null,
295
+ machineId: getMachineId(),
296
+ keySource: source.kind,
297
+ keyPrefix: source.key ? keyPrefix(source.key) : null,
298
+ // The value of the drift, not the keys: knowing two places disagree is the
299
+ // whole diagnostic, and the keys themselves are what must not travel.
300
+ keyDrift: source.drift !== null,
301
+ identity: identity ? stripWorkspace(identity) : null,
302
+ agents: detectAllAgents().map((entry) => ({ id: entry.agent, detected: entry.result })),
303
+ checks: options2.checks ?? []
304
+ };
305
+ };
306
+ var stripWorkspace = (identity) => {
307
+ const { workspaceId: _workspaceId, ...rest } = identity;
308
+ return rest;
309
+ };
310
+ var KEY_PATTERN = /pk_[A-Za-z0-9_]+\.[A-Za-z0-9_]+/g;
311
+ var homePattern = () => new RegExp(homedir().replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "g");
312
+ var redactText = (text) => text.replace(KEY_PATTERN, (match) => `${match.split(".")[0]}.REDACTED`).replace(homePattern(), "~").replace(new RegExp(hostname().replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "g"), "HOST");
313
+ var redact = (bundle) => JSON.parse(redactText(JSON.stringify(bundle)));
314
+ var bundleFileName = (bundle) => `pushary-diagnostics-${bundle.generatedAt.replace(/[:.]/g, "-")}.json`;
315
+
316
+ // src/diagnostics/exit-code.ts
317
+ var resolveDoctorExit = ({ keyExit, failures }) => {
318
+ if (keyExit !== null) return keyExit;
319
+ if (failures.length === 0) return EXIT.OK;
320
+ const every = (kind) => failures.every((f) => f.kind === kind);
321
+ if (every("unreachable")) return EXIT.UNREACHABLE;
322
+ if (every("reachability")) return EXIT.NO_DEVICE;
323
+ return EXIT.PROBLEMS_FOUND;
324
+ };
325
+
326
+ // src/diagnostics/config-rewriters.ts
327
+ var CONFIG_REWRITERS = Object.freeze([
328
+ {
329
+ id: "cc-switch",
330
+ label: "cc-switch",
331
+ markers: [[".cc-switch", "cc-switch.db"], [".cc-switch", "config.json"]],
332
+ rewrites: ["Claude Code"],
333
+ what: "replaces ~/.claude/settings.json with what that provider stored plus your common config, rather than merging into the file on disk"
334
+ }
335
+ ]);
336
+ var rewriterFindings = ({ present, agentState }, rewriters = CONFIG_REWRITERS) => rewriters.filter(present).flatMap((rewriter) => rewriter.rewrites.flatMap((agent) => {
337
+ const state = agentState(agent);
338
+ if (!state?.wired) return [];
339
+ return [{ rewriter, agent, state: state.hooksIntact ? "armed" : "suspect" }];
340
+ }));
341
+ var describeRewriterFinding = ({ rewriter, agent, state }) => state === "armed" ? `${rewriter.label} is installed, and a provider switch can take ${agent}'s Pushary hooks out of settings.json` : `${agent}'s Pushary hooks are gone from settings.json, and ${rewriter.label} is one way that happens`;
342
+ var rewriterMechanism = ({ rewriter }) => `${rewriter.label} ${rewriter.what}.`;
343
+ var rewriterRemedy = ({ state }) => state === "armed" ? 'Tick "Apply Common Config" on the provider so a switch keeps them, or re-run setup after each switch' : 'Re-run setup to put them back, then tick "Apply Common Config" on the provider so a switch keeps them';
344
+
345
+ // src/diagnostics/inert-registrations.ts
346
+ var REGISTRATION_FLAGS = ["agent", "source"];
347
+ var FLAG_PATTERNS = {
348
+ agent: /--agent(?:=|\s+)([A-Za-z0-9_-]*)/g,
349
+ source: /--source(?:=|\s+)([A-Za-z0-9_-]*)/g
350
+ };
351
+ function collectHookCommands(value) {
352
+ if (value === null || typeof value !== "object") return [];
353
+ const commands = [];
354
+ for (const [key, nested] of Object.entries(value)) {
355
+ if (key === "command" && typeof nested === "string") commands.push(nested);
356
+ else commands.push(...collectHookCommands(nested));
357
+ }
358
+ return commands;
359
+ }
360
+ function readHookCommands(contents) {
361
+ try {
362
+ return collectHookCommands(JSON.parse(contents));
363
+ } catch {
364
+ return contents.split("\n");
365
+ }
366
+ }
367
+ function registeredNames(contents, flag) {
368
+ const names = /* @__PURE__ */ new Set();
369
+ for (const command of readHookCommands(contents)) {
370
+ if (pusharyHookExecutable(command) === null) continue;
371
+ for (const match of command.matchAll(FLAG_PATTERNS[flag])) names.add(match[1] ?? "");
372
+ }
373
+ return [...names];
374
+ }
375
+ function pusharyAgentsNamedIn(contents) {
376
+ return registeredNames(contents, "agent");
377
+ }
378
+ function resolvesHookRegistration(name, flag) {
379
+ if (flag === "agent") return hookCompatibleProfile(name) !== void 0;
380
+ return HOOK_SOURCES.includes(name);
381
+ }
382
+ function inertRegistrations(configs, resolves) {
383
+ const findings = [];
384
+ for (const config of configs) {
385
+ for (const flag of REGISTRATION_FLAGS) {
386
+ const names = flag === "agent" ? pusharyAgentsNamedIn(config.contents) : registeredNames(config.contents, flag);
387
+ for (const name of names) {
388
+ if (!resolves(name, flag)) findings.push({ file: config.file, named: name });
389
+ }
390
+ }
391
+ }
392
+ return findings;
393
+ }
394
+ var describeInertRegistration = ({ named }) => `hooks here are registered for "${named}", which this CLI has no profile for, so every one of them stands aside`;
395
+ var inertRegistrationRemedy = () => "Upgrade with npx @pushary/agent-hooks@latest upgrade, or re-run setup to rewrite them for an agent this version knows";
396
+
397
+ // src/hooks-spec.ts
398
+ var CURSOR_GATE_SCRIPT = "pushary-gate.mjs";
399
+
400
+ // src/diagnostics/activation.ts
401
+ var readHookActivation = async (apiKey, agentType, target, deps = {}) => {
402
+ const machineId = deps.machineId ?? getMachineId();
403
+ const url = new URL("/api/agent/onboarding/activation", deps.baseUrl ?? getBaseUrl());
404
+ url.searchParams.set("machineId", machineId);
405
+ url.searchParams.set("agent", agentType);
406
+ url.searchParams.set("target", target);
407
+ try {
408
+ const response = await (deps.fetchImpl ?? fetch)(url, {
409
+ headers: { Authorization: `Bearer ${apiKey}` },
410
+ signal: AbortSignal.timeout(1e4)
411
+ });
412
+ if (!response.ok) return { kind: "unavailable", detail: response.status === 404 ? "This server does not report hook activation yet." : `Evidence request returned HTTP ${response.status}.` };
413
+ const body = await response.json();
414
+ if (!isHookActivationReport(body) || body.machineId !== machineId || body.agentType !== agentType || body.target !== target) {
415
+ return { kind: "unavailable", detail: "The server returned invalid or mismatched activation evidence." };
416
+ }
417
+ return { kind: "ok", report: body };
418
+ } catch {
419
+ return { kind: "unavailable", detail: "Activation evidence could not be read. Retry when the connection is available." };
420
+ }
421
+ };
422
+
423
+ // src/diagnostics/roundtrip-plan.ts
424
+ var planRoundTrip = (context) => {
425
+ if (context.roundtrip) return { kind: "always" };
426
+ if (context.noPush) return { kind: "never", why: "--no-push" };
427
+ if (context.json) return { kind: "never", why: "--json" };
428
+ if (!context.stdinIsTty) return { kind: "never", why: "not a terminal" };
429
+ return { kind: "ask", defaultAnswer: true };
430
+ };
431
+
432
+ // src/setup/agent-exec.ts
433
+ var firstToken = (command) => {
434
+ const trimmed = command.trim();
435
+ const quote = trimmed[0];
436
+ if (quote === '"' || quote === "'") {
437
+ const close = trimmed.indexOf(quote, 1);
438
+ if (close > 0) return trimmed.slice(1, close);
439
+ }
440
+ return trimmed.split(/\s+/)[0] ?? "";
441
+ };
442
+
443
+ // bin/pushary-doctor.ts
444
+ exitOnHelpFlag("doctor");
445
+ var parseDoctorOptions = (argv) => {
446
+ const flags = parseFlags(argv, { bools: ["json", "roundtrip", "no-push", "bundle", "help", "h"], values: ["activation", "activation-target"] });
447
+ if (flags.command !== void 0 && flags.command !== "doctor") {
448
+ throw new UsageError(`doctor takes no positional arguments (got ${flags.command})`);
449
+ }
450
+ const roundtrip = flags.bools.has("roundtrip");
451
+ const activation2 = flags.values.get("activation") ?? null;
452
+ if (activation2 && (!Object.hasOwn(AGENTS, activation2) || !AGENTS[activation2].activationGuide)) throw new UsageError("This build supports --activation claude_code. Other integrations remain unverified.");
453
+ const rawTarget = flags.values.get("activation-target");
454
+ if (Boolean(activation2) !== Boolean(rawTarget)) throw new UsageError("--activation requires --activation-target with the unique scratch-file path used for this check.");
455
+ const activationTarget = rawTarget ? resolve(rawTarget) : null;
456
+ const noPush = flags.bools.has("no-push") || activation2 !== null && !roundtrip;
457
+ if (roundtrip && noPush) {
458
+ throw new UsageError("--roundtrip needs to send a push, so it cannot be combined with --no-push");
459
+ }
460
+ return { activationTarget, activation: activation2, json: flags.bools.has("json"), roundtrip, noPush, bundle: flags.bools.has("bundle") };
461
+ };
462
+ var activation = null;
463
+ var hookActivation = "unverified";
464
+ var roundTrip = null;
465
+ var options;
466
+ try {
467
+ options = parseDoctorOptions(process.argv.slice(2));
468
+ } catch (err) {
469
+ process.stderr.write(` ! ${err instanceof Error ? err.message : String(err)}
470
+ `);
471
+ process.stderr.write(" Run `pushary doctor --help` for the full list.\n");
472
+ process.exit(EXIT.USAGE);
473
+ }
474
+ if (options.json) {
475
+ console.log = (...args) => {
476
+ process.stderr.write(`${args.map((a) => typeof a === "string" ? a : String(a)).join(" ")}
477
+ `);
478
+ };
479
+ }
480
+ var { bold, dim, green, yellow, red, cyan } = createIo();
481
+ var pass = green("\u2713");
482
+ var fail = red("\u2717");
483
+ var warn = yellow("!");
484
+ var CLAUDE_SETTINGS = claudeSettings();
485
+ var registeredConfigs = () => [CLAUDE_SETTINGS, codexHooksJson(), ...HOOK_COMPATIBLE_PROFILES.map((profile) => compatibleConfigFile(profile))].filter(existsSync).map((file) => ({ file, contents: readFileSync(file, "utf-8") }));
486
+ var SKILL_PATH = join(claudeSkillDir(), "SKILL.md");
487
+ var SHELL_FILES = [".zshrc", ".zprofile", ".bashrc", ".bash_profile"].map((f) => join(homedir2(), f));
488
+ var reportFxQuestionRouting = (routing) => {
489
+ if (routing === "routed") {
490
+ check(true, "fx: questions go through Pushary", `${FX_QUESTION_TOOL} is off in ${fxSettings()}`);
491
+ return;
492
+ }
493
+ console.log(` ${warn} fx: questions can still stop in the terminal ${dim(`(${FX_QUESTION_ROUTING_WARNINGS[routing]})`)}`);
494
+ };
495
+ var reportHermesStatus = (status) => {
496
+ const config = hermesConfig();
497
+ check(status.pluginInstalled, "Hermes: plugin installed", status.pluginInstalled ? HERMES_PIP_PACKAGE : `${HERMES_PIP_PACKAGE} is not installed in Hermes' Python \u2014 run ${retryAgentCommand("hermes")}`);
498
+ check(status.pluginEnabled, "Hermes: plugin enabled", status.pluginEnabled ? config : `"${HERMES_PLUGIN_NAME}" is missing from plugins.enabled in ${config} \u2014 run ${retryAgentCommand("hermes")}`);
499
+ if (status.approvalsRouted) check(true, "Hermes: approvals go through Pushary", `security.approval.transport in ${config}`);
500
+ else console.log(` ${warn} Hermes: approvals stay in the terminal ${dim(`(security.approval.transport is not "pushary" in ${config})`)}`);
501
+ if (status.questionsRouted) check(true, "Hermes: questions go through Pushary", `clarify is off in ${config}`);
502
+ else console.log(` ${warn} Hermes: questions can stop in the terminal ${dim(`(clarify is not in agent.disabled_toolsets in ${config})`)}`);
503
+ };
504
+ var FX_QUESTION_ROUTING_WARNINGS = {
505
+ "full-access": "full-access mode ignores permission rules, so fx's terminal question tool stays on; its instructions ask it to use Pushary instead",
506
+ overridden: `a rule in ${fxSettings()} keeps ${FX_QUESTION_TOOL} on; make "${FX_QUESTION_TOOL}": "deny" the last rule that matches it`
507
+ };
508
+ var fxBridgeFound = (target) => target.includes("/") ? existsSync(target) : resolvePathBinary(target) !== void 0;
509
+ var readJson = (path) => {
510
+ try {
511
+ return JSON.parse(readFileSync(path, "utf-8"));
512
+ } catch {
513
+ return null;
514
+ }
515
+ };
516
+ var commandResolves = (command) => {
517
+ const target = pusharyHookExecutable(command) ?? firstToken(command);
518
+ if (!target) return false;
519
+ if (target.includes("/") || target.includes("\\")) return existsSync(target);
520
+ try {
521
+ const whichCmd = process.platform === "win32" ? "where" : "which";
522
+ execFileSync(whichCmd, [target], { stdio: "ignore", timeout: 5e3 });
523
+ return true;
524
+ } catch {
525
+ return false;
526
+ }
527
+ };
528
+ var extractHookCommand = (entries, needle) => {
529
+ if (!Array.isArray(entries)) return null;
530
+ for (const entry of entries) {
531
+ const hookList = entry.hooks;
532
+ const candidates = Array.isArray(hookList) ? hookList : [entry];
533
+ for (const candidate of candidates) {
534
+ const command = candidate.command;
535
+ if (typeof command === "string" && command.includes(needle)) return command;
536
+ }
537
+ }
538
+ return null;
539
+ };
540
+ var extractAgentHookCommand = (entries, needle) => extractHookCommand(entries, needle) ?? extractHookCommand(entries, HOOK_LOCATOR_BINARY);
541
+ var extractHookTimeout = (entries, needle) => {
542
+ if (!Array.isArray(entries)) return null;
543
+ for (const entry of entries) {
544
+ const hookList = entry.hooks;
545
+ if (!Array.isArray(hookList)) continue;
546
+ for (const candidate of hookList) {
547
+ const hook = candidate;
548
+ if (typeof hook.command === "string" && hook.command.includes(needle) && typeof hook.timeout === "number") {
549
+ return hook.timeout;
550
+ }
551
+ }
552
+ }
553
+ return null;
554
+ };
555
+ var results = [];
556
+ var check = (passed, label, detail, kind = "config") => {
557
+ results.push({ passed, label, detail, kind });
558
+ const icon = passed ? pass : fail;
559
+ const suffix = detail ? ` ${dim(`(${detail})`)}` : "";
560
+ console.log(` ${icon} ${label}${suffix}`);
561
+ };
562
+ var probes = agentProbes();
563
+ var installed = (agent) => isDetected(detectAgent(probes[agent]));
564
+ var agentsChecked = [];
565
+ var otherHoldersByAgent = {};
566
+ var bridgeStub = null;
567
+ var noteHookTarget = (command) => {
568
+ const executable = command ? pusharyHookExecutable(command) : null;
569
+ if (isBridgeLocator(executable)) bridgeStub = executable;
570
+ };
571
+ var reportOtherHolders = (agentLabel, holders) => {
572
+ if (holders.length === 0) return;
573
+ otherHoldersByAgent[agentLabel] = holders;
574
+ for (const holder of holders) {
575
+ const icon = holdsLong(holder) ? warn : dim("\u2013");
576
+ console.log(` ${icon} ${agentLabel}: ${describeOtherHolder(holder)}`);
577
+ }
578
+ console.log(` ${dim(describeHoldSentence(agentLabel))}`);
579
+ };
580
+ var describeReadings = (readings) => readings.map((reading) => `${reading.event} ${reading.status}`).join(", ");
581
+ var CODEX_NONE_LISTED = "Codex lists none of the Pushary hooks, so it runs none of them. Check [features].hooks in config.toml, then re-run setup";
582
+ var reportCodexOwnTrust = (assessment) => {
583
+ if (assessment.kind === "unlisted") {
584
+ check(false, "Codex: hooks trusted", CODEX_NONE_LISTED);
585
+ return;
586
+ }
587
+ check(assessment.kind === "trusted", "Codex: hooks trusted", assessment.kind === "trusted" ? `Codex reports ${assessment.trusted} of ${assessment.total} trusted` : `Codex reports ${describeReadings(assessment.failures)}. Type /hooks in Codex and trust them, once`);
588
+ if (assessment.notListed.length > 0) {
589
+ console.log(` ${warn} Codex: does not list ${assessment.notListed.join(", ")} ${dim("(this Codex may not support those events yet)")}`);
590
+ }
591
+ if (assessment.disabled.length > 0) {
592
+ console.log(` ${warn} Codex: ${assessment.disabled.join(", ")} turned off in Codex ${dim("(turn them back on in /hooks)")}`);
593
+ }
594
+ };
595
+ var skipped = (label, why) => {
596
+ console.log(` ${dim("\u2013")} ${label} ${dim(`(${why})`)}`);
597
+ };
598
+ var checking = (label) => {
599
+ agentsChecked.push(label);
600
+ };
601
+ var main = async () => {
602
+ console.log();
603
+ console.log(` ${bold("Pushary Doctor")}`);
604
+ console.log();
605
+ console.log(` ${dim("Configuration")}`);
606
+ const source = readKeySource();
607
+ let apiKey = source.key ?? void 0;
608
+ let keySource = source.kind === "env" ? "env" : source.kind === "config" ? "config-file" : null;
609
+ if (!apiKey) {
610
+ for (const f of SHELL_FILES) {
611
+ try {
612
+ const content = readFileSync(f, "utf-8");
613
+ const match = content.match(/export\s+PUSHARY_API_KEY=['"](pk_[a-f0-9]+\.[a-f0-9]+)['"]/);
614
+ if (match) {
615
+ apiKey = match[1];
616
+ keySource = "profile";
617
+ break;
618
+ }
619
+ } catch {
620
+ }
621
+ }
622
+ }
623
+ const maskKey = (k) => `pk_${k.split(".")[0]?.slice(3, 7)}...`;
624
+ if (keySource === "config-file") {
625
+ console.log(` ${pass} API key in ${dim(source.origin ?? "the config file")} ${dim(`(${maskKey(apiKey)})`)}`);
626
+ } else if (keySource === "profile") {
627
+ check(
628
+ false,
629
+ "API key readable by hooks",
630
+ `found only in a shell profile (${maskKey(apiKey)}), which hooks do not read \u2014 re-run setup, or open a new terminal`
631
+ );
632
+ console.log(` ${dim("Hooks read $PUSHARY_API_KEY, then")} ${dim(pusharyConfigFile())}`);
633
+ } else {
634
+ check(!!apiKey, "API key in environment", apiKey ? maskKey(apiKey) : "PUSHARY_API_KEY not set");
635
+ }
636
+ if (source.drift) {
637
+ check(
638
+ false,
639
+ "One key, not two",
640
+ `shell has ${source.drift.envPrefix}, ${source.drift.configPath} has ${source.drift.configPrefix} \u2014 the shell one wins. Re-run setup, or open a new terminal.`
641
+ );
642
+ }
643
+ let keyVerdict = null;
644
+ if (apiKey) {
645
+ keyVerdict = await checkApiKey(apiKey);
646
+ switch (keyVerdict.kind) {
647
+ case "ok":
648
+ check(
649
+ true,
650
+ "Key verified with pushary.com",
651
+ `${keyVerdict.identity.siteSlug || keyVerdict.identity.siteName} \xB7 plan ${keyVerdict.identity.plan} \xB7 ${keyVerdict.identity.subscriptionStatus}`
652
+ );
653
+ break;
654
+ case "locked":
655
+ check(false, "Plan active", describeKeyCheck(keyVerdict).message);
656
+ for (const line of describeKeyCheck(keyVerdict).next) console.log(` ${dim(line)}`);
657
+ break;
658
+ case "rejected":
659
+ check(false, "Key accepted by pushary.com", describeKeyCheck(keyVerdict).message);
660
+ for (const line of describeKeyCheck(keyVerdict).next) console.log(` ${dim(line)}`);
661
+ break;
662
+ case "malformed":
663
+ check(false, "Key format", "expected pk_xxx.xxx");
664
+ break;
665
+ case "unreachable":
666
+ check(false, "Key verified with pushary.com", `could not reach pushary.com (${keyVerdict.detail})`, "unreachable");
667
+ break;
668
+ }
669
+ }
670
+ const CLAUDE_JSON = claudeJson();
671
+ const claudeInPlay = claudeWired({
672
+ claudeJson: readJson(CLAUDE_JSON),
673
+ settings: readJson(CLAUDE_SETTINGS),
674
+ skillExists: existsSync(SKILL_PATH)
675
+ });
676
+ const claudeJson2 = claudeInPlay ? readJson(CLAUDE_JSON) : null;
677
+ const mcpServers = claudeJson2?.mcpServers ?? {};
678
+ const pusharyServer = mcpServers?.pushary;
679
+ if (!claudeInPlay) {
680
+ skipped("Claude Code", installed("claude_code") ? "installed, but Pushary is not set up for it" : "not installed");
681
+ } else {
682
+ checking("Claude Code");
683
+ check(!!pusharyServer, "Claude Code: MCP server configured");
684
+ if (pusharyServer) {
685
+ check(pusharyServer.type === "http", "Claude Code: MCP server type", pusharyServer.type ? String(pusharyServer.type) : 'missing \u2014 add type: "http"');
686
+ const claudeAuth = pusharyServer.headers?.Authorization;
687
+ if (typeof claudeAuth === "string" && apiKey) {
688
+ const matches = claudeAuth === `Bearer ${apiKey}`;
689
+ 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");
690
+ }
691
+ }
692
+ }
693
+ const settings = claudeInPlay ? readJson(CLAUDE_SETTINGS) : null;
694
+ let claudeHooksMissing = 0;
695
+ if (settings) {
696
+ const hooks = settings.hooks;
697
+ const CLAUDE_HOOK_CHECKS = [
698
+ { event: "PreToolUse", needle: "pushary-hook", label: "PreToolUse hook" },
699
+ { event: "PostToolUse", needle: "pushary-post-hook", label: "PostToolUse hook" },
700
+ { event: "UserPromptSubmit", needle: "pushary-prompt-hook", label: "UserPromptSubmit hook" },
701
+ { event: "Stop", needle: "pushary-stop-hook", label: "Stop hook" },
702
+ { event: "StopFailure", needle: "pushary-stopfailure-hook", label: "StopFailure hook" },
703
+ { event: "Notification", needle: "pushary-notification-hook", label: "Notification hook" },
704
+ { event: "SessionStart", needle: "pushary-session-start-hook", label: "SessionStart hook" },
705
+ { event: "SessionEnd", needle: "pushary-session-end-hook", label: "SessionEnd hook" },
706
+ { event: "PermissionRequest", needle: "pushary-permission-hook", label: "PermissionRequest hook" },
707
+ { event: "PermissionDenied", needle: "pushary-permission-denied-hook", label: "PermissionDenied hook" }
708
+ ];
709
+ for (const { event, needle, label } of CLAUDE_HOOK_CHECKS) {
710
+ const configured = JSON.stringify(hooks?.[event] ?? []);
711
+ const present = configured.includes(needle) || configured.includes(HOOK_LOCATOR_BINARY);
712
+ if (!present) claudeHooksMissing += 1;
713
+ check(present, `Claude Code: ${label}`, present ? void 0 : "missing \u2014 run npx @pushary/agent-hooks@latest upgrade");
714
+ }
715
+ for (const event of claudeUnprovenHolds(settings)) {
716
+ console.log(` ${warn} Claude Code: ${event} hook carries no current hold budget ${dim("(an auto-mode block only notifies; run npx @pushary/agent-hooks@latest upgrade)")}`);
717
+ }
718
+ const preHookCommand = extractAgentHookCommand(hooks?.PreToolUse, "pushary-hook");
719
+ if (preHookCommand) {
720
+ const resolves = commandResolves(preHookCommand);
721
+ check(resolves, "Claude Code: hook command resolves", resolves ? preHookCommand : `not on PATH \u2014 ${preHookCommand}`);
722
+ }
723
+ noteHookTarget(preHookCommand);
724
+ reportOtherHolders("Claude Code", claudeOtherHolders(settings));
725
+ const permissions = settings.permissions;
726
+ const hasWildcard = permissions?.allow?.some((r) => r === "mcp__pushary__*" || r === "MCP(pushary:*)") ?? false;
727
+ check(hasWildcard, "Claude Code: Pushary tools auto-allowed", hasWildcard ? "mcp__pushary__*" : "missing");
728
+ const hasLegacyPerms = permissions?.allow?.some((r) => r.startsWith("mcp__pushary__") && r !== "mcp__pushary__*") ?? false;
729
+ if (hasLegacyPerms) {
730
+ console.log(` ${warn} Legacy individual permissions detected ${dim("(run pushary clean, then setup again)")}`);
731
+ }
732
+ } else if (claudeInPlay) {
733
+ check(false, "Claude Code: settings.json", "not found");
734
+ }
735
+ const claudeHookState = { wired: claudeInPlay, hooksIntact: settings !== null && claudeHooksMissing === 0 };
736
+ for (const finding of rewriterFindings({
737
+ present: (rewriter) => rewriter.markers.some((marker) => existsSync(join(homedir2(), ...marker))),
738
+ agentState: (agent) => agent === "Claude Code" ? claudeHookState : void 0
739
+ })) {
740
+ console.log(` ${warn} ${describeRewriterFinding(finding)}`);
741
+ console.log(` ${dim(rewriterMechanism(finding))}`);
742
+ console.log(` ${dim(rewriterRemedy(finding))}`);
743
+ }
744
+ const codexConfigPath = codexConfigToml();
745
+ const codexHooksPath = codexHooksJson();
746
+ const codexHooksConfig = readJson(codexHooksPath);
747
+ const codexConfig = existsSync(codexConfigPath) ? readFileSync(codexConfigPath, "utf-8") : "";
748
+ let codexParsed = {};
749
+ try {
750
+ codexParsed = parseTOML(codexConfig);
751
+ } catch {
752
+ }
753
+ const codexInPlay = codexWired({
754
+ config: codexParsed,
755
+ hooks: codexHooksConfig,
756
+ skillExists: existsSync(join(codexSkillDir(), "SKILL.md"))
757
+ });
758
+ if (codexInPlay) {
759
+ checking("Codex");
760
+ const codexMcp = codexParsed.mcp_servers?.pushary;
761
+ const codexTransport = !!codexMcp && (typeof codexMcp.url === "string" || typeof codexMcp.command === "string");
762
+ const codexAuth = readCodexMcpAuth(codexParsed);
763
+ const codexMcpOk = !!codexMcp && codexTransport && !!codexAuth;
764
+ check(codexMcpOk, "Codex: MCP server configured", codexMcpOk ? void 0 : !codexMcp ? "no [mcp_servers.pushary] \u2014 re-run setup" : !codexTransport ? "entry has no url \u2014 re-run setup" : "entry has no Authorization header or bearer token \u2014 re-run setup");
765
+ if (codexMcp) {
766
+ const hasAutoApprove = codexMcp.default_tools_approval_mode === "approve";
767
+ check(hasAutoApprove, "Codex: tools auto-allowed", hasAutoApprove ? "default_tools_approval_mode = approve" : "missing \u2014 MCP calls will prompt for approval");
768
+ if (codexMcp.tools && typeof codexMcp.tools === "object") {
769
+ console.log(` ${warn} Codex: per-tool approval overrides detected ${dim("(redundant with default_tools_approval_mode)")}`);
770
+ }
771
+ if (codexAuth?.method === "embedded") {
772
+ const keyMatches = !apiKey || codexAuth.key === apiKey;
773
+ check(keyMatches, "Codex: MCP key embedded (config.toml)", keyMatches ? "no shell export needed" : "differs from your active key \u2014 re-run setup");
774
+ } else if (codexAuth?.method === "env") {
775
+ const envValue = process.env[codexAuth.envVar]?.trim();
776
+ const exportedInProfile = SHELL_FILES.some((f) => {
777
+ try {
778
+ return readFileSync(f, "utf-8").includes(`export ${codexAuth.envVar}=`);
779
+ } catch {
780
+ return false;
781
+ }
782
+ });
783
+ check(!!envValue, `Codex: MCP key available in shell ($${codexAuth.envVar})`, envValue ? "set \u2014 re-run setup to embed the key and drop the shell dependency" : exportedInProfile ? "exported in your profile but not in this shell \u2014 restart the terminal, or re-run setup to embed the key in config.toml" : "not set \u2014 Codex reads its key from your shell; re-run setup to embed the key in config.toml");
784
+ if (envValue && apiKey && envValue !== apiKey) {
785
+ console.log(` ${warn} Codex: $${codexAuth.envVar} differs from your active key ${dim("(rotate or re-run setup so hooks and MCP match)")}`);
786
+ }
787
+ } else if (codexAuth?.method === "legacy-bearer") {
788
+ console.log(` ${warn} Codex: legacy bearer_token entry ${dim("(re-run setup to embed the key as an Authorization header)")}`);
789
+ }
790
+ }
791
+ const codexNotifyPath = codexConfig.match(/["']([^"']*pushary-codex[^"']*)["']/)?.[1] ?? null;
792
+ const hooksInstalled = !!codexHooksConfig && hasCodexHooks(codexHooksConfig);
793
+ if (hooksInstalled) {
794
+ const missingEvents = missingCodexHookEvents(codexHooksConfig);
795
+ check(missingEvents.length === 0, "Codex: native hooks installed", missingEvents.length === 0 ? `all ${CODEX_HOOK_EVENTS.length} events` : `missing ${missingEvents.join(", ")}, re-run setup`);
796
+ const hookCommand = extractAgentHookCommand(codexHooksConfig.hooks?.PreToolUse, "pushary-codex-hook") ?? extractAgentHookCommand(codexHooksConfig.hooks?.PermissionRequest, "pushary-codex-hook");
797
+ if (hookCommand) {
798
+ const resolves = commandResolves(hookCommand);
799
+ check(resolves, "Codex: hook command resolves", resolves ? hookCommand : `not on PATH: ${hookCommand}`);
800
+ }
801
+ noteHookTarget(hookCommand);
802
+ reportOtherHolders("Codex", codexOtherHolders(codexHooksConfig));
803
+ const features = codexParsed.features;
804
+ const hooksFeatureDisabled = !!features && typeof features === "object" && features.hooks === false;
805
+ check(!hooksFeatureDisabled, "Codex: hooks feature enabled", hooksFeatureDisabled ? "[features].hooks = false in config.toml" : void 0);
806
+ if (codexNotifyPath) {
807
+ console.log(` ${warn} Codex: stale legacy notify entry ${dim("(double-push risk, re-run setup to remove it)")}`);
808
+ }
809
+ if (hookCommand) {
810
+ const positions = codexHookPositions(codexHooksConfig);
811
+ const gaps = codexTrustGaps(
812
+ codexParsed,
813
+ codexHooksConfig,
814
+ codexHooksPath,
815
+ hookCommand,
816
+ positions
817
+ );
818
+ const untrusted = gaps.filter((gap) => gap.kind === "untrusted").map((gap) => gap.event);
819
+ const stale = gaps.filter((gap) => gap.kind === "stale").map((gap) => gap.event);
820
+ const codexTrust = await readCodexTrustForPusharyHooks();
821
+ if (codexTrust.kind === "listed") {
822
+ reportCodexOwnTrust(assessCodexHookTrust(Object.keys(positions), codexTrust.hooks));
823
+ } else {
824
+ const why = codexTrust.kind === "skipped" ? "did not ask" : "could not ask";
825
+ console.log(` ${warn} Codex: ${why} Codex about hook trust ${dim(`(${codexTrust.reason}); checking Pushary's own record instead`)}`);
826
+ check(untrusted.length === 0, "Codex: hooks trusted", untrusted.length === 0 ? "all events trusted" : `untrusted (${untrusted.join(", ")}) \u2014 type /hooks in Codex and trust them, once`);
827
+ }
828
+ if (stale.length > 0) {
829
+ console.log(` ${warn} Codex: ${stale.join(", ")} trusted at an older shape ${dim("(fires normally; re-run setup to pick up the current one)")}`);
830
+ }
831
+ }
832
+ for (const event of codexUnprovenHolds(codexHooksConfig)) {
833
+ console.log(` ${warn} Codex: ${event} hook carries no current hold budget ${dim("(an auto-reviewer denial is not asked about; run npx @pushary/agent-hooks@latest upgrade)")}`);
834
+ }
835
+ } else {
836
+ check(!!codexNotifyPath, "Codex: notify handler configured (deprecated)", codexNotifyPath ? "upgrade Codex and re-run setup for native hooks" : "missing, re-run setup");
837
+ if (codexNotifyPath) {
838
+ const resolves = commandResolves(codexNotifyPath);
839
+ check(resolves, "Codex: notify handler resolves", resolves ? codexNotifyPath : `not found: ${codexNotifyPath}`);
840
+ }
841
+ }
842
+ const codexSkillPath = join(codexSkillDir(), "SKILL.md");
843
+ check(existsSync(codexSkillPath), "Codex: skill installed");
844
+ const codexAgentsMd2 = codexAgentsMd();
845
+ const hasCodexInstructions = hasInstructionBlock(codexAgentsMd2);
846
+ check(hasCodexInstructions, "Codex: proactive instructions (AGENTS.md)", hasCodexInstructions ? codexAgentsMd2 : "missing \u2014 re-run setup so Codex asks via push, not the terminal");
847
+ } else {
848
+ skipped("Codex", installed("codex") ? "installed, but Pushary is not set up for it" : "not installed");
849
+ }
850
+ const geminiSettingsPath = geminiSettings();
851
+ const geminiSettings2 = readJson(geminiSettingsPath);
852
+ const geminiInPlay = geminiSettings2 !== null && geminiWired({ settings: geminiSettings2 });
853
+ if (geminiInPlay) {
854
+ checking("Gemini CLI");
855
+ const geminiServers = geminiSettings2.mcpServers ?? {};
856
+ const geminiPushary = geminiServers.pushary;
857
+ check(!!geminiPushary, "Gemini CLI: MCP server configured");
858
+ if (geminiPushary) {
859
+ check(typeof geminiPushary.httpUrl === "string", "Gemini CLI: MCP transport", geminiPushary.httpUrl ? String(geminiPushary.httpUrl) : "missing \u2014 add httpUrl");
860
+ check(geminiPushary.trust === true, "Gemini CLI: tools auto-allowed", geminiPushary.trust === true ? "trust: true" : "missing \u2014 MCP calls will prompt for confirmation");
861
+ const geminiAuth = geminiPushary.headers?.Authorization;
862
+ if (typeof geminiAuth === "string") {
863
+ if (geminiAuth.includes("${PUSHARY_API_KEY}")) {
864
+ check(false, "Gemini CLI: API key embedded", "header has an unexpanded ${PUSHARY_API_KEY} \u2014 re-run setup to embed the key");
865
+ } else if (apiKey && geminiAuth !== `Bearer ${apiKey}`) {
866
+ check(false, "Gemini CLI: API key matches active key", "embedded key differs from your active key (rotated?) \u2014 re-run setup");
867
+ } else {
868
+ check(true, "Gemini CLI: API key embedded");
869
+ }
870
+ }
871
+ }
872
+ const geminiHooksInstalled = hasGeminiHooks(geminiSettings2);
873
+ if (geminiHooksInstalled) {
874
+ const missingEvents = missingGeminiHookEvents(geminiSettings2);
875
+ check(missingEvents.length === 0, "Gemini CLI: native hooks installed", missingEvents.length === 0 ? "all 5 events" : `missing ${missingEvents.join(", ")}, re-run setup`);
876
+ const geminiHooks = geminiSettings2.hooks;
877
+ const hookCommand = extractAgentHookCommand(geminiHooks?.BeforeTool, GEMINI_HOOK_BINARY) ?? extractAgentHookCommand(geminiHooks?.AfterTool, GEMINI_HOOK_BINARY) ?? extractAgentHookCommand(geminiHooks?.SessionStart, GEMINI_HOOK_BINARY);
878
+ if (hookCommand) {
879
+ const resolves = commandResolves(hookCommand);
880
+ check(resolves, "Gemini CLI: hook command resolves", resolves ? hookCommand : `not on PATH: ${hookCommand}`);
881
+ }
882
+ noteHookTarget(hookCommand);
883
+ const geminiTimeout = extractHookTimeout(geminiHooks?.BeforeTool, GEMINI_HOOK_BINARY) ?? extractHookTimeout(geminiHooks?.BeforeTool, HOOK_LOCATOR_BINARY);
884
+ if (typeof geminiTimeout === "number") {
885
+ const ok = geminiTimeout >= 3e4;
886
+ check(ok, "Gemini CLI: hook timeout", ok ? `${geminiTimeout}ms` : `${geminiTimeout}ms too short (Gemini timeouts are milliseconds) \u2014 re-run setup`);
887
+ }
888
+ reportOtherHolders("Gemini CLI", geminiOtherHolders(geminiSettings2));
889
+ } else {
890
+ check(false, "Gemini CLI: native hooks installed", "no Pushary hooks in ~/.gemini/settings.json \u2014 re-run setup");
891
+ }
892
+ const geminiMd2 = geminiMd();
893
+ const hasGeminiInstructions = hasInstructionBlock(geminiMd2);
894
+ check(hasGeminiInstructions, "Gemini CLI: proactive instructions (GEMINI.md)", hasGeminiInstructions ? geminiMd2 : "missing \u2014 re-run setup so Gemini asks via push, not the terminal");
895
+ } else {
896
+ skipped("Gemini CLI", installed("gemini_cli") ? "installed, but Pushary is not set up for it" : "not installed");
897
+ }
898
+ for (const finding of inertRegistrations(registeredConfigs(), resolvesHookRegistration)) {
899
+ console.log(` ${warn} ${shortenHome(finding.file)}: ${describeInertRegistration(finding)}`);
900
+ console.log(` ${dim(inertRegistrationRemedy())}`);
901
+ }
902
+ for (const profile of HOOK_COMPATIBLE_PROFILES) {
903
+ const configPath = compatibleConfigFile(profile);
904
+ const config = readJson(configPath);
905
+ if (!config || !hasCompatibleHooks(config, profile)) {
906
+ skipped(profile.label, installed(profile.agentType) ? "installed, but Pushary is not set up for it" : "not installed");
907
+ continue;
908
+ }
909
+ checking(profile.label);
910
+ const missing = missingCompatibleHookEvents(config, profile);
911
+ check(
912
+ missing.length === 0,
913
+ `${profile.label}: native hooks installed`,
914
+ missing.length === 0 ? shortenHome(configPath) : `missing ${missing.join(", ")} \u2014 re-run setup`
915
+ );
916
+ const mcpPath = compatibleMcpFile(profile);
917
+ if (mcpPath) {
918
+ const hasServer = Boolean(readJson(mcpPath)?.mcpServers?.pushary);
919
+ check(hasServer, `${profile.label}: MCP server`, hasServer ? shortenHome(mcpPath) : "missing \u2014 re-run setup");
920
+ }
921
+ const instructionsPath = compatibleInstructionsFile(profile);
922
+ if (instructionsPath) {
923
+ const present = hasInstructionBlock(instructionsPath);
924
+ check(
925
+ present,
926
+ `${profile.label}: proactive instructions`,
927
+ present ? shortenHome(instructionsPath) : "missing \u2014 re-run setup so it asks via push, not the terminal"
928
+ );
929
+ }
930
+ }
931
+ const configuredManagedProviders = [
932
+ ...claudeInPlay ? ["claude"] : [],
933
+ ...codexInPlay ? ["codex"] : [],
934
+ ...geminiInPlay ? ["gemini"] : []
935
+ ];
936
+ const controlCapabilities = detectProviderLiveCapabilities(configuredManagedProviders);
937
+ const controlSupported = hasSpawnProvider(controlCapabilities);
938
+ const controlMode = readControlMode();
939
+ const daemon = readDaemonStatus();
940
+ const loginBlock = readDaemonNeedsLogin();
941
+ const serviceDefined = hasDaemonServiceDefinition();
942
+ const managerAvailable = daemonServiceManagerAvailable();
943
+ const controlPolicy = resolveControlPolicy({
944
+ mode: controlMode,
945
+ providerAvailable: controlSupported,
946
+ managerAvailable,
947
+ servicePresent: serviceDefined || daemon.kind !== "absent"
948
+ });
949
+ const controlRequired = controlPolicy === "enabled";
950
+ const controlUnsupported = controlMode === "on" && controlPolicy !== "enabled";
951
+ const daemonOk = controlUnsupported ? false : controlPolicy === "disabled" ? daemon.kind === "absent" && !serviceDefined : controlPolicy === "enabled" ? daemonHasRemoteProof(daemon) : controlPolicy === "legacy" ? false : daemon.kind === "absent" && !serviceDefined;
952
+ const needsLogin = loginBlock.kind === "blocked" ? `needs login: ${describeDaemonLoginBlock(loginBlock.record)}` : null;
953
+ const daemonFailure = daemonOk ? null : needsLogin ?? (controlUnsupported ? !controlSupported ? "install and configure a supported Claude Code, Codex or Gemini provider" : "no native background-service manager is available in this OS session" : controlPolicy === "enabled" && daemon.kind === "running" ? "the daemon has not completed an authenticated heartbeat and control poll" : controlPolicy === "disabled" ? `control is disabled but ${serviceDefined ? "the service definition remains" : `local state is ${daemon.kind}`}` : controlPolicy === "legacy" ? "a legacy service exists without a usable provider/service-manager pair" : `local state is ${daemon.kind}`);
954
+ const daemonDetail = daemonFailure ? needsLogin ? daemonFailure : controlUnsupported ? `${daemonFailure}; re-run setup with \`--control off\`, or repair the missing prerequisite` : controlPolicy === "disabled" ? `${daemonFailure}; re-run setup with \`--control off\`` : controlPolicy === "legacy" ? `${daemonFailure}; run setup with \`--control off\`, or repair the missing prerequisite` : `${daemonFailure}; run \`pushary daemon install\` to install or repair the background service` : controlPolicy === "disabled" ? "disabled by setup" : controlPolicy === "preserve-absent" ? "legacy absence preserved" : !controlRequired && daemon.kind === "absent" ? "managed control is unsupported on this provider or operating-system session" : null;
955
+ check(
956
+ daemonOk,
957
+ "Remote-control daemon",
958
+ [
959
+ daemonDetail,
960
+ daemon.kind,
961
+ `pid ${daemon.pid ?? "none"}`,
962
+ `daemon ${daemon.daemonVersion ?? "none"}`,
963
+ `installed ${daemon.installedVersion}`,
964
+ `machine ${daemon.machineId ?? "none"}`,
965
+ `heartbeat ${daemon.lastSuccessfulHeartbeatAt ?? "none yet"}`,
966
+ `control ${daemon.lastSuccessfulControlAt ?? "none yet"}`,
967
+ daemon.lastError ? `last error ${daemon.lastError.message} at ${daemon.lastError.at}` : null
968
+ ].filter((value) => value !== null).join(" \xB7 ")
969
+ );
970
+ const serviceFile = serviceDefined ? readInstalledDaemonService() : null;
971
+ const serviceNode = serviceFile ? readServiceExecPath(serviceFile.contents, process.platform) : null;
972
+ if (serviceFile && serviceNode) {
973
+ const present = existsSync(serviceNode);
974
+ const manager = versionManagerOf(serviceNode);
975
+ check(
976
+ present,
977
+ "Remote-control service runtime",
978
+ present ? `${serviceNode}${manager ? ` (managed by ${manager}; a version switch strands the service)` : ""}` : `${serviceNode} is gone${manager ? ` (${manager} switched Node since setup)` : ""}; run \`pushary daemon install\` to repin the service`
979
+ );
980
+ } else if (serviceFile) {
981
+ console.log(` ${warn} Remote-control service runtime ${dim(`(could not read the node path out of ${serviceFile.path})`)}`);
982
+ }
983
+ let serverProof = null;
984
+ if (controlPolicy === "enabled" && apiKey && daemon.machineId) {
985
+ serverProof = await readMachineReadiness(apiKey, ["spawn", ...controlCapabilities], {
986
+ machineId: daemon.machineId
987
+ });
988
+ if (serverProof.kind === "unsupported") {
989
+ console.log(` ${warn} Remote-control server proof unavailable ${dim(`(${serverProof.detail})`)}`);
990
+ } else {
991
+ check(
992
+ serverProof.kind === "ready",
993
+ "Remote-control server proof",
994
+ serverProof.kind === "ready" ? "heartbeat \xB7 capabilities \xB7 control queue" : serverProof.detail,
995
+ serverProof.kind === "failed" ? "unreachable" : void 0
996
+ );
997
+ }
998
+ }
999
+ const cursorPluginDir2 = cursorPluginDir();
1000
+ const cursorUserHooksJson = readJson(cursorUserHooks());
1001
+ const cursorUserMcpJson = readJson(cursorUserMcp());
1002
+ if (cursorWired({
1003
+ userHooks: cursorUserHooksJson,
1004
+ mcp: cursorUserMcpJson,
1005
+ pluginDirExists: existsSync(cursorPluginDir2)
1006
+ })) {
1007
+ checking("Cursor");
1008
+ const userHooks = cursorUserHooksJson;
1009
+ const gateEntries = CURSOR_GATE_EVENTS.map((event) => ({
1010
+ event,
1011
+ entry: userHooks?.hooks?.[event]?.find((h) => pusharyHookExecutable(String(h.command ?? "")) !== null)
1012
+ }));
1013
+ const missing = gateEntries.filter(({ entry }) => !entry).map(({ event }) => event);
1014
+ if (missing.length === gateEntries.length) {
1015
+ check(false, "Cursor: permission gate registered", "no Pushary gate in ~/.cursor/hooks.json \u2014 re-run setup (a plugin-only hooks.json is never read by Cursor)");
1016
+ } else if (missing.length > 0) {
1017
+ check(false, "Cursor: permission gate registered", `${missing.join(", ")} not registered \u2014 re-run setup`);
1018
+ } else {
1019
+ const paths_ = gateEntries.map(({ entry }) => pusharyHookExecutable(String(entry.command)) ?? "");
1020
+ const broken = paths_.filter((path) => !path || !existsSync(path));
1021
+ check(
1022
+ broken.length === 0,
1023
+ "Cursor: permission gate registered",
1024
+ broken.length === 0 ? `~/.cursor/hooks.json \u2192 ${paths_[0]} (${gateEntries.length} events)` : `gate script not found: ${broken[0]} \u2014 re-run setup`
1025
+ );
1026
+ noteHookTarget(String(gateEntries[0].entry.command));
1027
+ }
1028
+ reportOtherHolders("Cursor", cursorOtherHolders(cursorUserHooksJson));
1029
+ const { authorization: cursorAuth, source: cursorMcpSource } = cursorMcpAuthorization(
1030
+ readJson(join(cursorPluginDir2, "mcp.json")),
1031
+ cursorUserMcpJson
1032
+ );
1033
+ if (!cursorAuth) {
1034
+ check(false, "Cursor: API key linked", `no Authorization in ${cursorMcpSource} \u2014 re-run setup`);
1035
+ } else if (cursorAuth.includes("PUSHARY_API_KEY")) {
1036
+ check(false, "Cursor: API key linked", `${cursorMcpSource} relies on $PUSHARY_API_KEY; re-run setup to embed the key so the gate works when Cursor is launched from the GUI`);
1037
+ } else if (apiKey && cursorAuth !== `Bearer ${apiKey}`) {
1038
+ check(false, "Cursor: API key matches active key", "the embedded key is not the key hooks use (rotated?) \u2014 re-run setup");
1039
+ } else {
1040
+ check(true, "Cursor: API key linked", `embedded in ${cursorMcpSource}`);
1041
+ }
1042
+ } else {
1043
+ skipped("Cursor", installed("cursor") ? "installed, but Pushary is not set up for it" : "not installed");
1044
+ }
1045
+ const vscodePluginDir2 = vscodePluginDir();
1046
+ if (existsSync(vscodePluginDir2)) {
1047
+ checking("VS Code");
1048
+ const registeredIn = vscodeSettingsTargets(existsSync).filter(
1049
+ (settingsPath) => existsSync(settingsPath) && isPluginRegistered(readFileSync(settingsPath, "utf-8"), vscodePluginDir2)
1050
+ );
1051
+ check(
1052
+ registeredIn.length > 0,
1053
+ "VS Code: plugin registered",
1054
+ registeredIn.length > 0 ? registeredIn.join(", ") : "not listed in chat.pluginLocations \u2014 re-run setup (an unregistered plugin directory is never loaded)"
1055
+ );
1056
+ const vscodeHooks = readJson(join(vscodePluginDir2, "hooks", "hooks.json"));
1057
+ const gateCommand = extractAgentHookCommand(vscodeHooks?.hooks?.PreToolUse, CURSOR_GATE_SCRIPT);
1058
+ if (!gateCommand) {
1059
+ check(false, "VS Code: permission gate present", "no Pushary gate in the plugin hooks.json \u2014 re-run setup");
1060
+ } else {
1061
+ const scriptPath = pusharyHookExecutable(gateCommand) ?? "";
1062
+ const resolves = scriptPath ? existsSync(scriptPath) : false;
1063
+ check(
1064
+ resolves,
1065
+ "VS Code: permission gate present",
1066
+ resolves ? scriptPath : `gate script not found: ${scriptPath || gateCommand} \u2014 re-run setup`
1067
+ );
1068
+ noteHookTarget(gateCommand);
1069
+ }
1070
+ const vscodeMcp = readJson(join(vscodePluginDir2, ".mcp.json"));
1071
+ const vscodeServers = vscodeMcp?.mcpServers ?? {};
1072
+ const vscodeAuth = vscodeServers.pushary?.headers?.Authorization;
1073
+ if (!vscodeAuth) {
1074
+ check(false, "VS Code: API key linked", "no Authorization in plugin .mcp.json \u2014 re-run setup");
1075
+ } else if (vscodeAuth.includes("PUSHARY_API_KEY")) {
1076
+ 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");
1077
+ } else if (apiKey && vscodeAuth !== `Bearer ${apiKey}`) {
1078
+ check(false, "VS Code: API key matches active key", "the embedded key is not the key hooks use (rotated?) \u2014 re-run setup");
1079
+ } else {
1080
+ check(true, "VS Code: API key linked", "embedded in plugin .mcp.json");
1081
+ }
1082
+ } else {
1083
+ skipped("VS Code", installed("vscode") ? "installed, but Pushary is not set up for it" : "not installed");
1084
+ }
1085
+ if (openCodeWired()) {
1086
+ checking("OpenCode");
1087
+ const plugin = openCodePluginFile();
1088
+ const contents = (() => {
1089
+ try {
1090
+ return readFileSync(plugin, "utf-8");
1091
+ } catch {
1092
+ return "";
1093
+ }
1094
+ })();
1095
+ check(true, "OpenCode: plugin present", plugin);
1096
+ const substituted = !contents.includes(OPENCODE_PLUGIN_PLACEHOLDER);
1097
+ check(
1098
+ substituted,
1099
+ "OpenCode: hook command written",
1100
+ substituted ? "the plugin carries this machine's command" : "placeholder never replaced \u2014 re-run setup"
1101
+ );
1102
+ const target = String(contents.match(/const COMMAND = (\[[^\n]*\])/)?.[1] ?? "");
1103
+ const argv = (() => {
1104
+ try {
1105
+ return JSON.parse(target);
1106
+ } catch {
1107
+ return [];
1108
+ }
1109
+ })();
1110
+ const binary = argv.find((part) => part.includes("/") && part.includes("pushary"));
1111
+ if (binary) {
1112
+ check(
1113
+ existsSync(binary),
1114
+ "OpenCode: hook binary resolves",
1115
+ existsSync(binary) ? binary : `${binary} is not there \u2014 re-run setup`
1116
+ );
1117
+ }
1118
+ const manifest = join(openCodePluginDir(), "package.json");
1119
+ check(
1120
+ existsSync(manifest),
1121
+ "OpenCode: plugins directory declares ESM",
1122
+ existsSync(manifest) ? manifest : `no package.json beside the plugin; Node may load it as CommonJS \u2014 re-run setup`
1123
+ );
1124
+ } else {
1125
+ skipped("OpenCode", installed("opencode") ? "installed, but Pushary is not set up for it" : "not installed");
1126
+ }
1127
+ const fx = inspectFxInstallation(fxMcpProfile(), fxSettings(), process.env);
1128
+ const fxProfile = fx.profile;
1129
+ if (fx.configured) {
1130
+ checking("fx");
1131
+ check(true, "fx: Pushary MCP server configured", fxMcpProfile());
1132
+ const bridge = fx.bridge;
1133
+ if (bridge) {
1134
+ const found = fxBridgeFound(bridge);
1135
+ check(found, "fx: bridge binary resolves", found ? bridge : `${bridge} is not installed or not on PATH \u2014 re-run ${retryAgentCommand("fx")}`);
1136
+ }
1137
+ const settingsProblem = fx.settingsProblem;
1138
+ if (settingsProblem) {
1139
+ console.log(` ${warn} fx: Pushary tools are not auto-allowed and questions can still stop in the terminal ${dim(`(${settingsProblem})`)}`);
1140
+ } else if (!fx.toolsAllowed) {
1141
+ console.log(` ${warn} fx: Pushary tools are not auto-allowed ${dim(`(fx will review or prompt for each call; add "${FX_PUSHARY_TOOLS_RULE}": "allow" to permission in ${fxSettings()})`)}`);
1142
+ }
1143
+ const questionRouting = fx.questionRouting;
1144
+ if (settingsProblem === null) reportFxQuestionRouting(questionRouting);
1145
+ const workspacesShowingQuestions = fx.workspacesShowingQuestions;
1146
+ if (workspacesShowingQuestions.length > 0) {
1147
+ console.log(` ${warn} fx: ${workspacesShowingQuestions.length} workspace(s) replace the global rules and keep ${FX_QUESTION_TOOL} on ${dim(`(add "${FX_QUESTION_TOOL}": "deny" to their permission rules in ${fxSettings()})`)}`);
1148
+ }
1149
+ } else if (fxProfile.kind === "refused" && installed("fx")) {
1150
+ check(false, "fx: MCP profile", `${fxMcpProfile()} ${FX_PROFILE_REFUSALS[fxProfile.reason]} \u2014 fix it, then run ${retryAgentCommand("fx")}`);
1151
+ } else {
1152
+ skipped("fx", installed("fx") ? "installed, but Pushary is not set up for it" : "not installed");
1153
+ }
1154
+ const hermesPython = resolveHermesPython();
1155
+ const hermesStatus = hermesPython ? readHermesStatus(hermesPython) : null;
1156
+ const hermesSetupRecorded = existsSync(hermesConfigReceipt());
1157
+ if (hermesStatus && hermesWiredByPushary(hermesStatus, hermesSetupRecorded)) {
1158
+ checking("Hermes");
1159
+ reportHermesStatus(hermesStatus);
1160
+ } else if (hermesPython && hermesStatus === null && hermesSetupRecorded) {
1161
+ check(false, "Hermes: config readable", `${hermesConfig()} could not be read with Hermes' Python \u2014 run ${retryAgentCommand("hermes")}`);
1162
+ } else {
1163
+ skipped("Hermes", installed("hermes") ? "installed, but Pushary is not set up for it" : "not installed");
1164
+ }
1165
+ if (bridgeStub) {
1166
+ const resolution = resolveBridgeStub(bridgeStub, liveBridgeProbe());
1167
+ check(resolution.kind === "bundle", "Mac app behind pushary-bridge", describeBridgeResolution(resolution));
1168
+ }
1169
+ check(
1170
+ agentsChecked.length > 0,
1171
+ "Wired to at least one agent",
1172
+ agentsChecked.length > 0 ? agentsChecked.join(", ") : "Pushary is installed but configured for no agent \u2014 run npx @pushary/agent-hooks@latest setup"
1173
+ );
1174
+ if (claudeInPlay) {
1175
+ check(existsSync(SKILL_PATH), "Skill installed", existsSync(SKILL_PATH) ? SKILL_PATH : "not found");
1176
+ }
1177
+ const globalPackage = globalPackageCheck(readGlobalCliInstall());
1178
+ const globalVersion = globalPackage.commands?.version ?? "";
1179
+ check(globalPackage.ok, "Global package installed", globalPackage.detail);
1180
+ if (globalPackage.commands && globalVersion) {
1181
+ const latest = await fetchLatestPublishedVersion({ packageName: globalPackage.commands.name });
1182
+ if (latest) {
1183
+ const freshness = globalPackageFreshness(globalVersion, latest);
1184
+ check(freshness.upToDate, "Global package up to date", freshness.detail);
1185
+ }
1186
+ }
1187
+ const autoUpdateStatePath = autoUpdateState();
1188
+ const autoUpdateAnnouncement = takeAutoUpdateAnnouncement(autoUpdateStatePath, true);
1189
+ if (autoUpdateAnnouncement) console.log(` ${pass} ${describeAutoUpdateAnnouncement(autoUpdateAnnouncement)}`);
1190
+ for (const note of describeAutoUpdateStatus({ env: process.env, platform: process.platform, state: readAutoUpdateState(autoUpdateStatePath) })) {
1191
+ console.log(` ${note.tone === "warn" ? warn : dim("\u2013")} ${note.label} ${dim(`(${note.detail})`)}`);
1192
+ }
1193
+ console.log();
1194
+ console.log(` ${dim("Remote control")}`);
1195
+ const aliasRc = SHELL_FILES.find((f) => {
1196
+ try {
1197
+ return /alias\s+claude=['"]?pushary claude/.test(readFileSync(f, "utf-8"));
1198
+ } catch {
1199
+ return false;
1200
+ }
1201
+ });
1202
+ if (aliasRc) {
1203
+ if (globalVersion) {
1204
+ check(true, "claude alias \u2192 pushary claude", `set in ${aliasRc.split("/").pop()}`);
1205
+ } else {
1206
+ check(false, "claude alias \u2192 pushary claude", "alias is set but pushary is not installed \u2014 run `unalias claude` (this shell) or reinstall");
1207
+ }
1208
+ } else {
1209
+ console.log(` ${dim("\u2013")} claude alias not set ${dim("(optional \u2014 run `pushary claude` directly, or re-run setup to add it)")}`);
1210
+ }
1211
+ if (apiKey) {
1212
+ try {
1213
+ const modeRes = await fetch(`${getBaseUrl()}/api/mcp/mode`, {
1214
+ headers: { Authorization: `Bearer ${apiKey}` },
1215
+ signal: AbortSignal.timeout(8e3)
1216
+ });
1217
+ const relayUrl = modeRes.ok ? (await modeRes.json()).relayUrl : void 0;
1218
+ if (typeof relayUrl === "string" && relayUrl.length > 0) {
1219
+ const healthUrl = relayUrl.replace(/^wss:/, "https:").replace(/\/relay$/, "/health/ready");
1220
+ try {
1221
+ const h = await fetch(healthUrl, { signal: AbortSignal.timeout(6e3) });
1222
+ console.log(h.ok ? ` ${pass} Relay reachable ${dim("(sub-second phone commands)")}` : ` ${dim("\u2013")} Relay advertised but returned ${h.status} ${dim("(commands still delivered via polling)")}`);
1223
+ } catch {
1224
+ console.log(` ${dim("\u2013")} Relay advertised but unreachable ${dim("(commands still delivered via polling)")}`);
1225
+ }
1226
+ } else {
1227
+ console.log(` ${dim("\u2013")} Relay not enabled ${dim("(phone commands delivered via polling)")}`);
1228
+ }
1229
+ } catch {
1230
+ }
1231
+ }
1232
+ console.log();
1233
+ console.log(` ${dim("Connectivity")}`);
1234
+ if (!apiKey) {
1235
+ check(false, "MCP server reachable", "skipped \u2014 no API key");
1236
+ check(false, "API key valid", "skipped");
1237
+ check(false, "MCP handshake", "skipped");
1238
+ const keylessSummary = readLedgerSummary(7);
1239
+ if (keylessSummary.count > 0) {
1240
+ check(false, "Keyless approval moments (7d)", `${keylessSummary.count} counted locally \u2014 npx @pushary/agent-hooks@latest stats`);
1241
+ }
1242
+ } else {
1243
+ let sessionId = "";
1244
+ try {
1245
+ const initResult = await sendMcpRequest(apiKey, {
1246
+ jsonrpc: "2.0",
1247
+ id: 1,
1248
+ method: "initialize",
1249
+ params: {
1250
+ protocolVersion: "2025-03-26",
1251
+ capabilities: {},
1252
+ clientInfo: { name: "pushary-doctor", version: "1.0" }
1253
+ }
1254
+ }, { timeoutMs: 1e4 });
1255
+ check(true, "MCP server reachable", `${initResult.status} ${initResult.statusText}`);
1256
+ sessionId = initResult.sessionId;
1257
+ if (sessionId) {
1258
+ console.log(` ${pass} Session ID returned ${dim(`(${sessionId.slice(0, 8)}...)`)}`);
1259
+ } else {
1260
+ console.log(` ${dim("\u2013")} Stateless mode ${dim("(no session ID)")}`);
1261
+ }
1262
+ check(!!initResult.data.result?.serverInfo, "API key valid", initResult.data.result?.serverInfo?.name ?? "unknown server");
1263
+ const toolsResult = await sendMcpRequest(apiKey, {
1264
+ jsonrpc: "2.0",
1265
+ id: 2,
1266
+ method: "tools/list",
1267
+ params: {}
1268
+ }, {
1269
+ sessionId,
1270
+ timeoutMs: 1e4
1271
+ });
1272
+ const toolCount = toolsResult.data.result?.tools?.length ?? 0;
1273
+ check(toolCount > 0, "MCP tools discovered", `${toolCount} tools`);
1274
+ } catch (err) {
1275
+ const msg = err instanceof Error ? err.message : "unknown error";
1276
+ check(false, "MCP server reachable", msg);
1277
+ }
1278
+ console.log();
1279
+ console.log(` ${dim("Notification Delivery")}`);
1280
+ const probe = await fetchChannels(apiKey);
1281
+ if (probe.kind !== "ok") {
1282
+ check(
1283
+ false,
1284
+ "Phone reachable",
1285
+ 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",
1286
+ probe.kind === "unreachable" ? "unreachable" : "config"
1287
+ );
1288
+ } else if (probe.status.precise) {
1289
+ const channels = probe.status;
1290
+ const verdict = reachVerdict(channels);
1291
+ check(
1292
+ verdict.kind === "yours",
1293
+ "Phone reachable",
1294
+ 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`,
1295
+ "reachability"
1296
+ );
1297
+ }
1298
+ const pushSuppressed = !options.roundtrip && (options.noPush || options.json || !process.stdout.isTTY);
1299
+ if (pushSuppressed) {
1300
+ const why = options.noPush ? "--no-push" : options.json ? "--json" : "not a terminal";
1301
+ console.log(` ${dim("\u2013")} Test push skipped ${dim(`(${why}; pass --roundtrip to send one)`)}`);
1302
+ } else try {
1303
+ const result = await callMcpTool(apiKey, "send_notification", {
1304
+ title: "Pushary Doctor",
1305
+ body: "If you see this, push notifications are working.",
1306
+ agentName: "Pushary Doctor"
1307
+ }, { sessionId, timeoutMs: 2e4 });
1308
+ const sent = typeof result.sent === "number" ? result.sent : (result.delivery?.mobile?.recipients ?? 0) + (result.delivery?.web?.recipients ?? 0);
1309
+ check(sent > 0, "Push notification sent", sent > 0 ? `${sent} device${sent === 1 ? "" : "s"} \u2014 check your phone` : "reached 0 devices");
1310
+ } catch (err) {
1311
+ const msg = err instanceof Error ? err.message : "network error";
1312
+ check(false, "Push notification sent", msg);
1313
+ }
1314
+ const roundTripPlan = planRoundTrip({
1315
+ roundtrip: options.roundtrip,
1316
+ noPush: options.noPush,
1317
+ json: options.json,
1318
+ stdinIsTty: process.stdin.isTTY === true
1319
+ });
1320
+ const testQuestion = roundTripPlan.kind === "always" ? true : roundTripPlan.kind === "never" ? false : await confirm({
1321
+ message: "Test a synthetic question and answer? (sends a notification; does not test agent hooks)",
1322
+ default: roundTripPlan.defaultAnswer
1323
+ });
1324
+ if (testQuestion) {
1325
+ console.log();
1326
+ console.log(` ${dim("Question Roundtrip")}`);
1327
+ console.log(` ${dim("\u2192")} Question sent, waiting for your answer...`);
1328
+ const outcome = await verifyRoundTrip(apiKey, "Pushary Doctor");
1329
+ roundTrip = outcome;
1330
+ const report = describeRoundTrip(outcome);
1331
+ check(report.ok, "Synthetic answer test", report.message);
1332
+ for (const line of report.next) console.log(` ${dim(line)}`);
1333
+ }
1334
+ }
1335
+ if (apiKey) {
1336
+ try {
1337
+ const res = await fetch(`${getBaseUrl()}/api/mcp/policy`, {
1338
+ headers: { Authorization: `Bearer ${apiKey}` },
1339
+ signal: AbortSignal.timeout(1e4)
1340
+ });
1341
+ if (res.ok) {
1342
+ const policy = await res.json();
1343
+ console.log();
1344
+ console.log(` ${dim("Wait Ladder")} ${dim("(what happens when you don't answer)")}`);
1345
+ const wildcard = policy.policies?.find((p) => p.tool === "*");
1346
+ const defaultWindow = policy.defaultPushFirstSeconds ?? 20;
1347
+ const defaultLadder = describeWaitLadder(wildcard ?? { tool: "*", mode: "push_first", pushFirstSeconds: defaultWindow, timeoutSeconds: 0, timeoutAction: "escalate" });
1348
+ console.log(` ${dim("\xB7")} default: ${cyan(`${defaultWindow}s`)} ${dim("\xB7")} ${defaultLadder}`);
1349
+ if (policy.modeOverride) {
1350
+ console.log(` ${warn} mode override active: ${cyan(policy.modeOverride)} ${dim("(pushary mode status)")}`);
1351
+ }
1352
+ const overrides = (policy.policies ?? []).filter((p) => p.tool !== "*").slice(0, 4);
1353
+ for (const p of overrides) {
1354
+ console.log(` ${dim("\xB7")} ${p.tool}: ${describeWaitLadder(p)}`);
1355
+ }
1356
+ console.log(` ${dim("Tune it:")} ${cyan("pushary wait <seconds>")} ${dim("\xB7 full ladder:")} ${cyan("pushary wait status")}`);
1357
+ }
1358
+ } catch {
1359
+ }
1360
+ }
1361
+ if (options.activation) {
1362
+ activation = apiKey ? await readHookActivation(apiKey, options.activation, options.activationTarget) : { kind: "unavailable", detail: "Sign in before reading this workspace\u2019s hook evidence." };
1363
+ const latest = activation.kind === "ok" ? activation.report.evidence[0] : void 0;
1364
+ hookActivation = hookActivationResult(activation.kind === "ok" ? activation.report : null);
1365
+ const observed = hookActivation === "approved_executed" || hookActivation === "denied_observed";
1366
+ check(observed, "Approval for the selected scratch file (last 10 minutes)", activation.kind !== "ok" ? activation.detail : activation.report.truncated ? "Too many recent decisions to establish a unique correlation. Start a quiet test session and retry." : activation.report.evidence.length > 1 ? `${hookActivation}: multiple decisions for this target. Use a new scratch file for each attempt; inspect hookActivationEvidence for all results.` : latest ? `${latest.result}; answer source: ${latest.answerSource ?? "unknown"}; decision ${latest.correlationId}; session ${latest.sessionId}; tool call ${latest.toolUseId}` : "Unverified: no recent runtime-tagged Write approval for this scratch file on this machine.");
1367
+ const guide = AGENTS[options.activation].activationGuide;
1368
+ console.log(` ${dim(`Real-agent check: https://pushary.com${guide}`)}`);
1369
+ }
1370
+ const managedControl = readManagedControlEvidence(apiKey, configuredManagedProviders, serverProof);
1371
+ console.log(` ${dim(`Phone-start and steering: ${managedControl.ready ? "local and server checks passed for this run" : "unverified; hook approvals are a separate capability"}.`)}`);
1372
+ console.log();
1373
+ if (!options.activation) console.log(` ${dim("Agent hook activation is unverified by these checks. Use --activation claude_code --activation-target <unique scratch file> after one real approval.")}`);
1374
+ const failed = results.filter((r) => !r.passed);
1375
+ if (failed.length === 0) {
1376
+ console.log(` ${green(bold("All checks passed."))}`);
1377
+ } else {
1378
+ console.log(` ${red(bold(`${failed.length} check${failed.length === 1 ? "" : "s"} failed:`))}`);
1379
+ for (const f of failed) {
1380
+ console.log(` ${fail} ${f.label}${f.detail ? ` \u2014 ${f.detail}` : ""}`);
1381
+ }
1382
+ console.log();
1383
+ console.log(` ${dim("Run")} ${cyan("npx @pushary/agent-hooks@latest clean")} ${dim("then")} ${cyan("setup")} ${dim("to fix.")}`);
1384
+ console.log(` ${dim("Still failing after that? Send this report to")} ${cyan(SUPPORT_EMAIL)} ${dim("or ask on")} ${cyan(SUPPORT_DISCORD)}`);
1385
+ }
1386
+ console.log();
1387
+ const keyExit = keyVerdict && (keyVerdict.kind === "rejected" || keyVerdict.kind === "locked" || keyVerdict.kind === "malformed") ? describeKeyCheck(keyVerdict).exitCode : null;
1388
+ const exitCode = resolveDoctorExit({ keyExit, failures: failed });
1389
+ if (options.bundle) {
1390
+ try {
1391
+ const bundle = redact(
1392
+ await collectBundle({
1393
+ identity: keyVerdict?.kind === "ok" ? keyVerdict.identity : null,
1394
+ checks: results.map((r) => ({ id: r.label, ok: r.passed, detail: r.detail ?? "" }))
1395
+ })
1396
+ );
1397
+ const target = join(process.cwd(), bundleFileName(bundle));
1398
+ writeFileSync(target, `${JSON.stringify(bundle, null, 2)}
1399
+ `, "utf-8");
1400
+ const say = options.json ? process.stderr : process.stdout;
1401
+ say.write(`
1402
+ Diagnostics written to ${target}
1403
+ `);
1404
+ say.write(" Keys, your home directory and this machine name are redacted. Read it before sharing.\n\n");
1405
+ } catch (err) {
1406
+ process.stderr.write(` ! could not write the diagnostics file: ${err instanceof Error ? err.message : String(err)}
1407
+ `);
1408
+ }
1409
+ }
1410
+ if (options.json) {
1411
+ await new Promise((resolve2, reject) => process.stdout.write(`${JSON.stringify({
1412
+ ok: exitCode === EXIT.OK,
1413
+ exitCode,
1414
+ keyCheck: keyVerdict?.kind ?? null,
1415
+ roundTripEvidence: roundTripEvidence(roundTrip),
1416
+ hookActivation,
1417
+ hookActivationEvidence: activation,
1418
+ daemon,
1419
+ managedControl,
1420
+ daemonNeedsLogin: loginBlock.kind === "blocked" ? loginBlock.record : null,
1421
+ passed: results.filter((r) => r.passed).length,
1422
+ failed: failed.length,
1423
+ checks: results.map((r) => ({ label: r.label, passed: r.passed, detail: r.detail ?? null })),
1424
+ otherHolders: otherHoldersByAgent
1425
+ }, null, 2)}
1426
+ `, (error) => error ? reject(error) : resolve2()));
1427
+ }
1428
+ process.exit(exitCode);
1429
+ };
1430
+ main().catch((err) => {
1431
+ console.error(` doctor failed: ${err instanceof Error ? err.message : String(err)}`);
1432
+ process.exit(1);
1433
+ });