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,2660 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ codexTrustOutcome,
4
+ describeCodexTrustConfirmation
5
+ } from "../chunk-TY4JT7W3.js";
6
+ import {
7
+ runBrowserLogin
8
+ } from "../chunk-JYG4B33G.js";
9
+ import {
10
+ coworkOutput,
11
+ coworkSelection
12
+ } from "../chunk-7Z7Q4GGS.js";
13
+ import "../chunk-PUZZELE6.js";
14
+ import {
15
+ detectClaudeVersion
16
+ } from "../chunk-SIFVFP6T.js";
17
+ import {
18
+ describeVersionManagerPin,
19
+ versionManagerOf
20
+ } from "../chunk-GU3EJCVV.js";
21
+ import {
22
+ ensureCodexTrustsPusharyHooks
23
+ } from "../chunk-RHR6NARM.js";
24
+ import {
25
+ readManagedControlEvidence,
26
+ waitForMachineReadiness
27
+ } from "../chunk-DE4Y6TCC.js";
28
+ import {
29
+ describeSetupSideEffects,
30
+ installAgentSkill,
31
+ installSkillViaSkillsCli,
32
+ registerCursorGate
33
+ } from "../chunk-KVQBWI5T.js";
34
+ import {
35
+ AgentConfigError,
36
+ FX_PUSHARY_TOOLS_RULE,
37
+ FX_QUESTION_TOOL,
38
+ FX_TOOL_DISCOVERY_NOTE,
39
+ HERMES_PIP_PACKAGE,
40
+ SHARED_AGENTS_MD_LABEL,
41
+ afterAgentFailureNote,
42
+ agentFailureFromError,
43
+ agentFailureWire,
44
+ configFailureKind,
45
+ configureFxSettings,
46
+ describeDetection,
47
+ describedAgentFailure,
48
+ detectAllAgents,
49
+ failedAgentsContext,
50
+ fxIgnoresInstructionsFile,
51
+ fxPusharyEntry,
52
+ inspectFxInstallation,
53
+ isDetected,
54
+ isHermesConfigReceipt,
55
+ pluginLocationSnippet,
56
+ readFxProfileForSetup,
57
+ registerPluginLocation,
58
+ renderAgentInstructions,
59
+ renderProjectAgentInstructions,
60
+ resolveHermesPython,
61
+ retryAgentCommand,
62
+ runHermesConfigScript,
63
+ shortenHome,
64
+ vscodeSettingsTargets,
65
+ withFxPusharyServer,
66
+ writeFxProfile,
67
+ writeInstructionBlock
68
+ } from "../chunk-MDEPU45E.js";
69
+ import {
70
+ MCP_BRIDGE_BINARY
71
+ } from "../chunk-SLFABHW3.js";
72
+ import {
73
+ OPENCODE_HOOK_BINARY,
74
+ addOpenCodeMcpServer,
75
+ installOpenCodePlugin,
76
+ openCodeConfigDir,
77
+ openCodeConfigFile,
78
+ openCodeInstructionsFile,
79
+ openCodePluginDir,
80
+ openCodePluginFile,
81
+ openCodeWired
82
+ } from "../chunk-AHV4LNB5.js";
83
+ import {
84
+ addCompatibleHooks,
85
+ addCompatibleMcpServer,
86
+ addCompatibleToolPermissions,
87
+ compatibleConfigFile,
88
+ compatibleHookEvents,
89
+ compatibleInstructionsFile,
90
+ compatibleMcpFile,
91
+ compatiblePermissionRuleFile,
92
+ hasCompatibleHooks
93
+ } from "../chunk-J5TIV3J6.js";
94
+ import {
95
+ addClaudeMcpServer,
96
+ addPusharyHooks,
97
+ addPusharyToolPermissions,
98
+ captureClaudeMcpServer,
99
+ isClaudeMcpSnapshot,
100
+ updateClaudeMcpSnapshot
101
+ } from "../chunk-AXPCESYO.js";
102
+ import {
103
+ detectProviderLiveCapabilities,
104
+ hasSpawnProvider,
105
+ resolveControlPolicy
106
+ } from "../chunk-HQ6G3B5R.js";
107
+ import {
108
+ describeAutoUpdateAnnouncement,
109
+ takeAutoUpdateAnnouncement
110
+ } from "../chunk-UI36QSBI.js";
111
+ import {
112
+ KEEP_HINT,
113
+ captureHookTakeover,
114
+ captureMcpServer,
115
+ describeAppOwnership,
116
+ describeHookOwnership,
117
+ hookOwnerOf,
118
+ mcpBearerKey,
119
+ planHooks
120
+ } from "../chunk-EYK3GSRH.js";
121
+ import {
122
+ AGENT_CHOICES,
123
+ effectiveControlMode,
124
+ effectiveVerify,
125
+ parseSetupOptions
126
+ } from "../chunk-ZVSKMPBZ.js";
127
+ import {
128
+ liveBridgeProbe,
129
+ resolveBridgeStub
130
+ } from "../chunk-C2WCPBF7.js";
131
+ import {
132
+ claudeWired,
133
+ codexWired,
134
+ geminiWired,
135
+ pusharyHookExecutable
136
+ } from "../chunk-6J2JC2RT.js";
137
+ import {
138
+ clearDaemonNeedsLogin,
139
+ daemonHasRemoteProof,
140
+ daemonServiceDefinitionPath,
141
+ daemonServiceManagerAvailable,
142
+ disableDaemonService,
143
+ ensureDaemonRunning,
144
+ hasDaemonServiceDefinition,
145
+ readDaemonStatus,
146
+ replaceDaemonService,
147
+ stopDaemonService,
148
+ waitForDaemonRemoteProof
149
+ } from "../chunk-WE2J62NC.js";
150
+ import {
151
+ GEMINI_HOOK_BINARY,
152
+ addGeminiHooks,
153
+ addGeminiMcpServer,
154
+ compareVersion,
155
+ parseVersion
156
+ } from "../chunk-3EVPNIBE.js";
157
+ import "../chunk-OKXS7WDZ.js";
158
+ import {
159
+ transcriptPreferenceForSetup
160
+ } from "../chunk-OLQRRFBX.js";
161
+ import "../chunk-XHKBHWLX.js";
162
+ import {
163
+ reportEvent
164
+ } from "../chunk-CULUZTWQ.js";
165
+ import "../chunk-N6IOSZOE.js";
166
+ import "../chunk-EPCXUP3P.js";
167
+ import "../chunk-NGDD6TXY.js";
168
+ import "../chunk-CVQYGMSR.js";
169
+ import "../chunk-POFBE6ZY.js";
170
+ import {
171
+ BROWSER_CONNECT_COMMAND,
172
+ confirmAppConnection,
173
+ connectDevice,
174
+ connectViaAppPairing,
175
+ describeRoundTrip,
176
+ pairingEscapeApplies,
177
+ printConnectInstructions,
178
+ roundTripEvidence,
179
+ verifyRoundTrip
180
+ } from "../chunk-M5JDZ45W.js";
181
+ import "../chunk-3EGEA4KH.js";
182
+ import {
183
+ setHumanStream
184
+ } from "../chunk-QCGNGEGE.js";
185
+ import {
186
+ ansiPainter,
187
+ createIo,
188
+ useColor
189
+ } from "../chunk-U4TMAANR.js";
190
+ import "../chunk-GMXKITVA.js";
191
+ import {
192
+ KEY_FILE_MODE,
193
+ exportLine,
194
+ installClaudeAlias,
195
+ keyPrefix,
196
+ readConfigFileKey,
197
+ readControlMode,
198
+ readKeySource,
199
+ resolveShellRc,
200
+ writeKey
201
+ } from "../chunk-P7WQEVWA.js";
202
+ import "../chunk-M5ICYGHT.js";
203
+ import {
204
+ checkApiKey,
205
+ describeKeyCheck,
206
+ samePusharyAccount
207
+ } from "../chunk-2ABTSGFT.js";
208
+ import "../chunk-2OY3ZZ5N.js";
209
+ import "../chunk-LXQ5FBBD.js";
210
+ import {
211
+ UsageError
212
+ } from "../chunk-7QLSKOSU.js";
213
+ import {
214
+ EXIT
215
+ } from "../chunk-KZERVKTD.js";
216
+ import "../chunk-DB3ONZB4.js";
217
+ import {
218
+ NpmMissingError,
219
+ addCodexHookTrust,
220
+ addCodexHooks,
221
+ addCodexMcpServer,
222
+ autoUpdateState,
223
+ captureCodexHookTrust,
224
+ claudeJson,
225
+ claudeMcpReceipt,
226
+ claudeSettings,
227
+ claudeSkillDir,
228
+ codexAgentsMd2 as codexAgentsMd,
229
+ codexConfigToml,
230
+ codexHome,
231
+ codexHookPositions,
232
+ codexSkillDir2 as codexSkillDir,
233
+ codexVacatedIndexes,
234
+ cursorPluginDir,
235
+ cursorUserHooks,
236
+ cursorUserMcp,
237
+ dropVacatedCodexHookTrust,
238
+ fetchLatestPublishedVersion,
239
+ fxAgentsMd,
240
+ fxConfigDir,
241
+ fxMcpProfile,
242
+ fxQuestionRuleReceipt,
243
+ fxSettings,
244
+ geminiMd,
245
+ geminiSettings,
246
+ guardBinary,
247
+ guardCursorGate,
248
+ guardedArgv,
249
+ hermesConfigReceipt,
250
+ hookTakeoverReceipt,
251
+ installGlobally,
252
+ npmErrorMessage,
253
+ plannedGlobalPackage,
254
+ pusharyConfigFile,
255
+ pusharyDir,
256
+ readCodexMcpAuth,
257
+ resolveGlobalBinDir,
258
+ resolveGlobalBinary,
259
+ vscodePluginDir,
260
+ vscodePluginMcp
261
+ } from "../chunk-A2A2YTMG.js";
262
+ import {
263
+ backupFile,
264
+ readJsonSafe,
265
+ writeFileAtomic,
266
+ writeJsonAtomic
267
+ } from "../chunk-T7E75EKA.js";
268
+ import "../chunk-GPEGOGRJ.js";
269
+ import {
270
+ NPX_LATEST_COMMAND,
271
+ renderCommandHelp
272
+ } from "../chunk-E4FJTUH4.js";
273
+ import {
274
+ HOOK_COMPATIBLE_PROFILES,
275
+ HOOK_LOCATOR_BINARY,
276
+ PRETOOLUSE_GATED_TOOLS,
277
+ SUPPORT_EMAIL,
278
+ compatibleTargetFiles,
279
+ getPackageVersion,
280
+ hookCompatibleProfile,
281
+ isNewerVersion,
282
+ isValidApiKey,
283
+ launchedPackage,
284
+ ownPackageName
285
+ } from "../chunk-NQTHFF4W.js";
286
+
287
+ // bin/pushary-setup.ts
288
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, cpSync, rmSync, renameSync, realpathSync, readdirSync, statSync, openSync, readSync, closeSync } from "fs";
289
+ import { join as join3, dirname as dirname2, basename } from "path";
290
+ import { homedir } from "os";
291
+ import { execSync } from "child_process";
292
+ import { checkbox, input, confirm } from "@inquirer/prompts";
293
+ import { fileURLToPath } from "url";
294
+ import { parse as parseTOML, stringify as stringifyTOML } from "smol-toml";
295
+
296
+ // src/cli/exit.ts
297
+ var isExitPromptError = (err) => err instanceof Error && (err.name === "ExitPromptError" || err.message.includes("User force closed") || /force closed the prompt/i.test(err.message));
298
+
299
+ // src/setup/phone-check.ts
300
+ var planPhoneCheck = (context) => {
301
+ if (context.locked) return "locked";
302
+ if (context.skipPhone) return "skip-phone";
303
+ if (context.verify === "none") return "verify-none";
304
+ if (!context.stdoutIsTty) return "instructions";
305
+ if (context.paired) return "confirm-pairing";
306
+ if (context.connectMode === "app") return "reuse-app";
307
+ return "connect";
308
+ };
309
+
310
+ // src/setup/closing.ts
311
+ var ACTIVITY_URL = "pushary.com/dashboard/agent/activity";
312
+ var COMMAND_SURFACE = [
313
+ { command: "pushary status", summary: "what this machine is connected to" },
314
+ { command: "pushary mode", summary: "choose where approval requests appear" },
315
+ { command: "pushary wait", summary: "block until a pending question is answered" },
316
+ { command: "pushary stats", summary: "what your agents have been asking" },
317
+ { command: "pushary claude", summary: "run Claude Code with encrypted transcript support" },
318
+ { command: "pushary doctor", summary: "check the install" }
319
+ ];
320
+ var SURFACE_COLUMN = Math.max(...COMMAND_SURFACE.map((entry) => entry.command.length)) + 2;
321
+ var renderCommandSurface = () => COMMAND_SURFACE.map((entry) => `${entry.command.padEnd(SURFACE_COLUMN)}${entry.summary}`);
322
+ var nextSteps = (facts, reachable) => {
323
+ const lines = ["Delivery follows the current approval mode, recipient rules and presence. pushary mode status shows the workspace setting.", facts.managedControlReady === true ? "Phone-start and steering: local and server checks passed just now. Recheck after sleep or network changes." : "Phone-start and steering: unverified. Run pushary doctor to check the provider, background service and server route."];
324
+ if (facts.staleEnvironmentKey) {
325
+ lines.push("Open a new terminal or unset PUSHARY_API_KEY, then run pushary doctor before starting an agent.");
326
+ }
327
+ if (facts.transcriptSync === false) {
328
+ lines.push("Transcripts: disabled on this machine.");
329
+ lines.push("pushary setup --transcripts on enable encrypted transcript sync");
330
+ } else {
331
+ lines.push("Transcripts: Pushary-launched Claude, Codex and Gemini sessions sync redacted, encrypted content. Your phone can decrypt it; authorized Pushary compliance admins can recover it for documented legal obligations.");
332
+ lines.push("pushary setup --transcripts off disable transcript sync on this machine");
333
+ }
334
+ if (facts.completed.includes("claude_code")) {
335
+ lines.push("pushary claude launch a managed Claude Code session");
336
+ }
337
+ if (!reachable) {
338
+ lines.push("pushary connect connect a phone without re-running setup");
339
+ }
340
+ lines.push("pushary doctor re-check everything");
341
+ return lines;
342
+ };
343
+ var describeClosing = (facts) => {
344
+ const reachable = facts.reachChecked && facts.reachesYou;
345
+ if (facts.configuredNothing) {
346
+ return {
347
+ ok: false,
348
+ headline: "Setup failed.",
349
+ next: ["Nothing was configured. Run pushary doctor to see why."],
350
+ exitCode: EXIT.FAILED,
351
+ reason: "configured-nothing"
352
+ };
353
+ }
354
+ if (facts.failed.length > 0) {
355
+ const count = facts.failed.length;
356
+ return {
357
+ ok: false,
358
+ headline: `Setup finished with ${count} problem${count === 1 ? "" : "s"}.`,
359
+ next: [
360
+ ...nextSteps(facts, reachable),
361
+ "",
362
+ `Approvals: ${ACTIVITY_URL}`
363
+ ],
364
+ exitCode: EXIT.PROBLEMS_FOUND,
365
+ reason: "agents-failed"
366
+ };
367
+ }
368
+ if (facts.staleEnvironmentKey) {
369
+ return {
370
+ ok: false,
371
+ headline: "Agents configured. This terminal still selects the previous key.",
372
+ next: nextSteps(facts, reachable),
373
+ exitCode: EXIT.PROBLEMS_FOUND,
374
+ reason: "stale-environment-key"
375
+ };
376
+ }
377
+ if (facts.daemonReady === false) {
378
+ return {
379
+ ok: false,
380
+ headline: "Setup finished with 1 problem.",
381
+ next: [
382
+ "Agent configuration and managed control are separate. Phone-start and steering could not be verified.",
383
+ "pushary daemon install install or repair the background service",
384
+ "pushary doctor verify it became reachable"
385
+ ],
386
+ exitCode: EXIT.PROBLEMS_FOUND,
387
+ reason: "control-unready"
388
+ };
389
+ }
390
+ if (facts.roundTripAnswered === true) {
391
+ return {
392
+ ok: true,
393
+ headline: "Agents configured. Synthetic answer returned.",
394
+ next: ["Agent hook activation remains unverified.", ...nextSteps(facts, reachable), "", `Approvals: ${ACTIVITY_URL}`],
395
+ exitCode: EXIT.OK,
396
+ reason: "synthetic-answer"
397
+ };
398
+ }
399
+ if (facts.reachChecked && !facts.reachesYou) {
400
+ return {
401
+ ok: false,
402
+ headline: "Agents configured. Phone delivery is unverified.",
403
+ next: [
404
+ "Approval routing depends on your mode, presence and recipients. Check the phone and its notification settings.",
405
+ "",
406
+ ...nextSteps(facts, false)
407
+ ],
408
+ exitCode: EXIT.NO_DEVICE,
409
+ reason: "phone-unverified"
410
+ };
411
+ }
412
+ if (facts.roundTripAnswered === false) {
413
+ return {
414
+ ok: false,
415
+ headline: "Agents configured. Nobody answered the test question.",
416
+ next: [
417
+ "No answer returned from the synthetic test. Delivery acceptance does not prove a visible notification.",
418
+ "",
419
+ "Check the notification opened something you could tap, then:",
420
+ "pushary doctor --roundtrip send another one",
421
+ "",
422
+ "If you simply were not looking, re-run setup or pass --verify push to skip this check."
423
+ ],
424
+ exitCode: EXIT.PROBLEMS_FOUND,
425
+ reason: "roundtrip-unanswered"
426
+ };
427
+ }
428
+ if (!facts.reachChecked) {
429
+ return {
430
+ ok: true,
431
+ headline: "Setup complete.",
432
+ next: [
433
+ "The phone check was skipped, so nothing here proves an approval reaches you.",
434
+ "",
435
+ ...nextSteps(facts, false)
436
+ ],
437
+ exitCode: EXIT.OK,
438
+ reason: "phone-skipped"
439
+ };
440
+ }
441
+ return {
442
+ ok: true,
443
+ headline: "Setup complete.",
444
+ next: [...nextSteps(facts, true), "", `Approvals: ${ACTIVITY_URL}`],
445
+ exitCode: EXIT.OK,
446
+ reason: "complete"
447
+ };
448
+ };
449
+ var RETRY_AGENT_LABEL = "Retry just this agent:";
450
+ var INSTALL_NODE_STEP = "Install the current Node.js LTS, which includes npm, then run setup again.";
451
+ var nextStepFor = (failure) => failure.remedy === "install-node" ? { kind: "install-node", instruction: INSTALL_NODE_STEP } : { kind: "retry-agent", command: retryAgentCommand(failure.agent) };
452
+ var describeAgentFailures = (failures, agentName) => failures.map((failure) => ({
453
+ summary: `${agentName(failure.agent)}: ${failure.detail}`,
454
+ next: nextStepFor(failure)
455
+ }));
456
+ var agentFailureFollowUps = (failures) => [
457
+ ...failures.some((failure) => failure.remedy === "install-node") ? ["install-node"] : [],
458
+ ...failures.some((failure) => failure.remedy === "retry-agent") ? ["run-doctor"] : []
459
+ ];
460
+ var unverifiedKeyClosingReason = (facts) => {
461
+ if (facts.failedCount > 0) return "agents-failed";
462
+ if (facts.controlLifecycleFailed) return "control-unready";
463
+ return facts.keyCheck === "locked" ? "plan-inactive" : "key-unverified";
464
+ };
465
+
466
+ // src/setup/codex-version.ts
467
+ import { dirname, join } from "path";
468
+ var parseCodexVersion = parseVersion;
469
+ var compareCodexVersion = compareVersion;
470
+ var versionFromManifest = (probe, binPath) => {
471
+ const readVersion = (path) => {
472
+ const raw = probe.readFile(path);
473
+ if (!raw) return null;
474
+ try {
475
+ const { version } = JSON.parse(raw);
476
+ return typeof version === "string" ? parseCodexVersion(version) : null;
477
+ } catch {
478
+ return null;
479
+ }
480
+ };
481
+ let dir = dirname(binPath);
482
+ const shimVersion = readVersion(join(dir, "node_modules", "@openai", "codex", "package.json"));
483
+ if (shimVersion) return shimVersion;
484
+ for (let depth = 0; depth < 4; depth++) {
485
+ const version = readVersion(join(dir, "package.json"));
486
+ if (version) return version;
487
+ const parent = dirname(dir);
488
+ if (parent === dir) break;
489
+ dir = parent;
490
+ }
491
+ return null;
492
+ };
493
+ var readCodexVersion = (probe) => {
494
+ const onPath = probe.resolveOnPath();
495
+ if (onPath) {
496
+ const fromManifest = versionFromManifest(probe, probe.realpath(onPath) ?? onPath);
497
+ if (fromManifest) return fromManifest;
498
+ }
499
+ const brew = probe.brewVersions();
500
+ if (brew) {
501
+ const parsed = parseCodexVersion(brew);
502
+ if (parsed) return parsed;
503
+ }
504
+ const bundled = probe.bundledVersion?.();
505
+ return bundled ? parseCodexVersion(bundled) : null;
506
+ };
507
+
508
+ // src/setup/codex-install.ts
509
+ var NATIVE_BINARY_MIN_BYTES = 20 * 1024 * 1024;
510
+ var packageRootFor = (probe, binPath) => {
511
+ let dir = probe.dirname(binPath);
512
+ for (let depth = 0; depth < 4; depth++) {
513
+ if (probe.exists(probe.join(dir, "package.json"))) return dir;
514
+ const parent = probe.dirname(dir);
515
+ if (parent === dir) return null;
516
+ dir = parent;
517
+ }
518
+ return null;
519
+ };
520
+ var holdsNativeBinary = (probe, dir, depth = 0) => {
521
+ if (depth > 4) return false;
522
+ const entries = probe.listDir(dir);
523
+ if (!entries) return false;
524
+ return entries.some((entry) => {
525
+ const path = probe.join(dir, entry);
526
+ const size = probe.fileSize(path);
527
+ if (size !== null) return size >= NATIVE_BINARY_MIN_BYTES;
528
+ return holdsNativeBinary(probe, path, depth + 1);
529
+ });
530
+ };
531
+ var platformPackages = (probe, packageRoot) => {
532
+ const scopes = [probe.join(packageRoot, "node_modules", "@openai"), probe.dirname(packageRoot)];
533
+ return scopes.flatMap(
534
+ (scope) => (probe.listDir(scope) ?? []).filter((entry) => entry.startsWith("codex-")).map((entry) => probe.join(scope, entry))
535
+ );
536
+ };
537
+ var checkCodexInstall = (probe) => {
538
+ const onPath = probe.resolveOnPath();
539
+ if (!onPath) return { kind: "unknown" };
540
+ const packageRoot = packageRootFor(probe, probe.realpath(onPath) ?? onPath);
541
+ if (!packageRoot) return { kind: "unknown" };
542
+ const vendors = platformPackages(probe, packageRoot).map((pkg) => probe.join(pkg, "vendor")).filter((vendor) => probe.exists(vendor));
543
+ if (vendors.length === 0) return { kind: "unknown" };
544
+ if (vendors.some((vendor) => holdsNativeBinary(probe, vendor))) return { kind: "ok" };
545
+ return { kind: "binary-missing", vendorDir: vendors[0] };
546
+ };
547
+
548
+ // src/setup/codex-bundled.ts
549
+ import { join as join2 } from "path";
550
+ var asRecord = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
551
+ var codexCliPathFromConfig = (config) => {
552
+ const servers = asRecord(config.mcp_servers);
553
+ if (!servers) return null;
554
+ for (const server of Object.values(servers)) {
555
+ const declared = asRecord(asRecord(server)?.env)?.CODEX_CLI_PATH;
556
+ if (typeof declared === "string" && declared.trim()) return declared.trim();
557
+ }
558
+ return null;
559
+ };
560
+ var bundledCodexCandidates = (home, platform) => platform === "darwin" ? [
561
+ "/Applications/ChatGPT.app/Contents/Resources/codex",
562
+ join2(home, "Applications", "ChatGPT.app", "Contents", "Resources", "codex")
563
+ ] : [];
564
+ var findBundledCodex = (probe) => {
565
+ const config = probe.config();
566
+ const declared = config ? codexCliPathFromConfig(config) : null;
567
+ if (declared && probe.exists(declared)) return declared;
568
+ return bundledCodexCandidates(probe.home, probe.platform).find(probe.exists) ?? null;
569
+ };
570
+ var parseEmbeddedCodexVersion = (haystack) => haystack.match(/cli_version(\d+\.\d+\.\d+)/)?.[1] ?? null;
571
+
572
+ // src/setup/telemetry.ts
573
+ var roundTripOutcome = (outcome) => {
574
+ switch (outcome?.kind) {
575
+ case "answered":
576
+ return "answered";
577
+ case "notify-only":
578
+ return "delivered";
579
+ case "unanswered":
580
+ return "delivered";
581
+ case "no-recipients":
582
+ return "unreachable";
583
+ default:
584
+ return null;
585
+ }
586
+ };
587
+ var currentStep = "start";
588
+ var markStep = (step) => {
589
+ currentStep = step;
590
+ };
591
+ var recordedOptions = null;
592
+ var recordSetupOptions = (options) => {
593
+ recordedOptions = options;
594
+ };
595
+ var recordedKey = null;
596
+ var recordSetupKey = (apiKey) => {
597
+ recordedKey = apiKey;
598
+ };
599
+ var recordedCodexTrust = null;
600
+ var recordCodexTrust = (outcome) => {
601
+ recordedCodexTrust = outcome;
602
+ };
603
+ var emitted = false;
604
+ var SETUP_COMPLETED_EVENT = "agent_setup_completed";
605
+ var SETUP_AGENT_TYPE = "setup";
606
+ var defaultDeps = { report: reportEvent };
607
+ var completeBeacon = (run) => ({
608
+ ...run,
609
+ codexTrust: recordedCodexTrust,
610
+ cliVersion: getPackageVersion()
611
+ });
612
+ var emit = async (deps, run) => {
613
+ if (emitted) return;
614
+ emitted = true;
615
+ try {
616
+ await deps.report(
617
+ { event: SETUP_COMPLETED_EVENT, agentType: SETUP_AGENT_TYPE, ...completeBeacon(run) },
618
+ { maxAttempts: 1, timeoutMs: 2e3, ...recordedKey ? { apiKey: recordedKey } : {} }
619
+ );
620
+ } catch {
621
+ }
622
+ };
623
+ var reportSetupOutcomeWith = async (deps, outcome) => emit(deps, {
624
+ agents: outcome.completed,
625
+ connectMode: outcome.options.connectMode,
626
+ connectOutcome: outcome.connectOutcome,
627
+ failedAgents: outcome.failures.length,
628
+ agentFailures: agentFailureWire(outcome.failures),
629
+ closingReason: outcome.closingReason,
630
+ skippedAgents: outcome.skipped,
631
+ nonInteractive: outcome.options.nonInteractive,
632
+ keyCheck: outcome.keyCheck,
633
+ exitCode: outcome.exitCode,
634
+ // Where the run stopped. On a completed run this is the closing step, so the
635
+ // interesting values are the ones that are not.
636
+ abandonedAt: outcome.exitCode === 0 ? null : currentStep,
637
+ // Always present, so a query can separate "ended" from "gave up" on the
638
+ // property rather than on the absence of one.
639
+ abortReason: null
640
+ });
641
+ var reportSetupOutcome = (outcome) => reportSetupOutcomeWith(defaultDeps, outcome);
642
+ var reportSetupAbortWith = async (deps, reason, exitCode, keyCheck = "unknown") => emit(deps, {
643
+ agents: [],
644
+ connectMode: recordedOptions?.connectMode ?? null,
645
+ connectOutcome: "skipped",
646
+ failedAgents: 0,
647
+ agentFailures: [],
648
+ closingReason: null,
649
+ skippedAgents: [],
650
+ nonInteractive: recordedOptions?.nonInteractive ?? null,
651
+ keyCheck,
652
+ exitCode,
653
+ abandonedAt: currentStep,
654
+ abortReason: reason
655
+ });
656
+ var reportSetupAbort = (reason, exitCode, keyCheck) => reportSetupAbortWith(defaultDeps, reason, exitCode, keyCheck);
657
+
658
+ // src/setup/app-key.ts
659
+ var pickAppKey = (candidates) => candidates.fromAgents.find(
660
+ (candidate) => typeof candidate === "string" && candidate !== candidates.storedKey && candidate !== candidates.envKey && isValidApiKey(candidate)
661
+ ) ?? null;
662
+
663
+ // bin/pushary-setup.ts
664
+ var CLAUDE_SETTINGS = claudeSettings();
665
+ var CLAUDE_JSON = claudeJson();
666
+ var CLAUDE_MCP_RECEIPT = claudeMcpReceipt();
667
+ var HERMES_CONFIG_RECEIPT = hermesConfigReceipt();
668
+ var CURSOR_PLUGIN_DIR = cursorPluginDir();
669
+ var CURSOR_USER_HOOKS = cursorUserHooks();
670
+ var VSCODE_PLUGIN_DIR = vscodePluginDir();
671
+ var VSCODE_PLUGIN_MCP = vscodePluginMcp();
672
+ var CLAUDE_SKILL_DIR = claudeSkillDir();
673
+ var CODEX_HOME = codexHome();
674
+ var CODEX_SKILL_DIR = codexSkillDir();
675
+ var CODEX_AGENTS_MD = codexAgentsMd();
676
+ var GEMINI_SETTINGS = geminiSettings();
677
+ var GEMINI_MD = geminiMd();
678
+ var PUSHARY_CONFIG_FILE = pusharyConfigFile();
679
+ var manualSteps = [];
680
+ var noteManual = (step) => {
681
+ manualSteps.push(step);
682
+ };
683
+ var humanStream = process.stdout;
684
+ var writeHuman = (text) => {
685
+ humanStream.write(text);
686
+ };
687
+ var jsonMode = false;
688
+ var paint = ansiPainter(() => useColor({ json: jsonMode }));
689
+ var dim = paint("\x1B[2m");
690
+ var bold = paint("\x1B[1m");
691
+ var green = paint("\x1B[32m");
692
+ var cyan = paint("\x1B[36m");
693
+ var yellow = paint("\x1B[33m");
694
+ var check = green("\u2713");
695
+ var abortWith = async (reason, code, keyCheck) => {
696
+ await reportSetupAbort(reason, code, keyCheck);
697
+ if (jsonMode) {
698
+ process.stdout.write(`${JSON.stringify({ ok: false, reason, exitCode: code, ...keyCheck ? { keyCheck } : {} }, null, 2)}
699
+ `);
700
+ }
701
+ return code;
702
+ };
703
+ var readKeyFromStdin = async (options) => {
704
+ if (!options.keyStdin) return void 0;
705
+ const chunks = [];
706
+ for await (const chunk of process.stdin) chunks.push(chunk);
707
+ const first = Buffer.concat(chunks).toString("utf-8").split("\n", 1)[0]?.trim();
708
+ if (!first) {
709
+ console.log(`
710
+ ${yellow("!")} --key-stdin was passed but stdin was empty.`);
711
+ process.exit(EXIT.USAGE);
712
+ }
713
+ if (!isValidApiKey(first)) {
714
+ console.log(`
715
+ ${yellow("!")} The key on stdin is not a valid Pushary key. Expected: ${dim("pk_xxx.xxx")}`);
716
+ console.log(` ${dim("Copy your key from")} ${cyan("https://pushary.com/dashboard/agent/settings")}
717
+ `);
718
+ process.exit(EXIT.USAGE);
719
+ }
720
+ return first;
721
+ };
722
+ var isInstalled = (command) => {
723
+ const whichCmd = process.platform === "win32" ? "where" : "which";
724
+ try {
725
+ execSync(`${whichCmd} ${command}`, { stdio: "ignore", timeout: 5e3 });
726
+ return true;
727
+ } catch {
728
+ return false;
729
+ }
730
+ };
731
+ var readJson = (filePath) => {
732
+ try {
733
+ return JSON.parse(readFileSync(filePath, "utf-8"));
734
+ } catch {
735
+ return {};
736
+ }
737
+ };
738
+ var readAgentJson = (filePath) => {
739
+ const result = readJsonSafe(filePath);
740
+ if (result.kind === "ok") return result.value;
741
+ if (result.kind === "missing") return {};
742
+ const backup = backupFile(filePath);
743
+ const reason = result.kind === "unparseable" ? "is not valid JSON" : "could not be read";
744
+ throw new AgentConfigError(
745
+ configFailureKind(result.kind),
746
+ filePath,
747
+ `${filePath} ${reason}, so it was left untouched. A copy is at ${backup}. Fix or remove the original, then re-run setup.`
748
+ );
749
+ };
750
+ var writeJson = (filePath, data) => {
751
+ const dir = dirname2(filePath);
752
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
753
+ writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n", "utf-8");
754
+ };
755
+ var readAgentToml = (filePath) => {
756
+ let raw;
757
+ try {
758
+ raw = readFileSync(filePath, "utf-8");
759
+ } catch (err) {
760
+ if (err.code === "ENOENT") return {};
761
+ throw new AgentConfigError("config-unreadable", filePath, `${filePath} could not be read, so it was left untouched.`);
762
+ }
763
+ if (raw.trim() === "") return {};
764
+ try {
765
+ return parseTOML(raw);
766
+ } catch {
767
+ const backup = backupFile(filePath);
768
+ throw new AgentConfigError(
769
+ "config-unparseable",
770
+ filePath,
771
+ `${filePath} is not valid TOML, so it was left untouched. A copy is at ${backup}. Fix or remove the original, then re-run setup.`
772
+ );
773
+ }
774
+ };
775
+ var formatError = (err) => {
776
+ if (err instanceof Error) {
777
+ const errWithSignal = err;
778
+ if (errWithSignal.killed && errWithSignal.signal === "SIGTERM") {
779
+ return "timed out \u2014 check your network connection";
780
+ }
781
+ return err.message;
782
+ }
783
+ return String(err);
784
+ };
785
+ var spinner = (label, fn, options = {}) => createIo(humanStream, { json: jsonMode }).spinner(label, fn, options);
786
+ var checkForUpdates = async (current, markAnnounced) => {
787
+ const announcement = takeAutoUpdateAnnouncement(autoUpdateState(), markAnnounced);
788
+ if (announcement) {
789
+ console.log(` ${check} ${describeAutoUpdateAnnouncement(announcement)}`);
790
+ console.log();
791
+ }
792
+ const latest = await fetchLatestPublishedVersion({ packageName: launchedPackage(ownPackageName()), timeoutMs: 3e3 });
793
+ if (latest && isNewerVersion(latest, current)) {
794
+ console.log(` ${yellow("!")} Update available: ${dim(current)} \u2192 ${green(latest)}`);
795
+ console.log(` ${dim("Run:")} ${NPX_LATEST_COMMAND} upgrade`);
796
+ console.log();
797
+ }
798
+ };
799
+ var globalInstallReady = false;
800
+ var warnVersionManagerPin = (execPath) => {
801
+ const manager = versionManagerOf(execPath);
802
+ if (manager) console.log(` ${yellow("!")} ${describeVersionManagerPin(execPath, manager)}`);
803
+ };
804
+ var installGlobally2 = async () => {
805
+ if (globalInstallReady) return;
806
+ const version = getPackageVersion();
807
+ await spinner(`Installing pushary-hook globally (${version})`, async () => {
808
+ try {
809
+ await installGlobally({ version });
810
+ } catch (err) {
811
+ throw err instanceof NpmMissingError ? err : new Error(npmErrorMessage(err));
812
+ }
813
+ });
814
+ globalInstallReady = true;
815
+ };
816
+ var _cachedSkillContent = null;
817
+ var fetchSkillContent = async () => {
818
+ if (_cachedSkillContent) return _cachedSkillContent;
819
+ const __dirname = dirname2(fileURLToPath(import.meta.url));
820
+ const candidates = [
821
+ join3(__dirname, "..", "..", "data", "SKILL.md"),
822
+ join3(__dirname, "..", "data", "SKILL.md")
823
+ ];
824
+ const source = candidates.find((path) => existsSync(path));
825
+ if (!source) throw new Error("packaged skill not found");
826
+ _cachedSkillContent = readFileSync(source, "utf-8");
827
+ return _cachedSkillContent;
828
+ };
829
+ var writePackagedSkill = async (dir) => {
830
+ const content = await fetchSkillContent();
831
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
832
+ writeFileSync(join3(dir, "SKILL.md"), content, "utf-8");
833
+ };
834
+ var skillsCliInUse = () => isInstalled("skills") || existsSync(join3(homedir(), ".agents", "skills"));
835
+ var skillInstallEffects = {
836
+ runSkillsCli: installSkillViaSkillsCli,
837
+ skillModifiedAt: (dir) => statSync(join3(dir, "SKILL.md"), { throwIfNoEntry: false })?.mtimeMs ?? null,
838
+ writePackagedSkill,
839
+ now: () => Date.now()
840
+ };
841
+ var installSkill = async (agent, agentSkillDir) => {
842
+ await spinner("Installing Pushary skill", async () => {
843
+ await installAgentSkill({ agent, agentSkillDir, skillsCliInUse: skillsCliInUse() }, skillInstallEffects);
844
+ }, { optional: true });
845
+ };
846
+ var setupClaudeCode = async (apiKey) => {
847
+ console.log(`
848
+ ${bold("Setting up Claude Code")}
849
+ `);
850
+ await installGlobally2();
851
+ const claudeJson2 = readAgentJson(CLAUDE_JSON);
852
+ const settings = readAgentJson(CLAUDE_SETTINGS);
853
+ const mcpReceipt = readJsonSafe(CLAUDE_MCP_RECEIPT);
854
+ let mcpSnapshot;
855
+ if (mcpReceipt.kind === "missing") {
856
+ mcpSnapshot = captureClaudeMcpServer(claudeJson2, apiKey);
857
+ } else {
858
+ if (mcpReceipt.kind !== "ok" || !isClaudeMcpSnapshot(mcpReceipt.value)) {
859
+ throw new Error(`Cannot safely replace Claude's Pushary MCP entry: ${CLAUDE_MCP_RECEIPT} is unreadable or invalid.`);
860
+ }
861
+ mcpSnapshot = updateClaudeMcpSnapshot(mcpReceipt.value, apiKey);
862
+ }
863
+ writeJsonAtomic(CLAUDE_MCP_RECEIPT, mcpSnapshot, 384);
864
+ await spinner("Adding MCP server (type: http)", async () => {
865
+ addClaudeMcpServer(claudeJson2, apiKey);
866
+ });
867
+ await spinner("Auto-allowing Pushary tools", async () => {
868
+ addPusharyToolPermissions(settings);
869
+ });
870
+ await spinner("Adding hooks (approvals, activity, presence, and lifecycle events)", async () => {
871
+ addPusharyHooks(settings, resolveGlobalBinDir("pushary-hook"), detectClaudeVersion());
872
+ });
873
+ await spinner(`Writing ${CLAUDE_JSON}`, async () => {
874
+ writeJsonAtomic(CLAUDE_JSON, claudeJson2, 384);
875
+ });
876
+ await spinner(`Writing ${CLAUDE_SETTINGS}`, async () => {
877
+ writeJsonAtomic(CLAUDE_SETTINGS, settings);
878
+ });
879
+ await installSkill("claude-code", CLAUDE_SKILL_DIR);
880
+ console.log();
881
+ console.log(` ${dim("What this configured:")}`);
882
+ console.log(` ${dim("\u2022")} MCP server: your agent can send notifications and ask questions`);
883
+ console.log(` ${dim("\u2022")} Skill: teaches your agent when and how to use Pushary`);
884
+ console.log(` ${dim("\u2022")} Hooks: route permission approvals through push notifications`);
885
+ console.log(` ${dim("\u2022")} Auto-allowed tools: no permission prompts for Pushary MCP calls`);
886
+ };
887
+ var IS_WINDOWS = process.platform === "win32";
888
+ var ensurePip = (python) => {
889
+ try {
890
+ execSync(`"${python}" -m pip --version`, { stdio: "ignore", timeout: 15e3 });
891
+ return;
892
+ } catch {
893
+ }
894
+ try {
895
+ execSync(`"${python}" -m ensurepip --upgrade`, { stdio: "pipe", timeout: 6e4 });
896
+ } catch {
897
+ }
898
+ };
899
+ var enablePusharyPlugin = (python) => {
900
+ return runHermesConfigScript(python, "setup", HERMES_CONFIG_RECEIPT) !== "failed";
901
+ };
902
+ var setupHermes = async (_apiKey) => {
903
+ console.log(`
904
+ ${bold("Setting up Hermes Agent")}
905
+ `);
906
+ if (!isInstalled("hermes")) {
907
+ console.log(` ${yellow("!")} Hermes CLI not found. Skipping.`);
908
+ console.log(` ${dim("Install Hermes and re-run setup to configure.")}`);
909
+ return "skipped";
910
+ }
911
+ const python = resolveHermesPython();
912
+ if (!python) {
913
+ console.log(` ${yellow("!")} Could not locate the Python that Hermes runs in. Skipping.`);
914
+ noteManual(`Install the Hermes plugin by hand: <hermes-python> -m pip install ${HERMES_PIP_PACKAGE}`);
915
+ return "skipped";
916
+ }
917
+ const existingReceipt = readJsonSafe(HERMES_CONFIG_RECEIPT);
918
+ if (existingReceipt.kind !== "missing" && (existingReceipt.kind !== "ok" || !isHermesConfigReceipt(existingReceipt.value))) {
919
+ throw new Error(`Cannot safely configure Hermes: ${HERMES_CONFIG_RECEIPT} is unreadable or invalid.`);
920
+ }
921
+ await spinner(`Installing ${HERMES_PIP_PACKAGE} into Hermes\u2019 environment`, async () => {
922
+ ensurePip(python);
923
+ execSync(`"${python}" -m pip install --upgrade ${HERMES_PIP_PACKAGE}`, { stdio: "pipe", timeout: 18e4 });
924
+ });
925
+ let pluginEnabled = false;
926
+ await spinner("Enabling plugin + routing questions to push", async () => {
927
+ pluginEnabled = enablePusharyPlugin(python);
928
+ });
929
+ if (!pluginEnabled) {
930
+ console.log(` ${yellow("!")} Installed the plugin, but could not edit Hermes' config.`);
931
+ noteManual('Add "pushary" to plugins.enabled and set security.approval.transport to "pushary" (with transport_fallback "builtin") in ~/.hermes/config.yaml, plus "clarify" in agent.disabled_toolsets');
932
+ }
933
+ console.log();
934
+ console.log(` ${dim("What this configured:")}`);
935
+ console.log(` ${dim("\u2022")} Native tools: pushary_notify, pushary_ask, pushary_wait, pushary_cancel, pushary_propose_scope`);
936
+ console.log(` ${dim("\u2022")} Approvals: Hermes' dangerous-command prompts go to your phone with allow once / this session / always / deny`);
937
+ console.log(` ${dim("\u2022")} Falls back to the terminal prompt when no device is connected, so work never blocks on us`);
938
+ console.log(` ${dim("\u2022")} Questions go to your phone: Hermes' terminal ${bold("clarify")} tool is disabled, so pushary_ask handles every question`);
939
+ console.log(` ${dim("\u2022")} Auto-notifications: push alert when tools return errors`);
940
+ console.log(` ${dim("To send approvals back to the terminal:")} remove ${bold("security.approval.transport")} from ~/.hermes/config.yaml`);
941
+ console.log(` ${dim("To re-enable terminal questions:")} remove ${bold("clarify")} from ${dim("agent.disabled_toolsets")}`);
942
+ };
943
+ var CODEX_HOOKS_JSON = join3(CODEX_HOME, "hooks.json");
944
+ var CODEX_HOOKS_MIN_VERSION = [0, 122, 0];
945
+ var codexOnPath = () => {
946
+ const whichCmd = IS_WINDOWS ? "where" : "which";
947
+ try {
948
+ const found = execSync(`${whichCmd} codex`, { encoding: "utf-8", stdio: "pipe", timeout: 5e3 }).split("\n")[0].trim();
949
+ return found || null;
950
+ } catch {
951
+ return null;
952
+ }
953
+ };
954
+ var codexRealpath = (path) => {
955
+ try {
956
+ return realpathSync(path);
957
+ } catch {
958
+ return null;
959
+ }
960
+ };
961
+ var EMBEDDED_SCAN_CHUNK_BYTES = 4 * 1024 * 1024;
962
+ var EMBEDDED_SCAN_MAX_BYTES = 320 * 1024 * 1024;
963
+ var readEmbeddedCodexVersion = (path) => {
964
+ let fd;
965
+ try {
966
+ fd = openSync(path, "r");
967
+ const chunk = Buffer.allocUnsafe(EMBEDDED_SCAN_CHUNK_BYTES);
968
+ let carry = "";
969
+ let scanned = 0;
970
+ while (scanned < EMBEDDED_SCAN_MAX_BYTES) {
971
+ const bytes = readSync(fd, chunk, 0, chunk.length, null);
972
+ if (bytes <= 0) return null;
973
+ scanned += bytes;
974
+ const text = carry + chunk.toString("latin1", 0, bytes);
975
+ const found = parseEmbeddedCodexVersion(text);
976
+ if (found) return found;
977
+ carry = text.slice(-32);
978
+ }
979
+ return null;
980
+ } catch {
981
+ return null;
982
+ } finally {
983
+ if (fd !== void 0) {
984
+ try {
985
+ closeSync(fd);
986
+ } catch {
987
+ fd = void 0;
988
+ }
989
+ }
990
+ }
991
+ };
992
+ var bundledCodex = () => findBundledCodex({
993
+ config: () => {
994
+ try {
995
+ return parseTOML(readFileSync(codexConfigToml(), "utf-8"));
996
+ } catch {
997
+ return null;
998
+ }
999
+ },
1000
+ exists: existsSync,
1001
+ home: homedir(),
1002
+ platform: process.platform
1003
+ });
1004
+ var detectCodexVersion = () => readCodexVersion({
1005
+ resolveOnPath: codexOnPath,
1006
+ realpath: codexRealpath,
1007
+ readFile: (path) => {
1008
+ try {
1009
+ return readFileSync(path, "utf-8");
1010
+ } catch {
1011
+ return null;
1012
+ }
1013
+ },
1014
+ bundledVersion: () => {
1015
+ const path = bundledCodex();
1016
+ return path ? readEmbeddedCodexVersion(path) : null;
1017
+ },
1018
+ brewVersions: () => {
1019
+ try {
1020
+ return execSync("brew list --versions codex", {
1021
+ encoding: "utf-8",
1022
+ stdio: "pipe",
1023
+ timeout: 1e4
1024
+ }).trim() || null;
1025
+ } catch {
1026
+ return null;
1027
+ }
1028
+ }
1029
+ });
1030
+ var detectCodexInstall = () => checkCodexInstall({
1031
+ resolveOnPath: codexOnPath,
1032
+ realpath: codexRealpath,
1033
+ exists: existsSync,
1034
+ listDir: (path) => {
1035
+ try {
1036
+ return readdirSync(path);
1037
+ } catch {
1038
+ return null;
1039
+ }
1040
+ },
1041
+ fileSize: (path) => {
1042
+ try {
1043
+ const stat = statSync(path);
1044
+ return stat.isFile() ? stat.size : null;
1045
+ } catch {
1046
+ return null;
1047
+ }
1048
+ },
1049
+ join: join3,
1050
+ dirname: dirname2
1051
+ });
1052
+ var codexSupportsHooks = (version) => version !== null && compareCodexVersion(version, CODEX_HOOKS_MIN_VERSION) >= 0;
1053
+ var removeCodexNotifyEntry = (codexConfig) => {
1054
+ let raw = "";
1055
+ try {
1056
+ raw = readFileSync(codexConfig, "utf-8");
1057
+ } catch {
1058
+ return;
1059
+ }
1060
+ if (!raw.includes("pushary-codex")) return;
1061
+ const config = parseTOML(raw);
1062
+ if (!Array.isArray(config.notify)) return;
1063
+ const filtered = config.notify.filter((entry) => typeof entry !== "string" || !entry.includes("pushary-codex"));
1064
+ if (filtered.length === config.notify.length) return;
1065
+ if (filtered.length === 0) {
1066
+ delete config.notify;
1067
+ } else {
1068
+ config.notify = filtered;
1069
+ }
1070
+ writeFileSync(codexConfig, stringifyTOML(config), "utf-8");
1071
+ };
1072
+ var addCodexNotifyEntry = (codexConfig) => {
1073
+ const pusharyCodexPath = resolveGlobalBinary("pushary-codex") ?? "pushary-codex";
1074
+ let raw = "";
1075
+ try {
1076
+ raw = readFileSync(codexConfig, "utf-8");
1077
+ } catch {
1078
+ }
1079
+ const config = raw ? parseTOML(raw) : {};
1080
+ const notify = Array.isArray(config.notify) ? config.notify : [];
1081
+ if (!notify.some((n) => typeof n === "string" && n.includes("pushary-codex"))) {
1082
+ notify.push(pusharyCodexPath);
1083
+ config.notify = notify;
1084
+ writeFileSync(codexConfig, stringifyTOML(config), "utf-8");
1085
+ }
1086
+ };
1087
+ var confirmCodexTrust = async () => {
1088
+ let confirmation = { kind: "unavailable", reason: "setup could not ask Codex" };
1089
+ await spinner("Checking the Pushary hook's trust with Codex", async () => {
1090
+ confirmation = await ensureCodexTrustsPusharyHooks();
1091
+ }, { optional: true });
1092
+ return confirmation;
1093
+ };
1094
+ var setupCodex = async (apiKey) => {
1095
+ console.log(`
1096
+ ${bold("Setting up Codex")}
1097
+ `);
1098
+ const onPath = isInstalled("codex");
1099
+ const bundled = onPath ? null : bundledCodex();
1100
+ if (!onPath && !bundled) {
1101
+ console.log(` ${yellow("!")} Codex not found. Skipping.`);
1102
+ console.log(` ${dim("Install the Codex CLI or the ChatGPT desktop app, then re-run setup.")}`);
1103
+ return "skipped";
1104
+ }
1105
+ if (bundled) {
1106
+ console.log(` ${dim("Using the Codex inside the ChatGPT desktop app.")}`);
1107
+ console.log(` ${dim(shortenHome(bundled))}`);
1108
+ console.log();
1109
+ }
1110
+ if (onPath && detectCodexInstall().kind === "binary-missing") {
1111
+ console.log(` ${yellow("!")} Your Codex install is missing the binary it runs.`);
1112
+ console.log(` ${dim("A Pushary setup before 0.71.0 asked Codex its version. On macOS that hands")}`);
1113
+ console.log(` ${dim("the binary to XProtect, which quarantines the Codex CLI and moves it to the")}`);
1114
+ console.log(` ${dim("Trash. Setup no longer runs it. Reinstall Codex to get it back:")}`);
1115
+ console.log(` ${cyan("npm install -g @openai/codex")}`);
1116
+ console.log(` ${dim("Configuring anyway, so it is ready when Codex is.")}`);
1117
+ console.log();
1118
+ noteManual("Reinstall Codex: a Pushary setup before 0.71.0 got its binary quarantined by macOS");
1119
+ }
1120
+ await installGlobally2();
1121
+ const codexConfig = codexConfigToml();
1122
+ await spinner("Adding Pushary MCP server (key embedded, auto-allowed)", async () => {
1123
+ const config = readAgentToml(codexConfig);
1124
+ addCodexMcpServer(config, apiKey);
1125
+ writeFileAtomic(codexConfig, stringifyTOML(config), KEY_FILE_MODE);
1126
+ });
1127
+ const codexVersion = detectCodexVersion();
1128
+ const hooksSupported = codexSupportsHooks(codexVersion);
1129
+ let portedTrustWritten = false;
1130
+ if (hooksSupported) {
1131
+ const hookCommand = guardBinary(resolveGlobalBinary("pushary-codex-hook") ?? "pushary-codex-hook");
1132
+ let hookPositions = {};
1133
+ let vacated = {};
1134
+ await spinner("Adding native hooks (~/.codex/hooks.json)", async () => {
1135
+ const hooksConfig = readAgentJson(CODEX_HOOKS_JSON);
1136
+ vacated = codexVacatedIndexes(hooksConfig);
1137
+ addCodexHooks(hooksConfig, hookCommand);
1138
+ writeJson(CODEX_HOOKS_JSON, hooksConfig);
1139
+ hookPositions = codexHookPositions(hooksConfig);
1140
+ });
1141
+ await spinner("Writing trust entries for the Pushary hook", async () => {
1142
+ let raw = "";
1143
+ try {
1144
+ raw = readFileSync(codexConfig, "utf-8");
1145
+ } catch {
1146
+ }
1147
+ const config = raw ? parseTOML(raw) : {};
1148
+ dropVacatedCodexHookTrust(config, CODEX_HOOKS_JSON, vacated);
1149
+ addCodexHookTrust(config, CODEX_HOOKS_JSON, hookCommand, hookPositions);
1150
+ writeFileSync(codexConfig, stringifyTOML(config), "utf-8");
1151
+ portedTrustWritten = true;
1152
+ }, { optional: true });
1153
+ await spinner("Removing legacy notify handler", async () => {
1154
+ removeCodexNotifyEntry(codexConfig);
1155
+ });
1156
+ } else {
1157
+ if (codexVersion !== null) {
1158
+ console.log(` ${yellow("!")} This Codex version predates native hooks (needs ${CODEX_HOOKS_MIN_VERSION.join(".")}+).`);
1159
+ console.log(` ${dim("Installing the deprecated notify handler instead. Upgrade Codex and re-run setup")}`);
1160
+ console.log(` ${dim("to get policy enforcement, phone approvals, and session tracking.")}`);
1161
+ } else {
1162
+ console.log(` ${yellow("!")} Could not read your Codex version from its install, so hooks were skipped.`);
1163
+ console.log(` ${dim("Deliberately not run: asking the binary its version is what macOS quarantines.")}`);
1164
+ console.log(` ${dim(`Installing the notify handler, which works on every version. If Codex is ${CODEX_HOOKS_MIN_VERSION.join(".")}+,`)}`);
1165
+ console.log(` ${dim("re-run setup after installing it through npm or Homebrew to get native hooks.")}`);
1166
+ }
1167
+ await spinner("Adding notify handler for Codex events (deprecated)", async () => {
1168
+ addCodexNotifyEntry(codexConfig);
1169
+ });
1170
+ }
1171
+ await installSkill("codex", CODEX_SKILL_DIR);
1172
+ await spinner("Teaching Codex to ask via push (~/.codex/AGENTS.md)", async () => {
1173
+ writeInstructionBlock(CODEX_AGENTS_MD, renderAgentInstructions("Codex"));
1174
+ });
1175
+ const confirmation = hooksSupported ? await confirmCodexTrust() : null;
1176
+ const trustOutcome = confirmation === null ? null : codexTrustOutcome(confirmation, portedTrustWritten);
1177
+ if (trustOutcome !== null) recordCodexTrust(trustOutcome);
1178
+ console.log();
1179
+ console.log(` ${dim("What this configured:")}`);
1180
+ console.log(` ${dim("\u2022")} MCP server: Codex can send notifications and ask questions`);
1181
+ console.log(` ${dim("\u2022")} API key embedded in config.toml: works without a shell reload or export`);
1182
+ console.log(` ${dim("\u2022")} Proactive instructions: ~/.codex/AGENTS.md tells Codex to ask via push, not the terminal`);
1183
+ console.log(` ${dim("\u2022")} Auto-allowed tools: no permission prompts for Pushary MCP calls`);
1184
+ if (hooksSupported) {
1185
+ console.log(` ${dim("\u2022")} Native hooks: phone approvals, policy enforcement, kill switch, session tracking`);
1186
+ if (trustOutcome === "verified") {
1187
+ console.log(` ${dim("\u2022")} Hook trusted, and Codex confirmed it: no ${cyan("/hooks")} step`);
1188
+ } else if (trustOutcome === "unverified" && confirmation) {
1189
+ console.log(` ${dim("\u2022")} Hook pre-trusted: no ${cyan("/hooks")} step. If approvals never arrive, ${cyan("/hooks")} re-trusts it`);
1190
+ console.log(` ${dim(describeCodexTrustConfirmation(confirmation))}`);
1191
+ } else {
1192
+ if (confirmation) {
1193
+ console.log(` ${yellow("!")} ${describeCodexTrustConfirmation(confirmation)}`);
1194
+ }
1195
+ console.log();
1196
+ console.log(` ${bold("One step left, and it happens inside Codex")}`);
1197
+ console.log(` ${dim("Codex makes you trust any hook before it runs. This is Codex's own")}`);
1198
+ console.log(` ${dim("security check, not a Pushary thing, and you only do it once.")}`);
1199
+ console.log();
1200
+ if (bundled) {
1201
+ console.log(` ${cyan("1.")} Open ChatGPT, then Settings and ${cyan("Hooks")}`);
1202
+ console.log(` ${cyan("2.")} You will see the Pushary hook commands. Trust them.`);
1203
+ } else {
1204
+ console.log(` ${cyan("1.")} Open Codex and type ${cyan("/hooks")}`);
1205
+ console.log(` ${cyan("2.")} You will see the Pushary hook commands. Trust them.`);
1206
+ }
1207
+ console.log();
1208
+ console.log(` ${dim("Until you do, Codex just runs as usual without the phone approvals.")}`);
1209
+ console.log(` ${dim("To confirm:")} run a command that needs approval and watch your phone.`);
1210
+ console.log(` ${dim("Pushary updates keep the same trust, so Codex will not ask again.")}`);
1211
+ noteManual(bundled ? "Open ChatGPT, then Settings and Hooks, and trust the Pushary hook. Until then Codex runs without phone approvals." : "Open Codex, type /hooks, and trust the Pushary hook. Until then Codex runs without phone approvals.");
1212
+ }
1213
+ } else {
1214
+ console.log(` ${dim("\u2022")} Notify handler (deprecated): captures turn completions and approval requests`);
1215
+ }
1216
+ };
1217
+ var resolveBundledPlugin = () => {
1218
+ const dir = dirname2(fileURLToPath(import.meta.url));
1219
+ const candidates = [
1220
+ join3(dir, "..", "..", "data", "cursor-plugin"),
1221
+ join3(dir, "..", "data", "cursor-plugin"),
1222
+ join3(dir, "..", "..", "..", "cursor-plugin"),
1223
+ join3(dir, "..", "..", "cursor-plugin")
1224
+ ];
1225
+ return candidates.find((p) => existsSync(join3(p, ".cursor-plugin", "plugin.json"))) ?? null;
1226
+ };
1227
+ var installCursorUserHooks = (gateScript) => {
1228
+ const bundled = readJson(join3(CURSOR_PLUGIN_DIR, "hooks", "hooks.json")).hooks ?? {};
1229
+ let userHooks = {};
1230
+ if (existsSync(CURSOR_USER_HOOKS)) {
1231
+ const result = readJsonSafe(CURSOR_USER_HOOKS);
1232
+ if (result.kind === "ok") userHooks = result.value;
1233
+ else if (result.kind !== "missing") {
1234
+ let backup = "";
1235
+ try {
1236
+ backup = backupFile(CURSOR_USER_HOOKS);
1237
+ } catch {
1238
+ }
1239
+ throw new AgentConfigError(
1240
+ configFailureKind(result.kind),
1241
+ CURSOR_USER_HOOKS,
1242
+ `Could not parse or read Cursor hooks.json; left it untouched${backup ? ` and backed it up at ${backup}` : ""}. Fix the file, then re-run setup.`
1243
+ );
1244
+ }
1245
+ }
1246
+ const hooks = userHooks.hooks ?? {};
1247
+ for (const event of Object.keys(bundled)) {
1248
+ const template = bundled[event]?.[0];
1249
+ if (!template) throw new Error(`bundled Cursor hooks.json missing a ${event} entry`);
1250
+ const existing = Array.isArray(hooks[event]) ? hooks[event] : [];
1251
+ hooks[event] = registerCursorGate(existing, { ...template, command: guardCursorGate(gateScript) });
1252
+ }
1253
+ writeJson(CURSOR_USER_HOOKS, { ...userHooks, version: userHooks.version ?? 1, hooks });
1254
+ };
1255
+ var neutralizePluginGate = () => {
1256
+ const path = join3(CURSOR_PLUGIN_DIR, "hooks", "hooks.json");
1257
+ if (!existsSync(path)) return;
1258
+ const data = readJson(path);
1259
+ if (!data.hooks) return;
1260
+ let changed = false;
1261
+ for (const [event, hooks] of Object.entries(data.hooks)) {
1262
+ if (!Array.isArray(hooks)) continue;
1263
+ const others = hooks.filter((hook) => pusharyHookExecutable(String(hook?.command ?? "")) === null);
1264
+ if (others.length === hooks.length) continue;
1265
+ changed = true;
1266
+ if (others.length) data.hooks[event] = others;
1267
+ else delete data.hooks[event];
1268
+ }
1269
+ if (!changed) return;
1270
+ writeJson(path, { version: data.version ?? 1, hooks: data.hooks });
1271
+ };
1272
+ var setupCursor = async (apiKey) => {
1273
+ console.log(`
1274
+ ${bold("Setting up Cursor")}
1275
+ `);
1276
+ const source = resolveBundledPlugin();
1277
+ if (!source) throw new Error("bundled Cursor plugin not found in this package");
1278
+ await spinner("Installing Pushary plugin", async () => {
1279
+ const staging = join3(dirname2(CURSOR_PLUGIN_DIR), `.pushary-staging-${process.pid}`);
1280
+ const backup = `${CURSOR_PLUGIN_DIR}.pushary-backup-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}`;
1281
+ rmSync(staging, { recursive: true, force: true });
1282
+ try {
1283
+ cpSync(source, staging, {
1284
+ recursive: true,
1285
+ filter: (p) => !["tools", "node_modules", ".git", ".DS_Store"].includes(basename(p))
1286
+ });
1287
+ const staged = readJsonSafe(join3(staging, ".cursor-plugin", "plugin.json"));
1288
+ if (staged.kind !== "ok") {
1289
+ throw new Error("staged Cursor plugin is missing or has an unreadable plugin.json");
1290
+ }
1291
+ const hadExisting = existsSync(CURSOR_PLUGIN_DIR);
1292
+ if (hadExisting) renameSync(CURSOR_PLUGIN_DIR, backup);
1293
+ try {
1294
+ renameSync(staging, CURSOR_PLUGIN_DIR);
1295
+ } catch (err) {
1296
+ if (hadExisting) renameSync(backup, CURSOR_PLUGIN_DIR);
1297
+ throw err;
1298
+ }
1299
+ if (hadExisting) rmSync(backup, { recursive: true, force: true });
1300
+ } finally {
1301
+ rmSync(staging, { recursive: true, force: true });
1302
+ }
1303
+ });
1304
+ await spinner("Linking your API key", async () => {
1305
+ const mcpPath = join3(CURSOR_PLUGIN_DIR, "mcp.json");
1306
+ const mcp = readAgentJson(mcpPath);
1307
+ const servers = mcp.mcpServers ?? {};
1308
+ if (servers.pushary) {
1309
+ servers.pushary.headers = { ...servers.pushary.headers, Authorization: `Bearer ${apiKey}` };
1310
+ mcp.mcpServers = servers;
1311
+ writeJsonAtomic(mcpPath, mcp, KEY_FILE_MODE);
1312
+ }
1313
+ });
1314
+ await spinner("Registering permission gate (~/.cursor/hooks.json)", async () => {
1315
+ installCursorUserHooks(join3(CURSOR_PLUGIN_DIR, "scripts", "pushary-gate.mjs"));
1316
+ neutralizePluginGate();
1317
+ });
1318
+ console.log();
1319
+ console.log(` ${dim("What this configured:")}`);
1320
+ console.log(` ${dim("\u2022")} Plugin installed to ~/.cursor/plugins/local/pushary (MCP tools, rule, skill)`);
1321
+ console.log(` ${dim("\u2022")} Permission gate registered once in ~/.cursor/hooks.json`);
1322
+ console.log(` ${dim("\u2022")} Risky shell commands route to push approval before they run`);
1323
+ console.log(` ${dim("\u2022")} Fully quit and reopen Cursor to load it (a Reload Window may not be enough)`);
1324
+ noteManual("Fully quit and reopen Cursor. A Reload Window may not be enough.");
1325
+ };
1326
+ var resolveBundledVsCodePlugin = () => {
1327
+ const dir = dirname2(fileURLToPath(import.meta.url));
1328
+ const candidates = [
1329
+ join3(dir, "..", "..", "data", "vscode-plugin"),
1330
+ join3(dir, "..", "data", "vscode-plugin"),
1331
+ join3(dir, "..", "..", "..", "vscode-plugin"),
1332
+ join3(dir, "..", "..", "vscode-plugin")
1333
+ ];
1334
+ return candidates.find((p) => existsSync(join3(p, ".claude-plugin", "plugin.json"))) ?? null;
1335
+ };
1336
+ var pinVsCodeGatePath = (pluginDir) => {
1337
+ const hooksPath = join3(pluginDir, "hooks", "hooks.json");
1338
+ const data = readJson(hooksPath);
1339
+ const entries = data.hooks?.PreToolUse;
1340
+ if (!Array.isArray(entries) || entries.length === 0) {
1341
+ throw new Error("bundled VS Code hooks.json is missing a PreToolUse entry");
1342
+ }
1343
+ const gate = join3(pluginDir, "scripts", "pushary-gate.mjs");
1344
+ data.hooks.PreToolUse = entries.map((entry) => ({ ...entry, command: `node "${gate}"` }));
1345
+ writeJson(hooksPath, data);
1346
+ };
1347
+ var registerVsCodePlugin = (pluginDir) => {
1348
+ const written = [];
1349
+ const manual = [];
1350
+ for (const settingsPath of vscodeSettingsTargets(existsSync)) {
1351
+ const current = existsSync(settingsPath) ? readFileSync(settingsPath, "utf-8") : null;
1352
+ const result = registerPluginLocation(current, pluginDir);
1353
+ if (result.kind === "already") continue;
1354
+ if (result.kind === "manual") {
1355
+ manual.push(settingsPath);
1356
+ continue;
1357
+ }
1358
+ if (current !== null) backupFile(settingsPath);
1359
+ mkdirSync(dirname2(settingsPath), { recursive: true });
1360
+ writeFileAtomic(settingsPath, result.content);
1361
+ written.push(settingsPath);
1362
+ }
1363
+ return { written, manual };
1364
+ };
1365
+ var setupVsCode = async (apiKey) => {
1366
+ console.log(`
1367
+ ${bold("Setting up VS Code")}
1368
+ `);
1369
+ const source = resolveBundledVsCodePlugin();
1370
+ if (!source) throw new Error("bundled VS Code plugin not found in this package");
1371
+ await spinner("Installing Pushary plugin", async () => {
1372
+ const staging = join3(dirname2(VSCODE_PLUGIN_DIR), `.pushary-staging-vscode-${process.pid}`);
1373
+ const backup = `${VSCODE_PLUGIN_DIR}.pushary-backup-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}`;
1374
+ mkdirSync(dirname2(VSCODE_PLUGIN_DIR), { recursive: true });
1375
+ rmSync(staging, { recursive: true, force: true });
1376
+ try {
1377
+ cpSync(source, staging, {
1378
+ recursive: true,
1379
+ filter: (p) => !["tools", "node_modules", ".git", ".DS_Store"].includes(basename(p))
1380
+ });
1381
+ const staged = readJsonSafe(join3(staging, ".claude-plugin", "plugin.json"));
1382
+ if (staged.kind !== "ok") {
1383
+ throw new Error("staged VS Code plugin is missing or has an unreadable plugin.json");
1384
+ }
1385
+ const hadExisting = existsSync(VSCODE_PLUGIN_DIR);
1386
+ if (hadExisting) renameSync(VSCODE_PLUGIN_DIR, backup);
1387
+ try {
1388
+ renameSync(staging, VSCODE_PLUGIN_DIR);
1389
+ } catch (err) {
1390
+ if (hadExisting) renameSync(backup, VSCODE_PLUGIN_DIR);
1391
+ throw err;
1392
+ }
1393
+ if (hadExisting) rmSync(backup, { recursive: true, force: true });
1394
+ } finally {
1395
+ rmSync(staging, { recursive: true, force: true });
1396
+ }
1397
+ });
1398
+ await spinner("Linking your API key", async () => {
1399
+ const mcpPath = VSCODE_PLUGIN_MCP;
1400
+ const mcp = readAgentJson(mcpPath);
1401
+ const servers = mcp.mcpServers ?? {};
1402
+ if (servers.pushary) {
1403
+ servers.pushary.headers = { ...servers.pushary.headers, Authorization: `Bearer ${apiKey}` };
1404
+ mcp.mcpServers = servers;
1405
+ writeJsonAtomic(mcpPath, mcp, KEY_FILE_MODE);
1406
+ }
1407
+ pinVsCodeGatePath(VSCODE_PLUGIN_DIR);
1408
+ });
1409
+ let registration = { written: [], manual: [] };
1410
+ await spinner("Registering the plugin with VS Code", async () => {
1411
+ registration = registerVsCodePlugin(VSCODE_PLUGIN_DIR);
1412
+ });
1413
+ console.log();
1414
+ console.log(` ${dim("What this configured:")}`);
1415
+ console.log(` ${dim("\u2022")} Plugin installed to ${shortenHome(VSCODE_PLUGIN_DIR)} (MCP tools, skill, commands)`);
1416
+ for (const path of registration.written) {
1417
+ console.log(` ${dim("\u2022")} Registered in ${shortenHome(path)} under chat.pluginLocations`);
1418
+ }
1419
+ console.log(` ${dim("\u2022")} Risky shell commands route to push approval before they run`);
1420
+ if (registration.manual.length > 0) {
1421
+ console.log();
1422
+ console.log(` ${yellow("!")} ${bold("One step left, by hand")}`);
1423
+ console.log(` ${dim("These settings files already have a chat.pluginLocations block, and")}`);
1424
+ console.log(` ${dim("they contain comments, so they were left untouched rather than rewritten.")}`);
1425
+ console.log(` ${dim("Add this entry inside that block:")}`);
1426
+ console.log();
1427
+ console.log(` ${cyan(`${JSON.stringify(VSCODE_PLUGIN_DIR)}: true`)}`);
1428
+ console.log();
1429
+ for (const path of registration.manual) {
1430
+ console.log(` ${dim("in")} ${path}`);
1431
+ }
1432
+ noteManual(
1433
+ `Add ${JSON.stringify(VSCODE_PLUGIN_DIR)}: true to the chat.pluginLocations block in ${registration.manual.join(" and ")}. Full snippet:
1434
+ ${pluginLocationSnippet(VSCODE_PLUGIN_DIR)}`
1435
+ );
1436
+ }
1437
+ console.log(` ${dim("\u2022")} Fully quit and reopen VS Code to load the plugin`);
1438
+ noteManual("Fully quit and reopen VS Code. A Reload Window may not pick up a new plugin location.");
1439
+ };
1440
+ var saveApiKey = async (apiKey, controlMode, transcriptSync) => {
1441
+ let result;
1442
+ await spinner("Saving your API key", async () => {
1443
+ result = writeKey(apiKey, {
1444
+ ...controlMode === null ? {} : { controlMode },
1445
+ transcriptsEnabled: transcriptSync
1446
+ });
1447
+ });
1448
+ if (!result) throw new Error("Could not save the API key");
1449
+ if (result.configBackupPath) {
1450
+ console.log(` ${yellow("!")} ${PUSHARY_CONFIG_FILE} did not parse. Saved a copy at ${dim(result.configBackupPath)}`);
1451
+ }
1452
+ const rc = result.rc;
1453
+ switch (rc.kind) {
1454
+ case "updated":
1455
+ console.log(` ${check} Replaced the old key in ${dim(rc.path ?? "")}${rc.previousKeyPrefix ? dim(` (was ${rc.previousKeyPrefix})`) : ""}`);
1456
+ break;
1457
+ case "declined":
1458
+ console.log(` ${yellow("!")} ${rc.path}:${rc.declinedLine} already sets ${cyan("PUSHARY_API_KEY")} in a form Pushary did not write.`);
1459
+ console.log(` ${dim("It was left alone, and it will shadow the new key in new shells. Update it to:")}`);
1460
+ console.log(` ${cyan(exportLine(apiKey))}`);
1461
+ break;
1462
+ case "unavailable":
1463
+ console.log(` ${yellow("!")} Could not update ${rc.path}${rc.detail ? dim(` (${rc.detail})`) : ""}. Add this line yourself:`);
1464
+ console.log(` ${cyan(exportLine(apiKey))}`);
1465
+ break;
1466
+ case "no-rc":
1467
+ console.log(` ${yellow("!")} No shell profile found for this shell. Add this line yourself:`);
1468
+ console.log(` ${cyan(exportLine(apiKey))}`);
1469
+ break;
1470
+ case "written":
1471
+ case "unchanged":
1472
+ break;
1473
+ }
1474
+ return rc;
1475
+ };
1476
+ var FX_SETTINGS_RULES_SNIPPET = `"permission": { "${FX_PUSHARY_TOOLS_RULE}": "allow", "${FX_QUESTION_TOOL}": "deny" }`;
1477
+ var FX_QUESTION_ROUTING_REPORTS = {
1478
+ "full-access": "fx runs in full-access mode, which ignores permission rules, so its terminal question tool stays on. Its instructions tell it to ask through Pushary instead; leave full-access mode with /permissions to turn the tool off for certain.",
1479
+ overridden: `A rule in ${fxSettings()} keeps fx's terminal question tool on, so fx can still ask there instead of through Pushary.`
1480
+ };
1481
+ var FX_QUESTION_SUMMARIES = {
1482
+ routed: "fx's agent asks through Pushary, which reaches your phone, or the Mac notch while the Pushary Mac app runs",
1483
+ "full-access": "fx's agent is told to ask through Pushary, but full-access mode keeps fx's terminal prompt available to it",
1484
+ overridden: "fx's terminal prompt stays available, so fx's agent is only told to ask through Pushary"
1485
+ };
1486
+ var reportFxSettings = (outcome) => {
1487
+ if (outcome.kind === "refused") {
1488
+ console.log(` ${yellow("!")} ${outcome.detail}, so Pushary tools were not auto-allowed and fx's terminal question tool stays on.`);
1489
+ noteManual(`Add ${FX_SETTINGS_RULES_SNIPPET} to ${fxSettings()} so fx runs Pushary tools without a prompt and asks through Pushary.`);
1490
+ return;
1491
+ }
1492
+ if (outcome.toolsChange === "user-rule-kept") {
1493
+ console.log(` ${yellow("!")} ${fxSettings()} already has a ${FX_PUSHARY_TOOLS_RULE} rule, so it was left as you set it.`);
1494
+ } else {
1495
+ console.log(` ${check} Auto-allowing Pushary tools (${fxSettings()})`);
1496
+ }
1497
+ if (outcome.questionRouting === "routed") {
1498
+ console.log(` ${check} Turning off fx's terminal question tool, so its questions go through Pushary`);
1499
+ } else if (outcome.questionChange === "user-rule-kept") {
1500
+ console.log(` ${yellow("!")} ${fxSettings()} already has a ${FX_QUESTION_TOOL} rule, so it was left as you set it and fx can still ask in the terminal.`);
1501
+ } else {
1502
+ console.log(` ${yellow("!")} ${FX_QUESTION_ROUTING_REPORTS[outcome.questionRouting]}`);
1503
+ }
1504
+ if (!outcome.ruleRecorded) {
1505
+ console.log(` ${yellow("!")} ${fxQuestionRuleReceipt()} could not be written, so pushary clean will treat the ${FX_QUESTION_TOOL} rule as yours and leave it.`);
1506
+ }
1507
+ if (outcome.workspacesWithOwnRules.length > 0) {
1508
+ console.log(` ${yellow("!")} ${outcome.workspacesWithOwnRules.length} fx workspace(s) set their own permission rules, which replace the global ones there. Add "${FX_PUSHARY_TOOLS_RULE}": "allow" and "${FX_QUESTION_TOOL}": "deny" to each to route them through Pushary.`);
1509
+ }
1510
+ };
1511
+ var teachFxToAskViaPush = () => {
1512
+ if (fxIgnoresInstructionsFile(fxAgentsMd(), fxConfigDir())) return false;
1513
+ writeInstructionBlock(fxAgentsMd(), `${renderAgentInstructions("fx")}
1514
+
1515
+ ${FX_TOOL_DISCOVERY_NOTE}`);
1516
+ return true;
1517
+ };
1518
+ var fxWired = () => inspectFxInstallation(fxMcpProfile(), fxSettings(), process.env).configured;
1519
+ var setupFx = async () => {
1520
+ console.log(`
1521
+ ${bold("Setting up fx")}
1522
+ `);
1523
+ if (process.platform === "win32") {
1524
+ console.log(` ${yellow("!")} fx does not run on Windows. Skipping.`);
1525
+ return "skipped";
1526
+ }
1527
+ if (!existsSync(fxConfigDir())) {
1528
+ console.log(` ${yellow("!")} fx not found (${fxConfigDir()} does not exist). Skipping.`);
1529
+ console.log(` ${dim("Install fx from https://fx.sh, run it once, then re-run setup.")}`);
1530
+ return "skipped";
1531
+ }
1532
+ await installGlobally2();
1533
+ await spinner(`Adding the Pushary MCP server (${fxMcpProfile()})`, async () => {
1534
+ const profile = readFxProfileForSetup(fxMcpProfile());
1535
+ const bridge = resolveGlobalBinary(MCP_BRIDGE_BINARY) ?? MCP_BRIDGE_BINARY;
1536
+ const entry = fxPusharyEntry(guardedArgv({ kind: "binary", path: bridge, runtime: "node" }));
1537
+ writeFxProfile(fxMcpProfile(), withFxPusharyServer(profile, entry));
1538
+ });
1539
+ const settingsOutcome = configureFxSettings(fxSettings(), fxQuestionRuleReceipt(), process.env);
1540
+ reportFxSettings(settingsOutcome);
1541
+ if (teachFxToAskViaPush()) {
1542
+ console.log(` ${check} Teaching fx to ask via push (${fxAgentsMd()})`);
1543
+ } else {
1544
+ console.log(` ${yellow("!")} ${fxAgentsMd()} links outside ${fxConfigDir()}, and fx ignores such a file, so no instructions were written.`);
1545
+ noteManual(`Replace the ${fxAgentsMd()} symlink with a file so fx loads Pushary's instructions, then run ${retryAgentCommand("fx")}.`);
1546
+ }
1547
+ console.log();
1548
+ console.log(` ${dim("What this configured:")}`);
1549
+ console.log(` ${dim("\u2022")} MCP server ${bold("pushary")} in ${fxMcpProfile()}, which runs ${MCP_BRIDGE_BINARY} and signs in with the key saved in ~/.pushary, so no key is written to fx's config`);
1550
+ console.log(` ${dim("\u2022")} Questions: ${FX_QUESTION_SUMMARIES[settingsOutcome.kind === "written" ? settingsOutcome.questionRouting : "overridden"]}`);
1551
+ console.log(` ${dim("\u2022")} fx cannot hand its own approval prompts to Pushary, so those still wait in the terminal`);
1552
+ console.log(` ${dim("Restart fx, or run /mcp reload in a running session.")}`);
1553
+ noteManual("Restart fx, or run /mcp reload in a running session.");
1554
+ return "configured";
1555
+ };
1556
+ var setupCustom = async (apiKey, context) => {
1557
+ console.log(`
1558
+ ${bold("Connect any other agent (MCP or HTTP)")}
1559
+ `);
1560
+ const wroteExport = context ? ["written", "updated", "unchanged"].includes(context.rc.kind) : true;
1561
+ if (wroteExport) {
1562
+ console.log(` ${dim("Your key is saved as")} ${cyan("$PUSHARY_API_KEY")} ${dim("in your shell profile.")}`);
1563
+ } else {
1564
+ console.log(` ${dim("Add this to your shell profile so")} ${cyan("$PUSHARY_API_KEY")} ${dim("resolves:")}`);
1565
+ console.log(` ${cyan(exportLine(apiKey))}`);
1566
+ }
1567
+ console.log();
1568
+ console.log(` ${dim("MCP clients (Windsurf, LangChain, custom) \u2014 add this server:")}`);
1569
+ console.log(` ${cyan("https://pushary.com/api/mcp/mcp")} ${dim("with header")} ${cyan("Authorization: Bearer $PUSHARY_API_KEY")}`);
1570
+ console.log();
1571
+ console.log(` ${dim("No-code / HTTP (n8n, Zapier, scripts) \u2014 ask a human and wait:")}`);
1572
+ console.log(` ${dim("POST")} ${cyan("https://pushary.com/api/v1/server/ask")} ${dim("body")} ${cyan('{"question":"Approve?","type":"confirm","wait":true}')}`);
1573
+ console.log();
1574
+ console.log(` ${dim("Full guide:")} ${cyan("https://pushary.com/docs/agents/connect-any-agent")}`);
1575
+ };
1576
+ var setupGemini = async (apiKey) => {
1577
+ console.log(`
1578
+ ${bold("Setting up Gemini CLI")}
1579
+ `);
1580
+ if (!isInstalled("gemini")) {
1581
+ console.log(` ${yellow("!")} Gemini CLI not found. Skipping.`);
1582
+ console.log(` ${dim("Install Gemini CLI and re-run setup to configure.")}`);
1583
+ return "skipped";
1584
+ }
1585
+ await installGlobally2();
1586
+ const settings = readAgentJson(GEMINI_SETTINGS);
1587
+ await spinner("Adding MCP server (~/.gemini/settings.json)", async () => {
1588
+ addGeminiMcpServer(settings, apiKey);
1589
+ });
1590
+ await spinner("Adding hooks (BeforeTool, AfterTool, BeforeAgent, SessionStart, SessionEnd)", async () => {
1591
+ addGeminiHooks(settings, guardBinary(resolveGlobalBinary(GEMINI_HOOK_BINARY) ?? GEMINI_HOOK_BINARY));
1592
+ });
1593
+ await spinner(`Writing ${GEMINI_SETTINGS}`, async () => {
1594
+ writeJsonAtomic(GEMINI_SETTINGS, settings, KEY_FILE_MODE);
1595
+ });
1596
+ await spinner("Teaching Gemini to ask via push (~/.gemini/GEMINI.md)", async () => {
1597
+ writeInstructionBlock(GEMINI_MD, renderAgentInstructions("Gemini"));
1598
+ });
1599
+ console.log();
1600
+ console.log(` ${dim("What this configured:")}`);
1601
+ console.log(` ${dim("\u2022")} MCP server: Gemini can send notifications and ask questions`);
1602
+ console.log(` ${dim("\u2022")} Proactive instructions: ~/.gemini/GEMINI.md tells Gemini to ask via push, not the terminal`);
1603
+ console.log(` ${dim("\u2022")} Hooks: phone approvals, policy enforcement, kill switch, session tracking`);
1604
+ console.log(` ${dim("\u2022")} Gate covers ${bold("run_shell_command")}, ${bold("write_file")}, and ${bold("replace")}`);
1605
+ console.log(` ${dim("\u2022")} Auto-allowed tools: Pushary MCP calls run without a confirmation prompt`);
1606
+ console.log(` ${dim("Restart Gemini CLI to load the new config.")}`);
1607
+ noteManual("Restart Gemini CLI to load the new config.");
1608
+ };
1609
+ var OPENCODE_INSTRUCTIONS = openCodeInstructionsFile;
1610
+ var setupOpenCode = async (apiKey) => {
1611
+ console.log(`
1612
+ ${bold("Setting up OpenCode")}
1613
+ `);
1614
+ if (!isInstalled("opencode") && !existsSync(openCodeConfigDir())) {
1615
+ console.log(` ${yellow("!")} OpenCode not found. Skipping.`);
1616
+ console.log(` ${dim("Install OpenCode and re-run setup to configure.")}`);
1617
+ return "skipped";
1618
+ }
1619
+ await installGlobally2();
1620
+ await spinner(`Writing the Pushary plugin (${openCodePluginFile()})`, async () => {
1621
+ const target = resolveGlobalBinary(OPENCODE_HOOK_BINARY) ?? OPENCODE_HOOK_BINARY;
1622
+ installOpenCodePlugin(
1623
+ guardedArgv({ kind: "binary", path: target, runtime: "node" }, ["--source", "opencode"]),
1624
+ openCodePluginDir()
1625
+ );
1626
+ });
1627
+ await spinner(`Adding MCP server (${openCodeConfigFile()})`, async () => {
1628
+ const config = readAgentJson(openCodeConfigFile());
1629
+ addOpenCodeMcpServer(config, apiKey);
1630
+ writeJsonAtomic(openCodeConfigFile(), config, KEY_FILE_MODE);
1631
+ });
1632
+ await spinner("Teaching OpenCode to ask via push", async () => {
1633
+ writeInstructionBlock(OPENCODE_INSTRUCTIONS(), renderAgentInstructions("OpenCode"));
1634
+ });
1635
+ console.log();
1636
+ console.log(` ${dim("What this configured:")}`);
1637
+ console.log(` ${dim("\u2022")} Plugin: ${openCodePluginFile()}`);
1638
+ console.log(` ${dim("\u2022")} MCP server: OpenCode can send notifications and ask questions`);
1639
+ console.log(` ${dim("\u2022")} Approvals: a permission OpenCode raises is answered from your phone`);
1640
+ console.log(` ${dim("\u2022")} Gate covers ${bold("bash")}, ${bold("edit")}, ${bold("write")} and ${bold("webfetch")}`);
1641
+ console.log(` ${dim("\u2022")} Receipts: tool results, prompts and turn boundaries reach your account`);
1642
+ console.log(` ${dim("Restart OpenCode to load the plugin.")}`);
1643
+ noteManual("Restart OpenCode to load the plugin.");
1644
+ return "configured";
1645
+ };
1646
+ var compatibleRecord = (of) => HOOK_COMPATIBLE_PROFILES.reduce(
1647
+ (record, profile) => ({ ...record, [profile.agentType]: of(profile) }),
1648
+ {}
1649
+ );
1650
+ var HOOK_FILES = {
1651
+ claude_code: () => CLAUDE_SETTINGS,
1652
+ codex: () => CODEX_HOOKS_JSON,
1653
+ gemini_cli: () => GEMINI_SETTINGS,
1654
+ cursor: () => CURSOR_USER_HOOKS,
1655
+ vscode: () => join3(VSCODE_PLUGIN_DIR, "hooks", "hooks.json"),
1656
+ ...compatibleRecord((profile) => () => compatibleConfigFile(profile))
1657
+ };
1658
+ var hookOwnerFor = (agent, appAvailable = true) => {
1659
+ const file = HOOK_FILES[agent]?.();
1660
+ if (!file) return "none";
1661
+ const document = readJsonSafe(file);
1662
+ const profile = hookCompatibleProfile(agent);
1663
+ return hookOwnerOf(
1664
+ document.kind === "ok" ? document.value : null,
1665
+ appAvailable,
1666
+ profile ? profile.hooksKeyPath : void 0
1667
+ );
1668
+ };
1669
+ var appMcpKeyFor = (agent) => {
1670
+ if (agent === "codex") {
1671
+ const auth = readCodexMcpAuth(readAgentToml(codexConfigToml()));
1672
+ return auth?.method === "embedded" ? auth.key : null;
1673
+ }
1674
+ const file = agent === "claude_code" ? CLAUDE_JSON : agent === "gemini_cli" ? GEMINI_SETTINGS : agent === "cursor" ? cursorUserMcp() : agent === "vscode" ? VSCODE_PLUGIN_MCP : null;
1675
+ return file ? mcpBearerKey(readAgentJson(file), "mcpServers", "headers") : null;
1676
+ };
1677
+ var appMcpKeyIfReadable = (agent) => {
1678
+ try {
1679
+ return appMcpKeyFor(agent);
1680
+ } catch {
1681
+ return null;
1682
+ }
1683
+ };
1684
+ var captureTakeoverReceipt = (agent) => {
1685
+ const hookFile = HOOK_FILES[agent]?.();
1686
+ if (!hookFile) return;
1687
+ const document = readAgentJson(hookFile);
1688
+ const captured = captureHookTakeover(agent, document);
1689
+ if (!captured) throw new Error(`Cannot safely take over ${AGENT_NAMES[agent]}'s hooks: no Mac app wiring was found.`);
1690
+ const codexConfig = agent === "codex" ? readAgentToml(codexConfigToml()) : null;
1691
+ const mcp = agent === "claude_code" ? [readAgentJson(CLAUDE_JSON), "mcpServers"] : agent === "codex" ? [codexConfig, "mcp_servers"] : agent === "gemini_cli" ? [document, "mcpServers"] : agent === "vscode" ? [readAgentJson(VSCODE_PLUGIN_MCP), "mcpServers"] : null;
1692
+ const receipt = {
1693
+ ...captured,
1694
+ ...codexConfig ? {
1695
+ codexTrust: captureCodexHookTrust(codexConfig, hookFile, codexHookPositions(document))
1696
+ } : {},
1697
+ ...mcp ? { mcpServer: captureMcpServer(mcp[0], mcp[1]) } : {}
1698
+ };
1699
+ writeJsonAtomic(hookTakeoverReceipt(agent), receipt, 384);
1700
+ };
1701
+ var agentIsWired = (agent) => {
1702
+ const readJson2 = (path) => {
1703
+ try {
1704
+ return JSON.parse(readFileSync(path, "utf-8"));
1705
+ } catch {
1706
+ return null;
1707
+ }
1708
+ };
1709
+ switch (agent) {
1710
+ case "claude_code":
1711
+ return claudeWired({
1712
+ claudeJson: readJson2(CLAUDE_JSON),
1713
+ settings: readJson2(CLAUDE_SETTINGS),
1714
+ skillExists: existsSync(join3(CLAUDE_SKILL_DIR, "SKILL.md"))
1715
+ });
1716
+ case "codex": {
1717
+ let config = null;
1718
+ try {
1719
+ config = parseTOML(readFileSync(join3(CODEX_HOME, "config.toml"), "utf-8"));
1720
+ } catch {
1721
+ config = null;
1722
+ }
1723
+ return codexWired({
1724
+ config,
1725
+ hooks: readJson2(CODEX_HOOKS_JSON),
1726
+ skillExists: existsSync(join3(CODEX_SKILL_DIR, "SKILL.md"))
1727
+ });
1728
+ }
1729
+ case "gemini_cli":
1730
+ return geminiWired({ settings: readJson2(GEMINI_SETTINGS) });
1731
+ case "cursor":
1732
+ return existsSync(join3(CURSOR_PLUGIN_DIR, "mcp.json"));
1733
+ case "vscode":
1734
+ return existsSync(VSCODE_PLUGIN_MCP);
1735
+ case "opencode":
1736
+ return openCodeWired();
1737
+ case "fx":
1738
+ return fxWired();
1739
+ default: {
1740
+ const profile = hookCompatibleProfile(agent);
1741
+ if (!profile) return true;
1742
+ const config = readJson2(compatibleConfigFile(profile));
1743
+ return config !== null && hasCompatibleHooks(config, profile);
1744
+ }
1745
+ }
1746
+ };
1747
+ var compatibleGateSummary = (profile) => {
1748
+ const gated = PRETOOLUSE_GATED_TOOLS.map((canonical) => Object.entries(profile.toolAliases).find(([, target]) => target === canonical)?.[0] ?? canonical);
1749
+ return gated.slice(0, 3).map((name) => bold(name)).join(", ");
1750
+ };
1751
+ var setupCompatibleAgent = (profile) => async (apiKey) => {
1752
+ const configFile = compatibleConfigFile(profile);
1753
+ const mcpFile = compatibleMcpFile(profile);
1754
+ const permissionFile = compatiblePermissionRuleFile(profile);
1755
+ const instructionsFile = compatibleInstructionsFile(profile);
1756
+ console.log(`
1757
+ ${bold(`Setting up ${profile.label}`)}
1758
+ `);
1759
+ if (!isInstalled(profile.binary) && !existsSync(dirname2(configFile))) {
1760
+ console.log(` ${yellow("!")} ${profile.label} not found. Skipping.`);
1761
+ console.log(` ${dim(`Install ${profile.label} and re-run setup to configure.`)}`);
1762
+ return "skipped";
1763
+ }
1764
+ await installGlobally2();
1765
+ const documents = /* @__PURE__ */ new Map();
1766
+ const open = (path) => {
1767
+ const existing = documents.get(path);
1768
+ if (existing) return existing;
1769
+ const document = readAgentJson(path);
1770
+ documents.set(path, document);
1771
+ return document;
1772
+ };
1773
+ const declined = [];
1774
+ const record = (path, write) => {
1775
+ if (!write.ok) declined.push(`${shortenHome(path)} \u2192 ${write.conflictAt}`);
1776
+ };
1777
+ const events = compatibleHookEvents(profile).map((hook) => hook.event);
1778
+ await spinner(`Adding hooks (${events.slice(0, 4).join(", ")}${events.length > 4 ? `, +${events.length - 4} more` : ""})`, async () => {
1779
+ record(configFile, addCompatibleHooks(open(configFile), profile, resolveGlobalBinDir("pushary-hook")));
1780
+ });
1781
+ if (mcpFile) {
1782
+ await spinner(`Adding MCP server (${shortenHome(mcpFile)})`, async () => {
1783
+ addCompatibleMcpServer(open(mcpFile), profile, apiKey);
1784
+ });
1785
+ }
1786
+ if (permissionFile) {
1787
+ await spinner("Auto-allowing Pushary MCP tools", async () => {
1788
+ record(permissionFile, addCompatibleToolPermissions(open(permissionFile)));
1789
+ });
1790
+ }
1791
+ if (declined.length > 0) {
1792
+ console.log(`
1793
+ ${yellow("!")} ${profile.label} keeps something unexpected where Pushary writes. Nothing was changed.`);
1794
+ for (const conflict of declined) console.log(` ${dim(`\u2022 ${conflict}`)}`);
1795
+ console.log(` ${dim("Pushary expects a list of hook entries there. Move that key and re-run setup.")}`);
1796
+ return "skipped";
1797
+ }
1798
+ for (const [path, document] of documents) {
1799
+ await spinner(`Writing ${shortenHome(path)}`, async () => {
1800
+ writeJsonAtomic(path, document, KEY_FILE_MODE);
1801
+ });
1802
+ }
1803
+ if (instructionsFile && profile.instructions) {
1804
+ await spinner(`Teaching ${profile.label} to ask via push (${shortenHome(instructionsFile)})`, async () => {
1805
+ writeInstructionBlock(instructionsFile, renderAgentInstructions(profile.instructions.label));
1806
+ });
1807
+ }
1808
+ console.log();
1809
+ console.log(` ${dim("What this configured:")}`);
1810
+ console.log(` ${dim("\u2022")} Hooks: phone approvals, policy enforcement, kill switch, session tracking`);
1811
+ console.log(` ${dim("\u2022")} Gate covers ${compatibleGateSummary(profile)}`);
1812
+ if (mcpFile) console.log(` ${dim("\u2022")} MCP server: ${profile.label} can send notifications and ask questions`);
1813
+ else console.log(` ${dim("\u2022")} ${dim(`No MCP server: ${profile.label} documents no remote-server config shape, so approvals work and agent-initiated questions do not`)}`);
1814
+ if (instructionsFile) console.log(` ${dim("\u2022")} Proactive instructions: ${shortenHome(instructionsFile)}`);
1815
+ console.log(` ${dim(`Restart ${profile.label} to load the new config.`)}`);
1816
+ noteManual(`Restart ${profile.label} to load the new config.`);
1817
+ return "configured";
1818
+ };
1819
+ var COMPATIBLE_SETUP = HOOK_COMPATIBLE_PROFILES.reduce((installers, profile) => ({
1820
+ ...installers,
1821
+ [profile.agentType]: setupCompatibleAgent(profile)
1822
+ }), {});
1823
+ var AGENT_SETUP = {
1824
+ claude_code: setupClaudeCode,
1825
+ codex: setupCodex,
1826
+ gemini_cli: setupGemini,
1827
+ hermes: setupHermes,
1828
+ cursor: setupCursor,
1829
+ vscode: setupVsCode,
1830
+ opencode: setupOpenCode,
1831
+ fx: setupFx,
1832
+ ...COMPATIBLE_SETUP,
1833
+ custom: setupCustom
1834
+ };
1835
+ var PROJECT_INSTRUCTION_TARGETS = {
1836
+ claude_code: { file: "CLAUDE.md", label: "Claude Code" },
1837
+ codex: { file: "AGENTS.md", label: SHARED_AGENTS_MD_LABEL }
1838
+ };
1839
+ var offerProjectInstructions = async (agents, options) => {
1840
+ const targets = agents.map((agent) => PROJECT_INSTRUCTION_TARGETS[agent]).filter((target) => target !== void 0);
1841
+ if (targets.length === 0 || !process.stdout.isTTY || options.nonInteractive) return;
1842
+ console.log();
1843
+ const wanted = await confirm({
1844
+ message: "Add Pushary instructions to this project's CLAUDE.md/AGENTS.md so your whole team's agents can use phone approvals?",
1845
+ default: false
1846
+ });
1847
+ if (!wanted) return;
1848
+ for (const target of targets) {
1849
+ await spinner(`Writing managed block to ${join3(process.cwd(), target.file)}`, async () => {
1850
+ writeInstructionBlock(join3(process.cwd(), target.file), renderProjectAgentInstructions(target.label));
1851
+ }, { optional: true });
1852
+ }
1853
+ console.log(` ${dim("Commit the file to share it. Teammates without a key fall back to the terminal.")}`);
1854
+ };
1855
+ var offerClaudeAlias = async (options) => {
1856
+ if (!process.stdout.isTTY || options.nonInteractive) return;
1857
+ const rc = resolveShellRc();
1858
+ console.log();
1859
+ if (!rc) {
1860
+ console.log(` ${dim("Tip: alias")} ${cyan("claude='pushary claude'")} ${dim("in your shell to make plain")} ${cyan("claude")} ${dim("reachable from your phone.")}`);
1861
+ return;
1862
+ }
1863
+ const wanted = await confirm({
1864
+ message: `Make plain ${bold("claude")} reachable from your phone by default? (adds an alias to ${basename(rc.path)}, reversible)`,
1865
+ default: false
1866
+ });
1867
+ if (!wanted) return;
1868
+ const result = installClaudeAlias();
1869
+ if (result.installed) {
1870
+ console.log(` ${check} Aliased ${cyan("claude")} \u2192 ${cyan("pushary claude")} in ${dim(result.path ?? rc.path)}`);
1871
+ console.log(` ${dim("Open a new terminal (or")} ${cyan(`source ${rc.path}`)}${dim(") to use it. Undo any time with")} ${cyan("pushary clean")}${dim(".")}`);
1872
+ } else if (result.reason === "exists") {
1873
+ console.log(` ${check} ${dim("The")} ${cyan("claude")} ${dim("alias is already set.")}`);
1874
+ }
1875
+ };
1876
+ var verifyKeyOrExit = async (apiKey, options, precomputed) => {
1877
+ const live = process.stdout.isTTY === true && !options.json && precomputed === void 0;
1878
+ if (live) writeHuman(` ${dim("...")} Verifying your key with pushary.com`);
1879
+ let result;
1880
+ if (precomputed !== void 0) {
1881
+ result = precomputed;
1882
+ } else {
1883
+ try {
1884
+ result = await checkApiKey(apiKey);
1885
+ } catch (err) {
1886
+ result = { kind: "unreachable", detail: formatError(err) };
1887
+ }
1888
+ }
1889
+ if (live) writeHuman("\r\x1B[2K");
1890
+ const report = describeKeyCheck(result);
1891
+ if (report.fatal) {
1892
+ console.log(` ${yellow("!")} ${report.message}`);
1893
+ for (const line of report.next) console.log(` ${dim(line)}`);
1894
+ console.log();
1895
+ process.exit(await abortWith("key-refused", report.exitCode, result.kind));
1896
+ }
1897
+ if (result.kind === "ok") {
1898
+ const { siteSlug, siteName, plan } = result.identity;
1899
+ console.log(` ${check} Key verified ${dim(`(${siteSlug || siteName} \xB7 plan ${plan})`)}`);
1900
+ } else {
1901
+ console.log(` ${yellow("!")} ${report.message}`);
1902
+ for (const line of report.next) console.log(` ${dim(line)}`);
1903
+ }
1904
+ return result;
1905
+ };
1906
+ var printPairingEscape = () => {
1907
+ console.log();
1908
+ console.log(` ${yellow("!")} No app? Two ways to finish without one:`);
1909
+ console.log();
1910
+ console.log(` ${cyan(BROWSER_CONNECT_COMMAND)}`);
1911
+ console.log(` ${dim("Approve in a browser tab. No phone needed.")}`);
1912
+ console.log();
1913
+ console.log(` ${cyan(`${NPX_LATEST_COMMAND} setup --key pk_...`)}`);
1914
+ console.log(` ${dim("Paste a key from")} ${cyan("pushary.com/dashboard/agent/settings")}`);
1915
+ console.log();
1916
+ console.log(` ${dim("Or get the app at")} ${cyan("https://pushary.com/download")} ${dim("and re-run setup.")}`);
1917
+ console.log();
1918
+ };
1919
+ var usableStoredKey = async (apiKey) => {
1920
+ let result;
1921
+ try {
1922
+ result = await checkApiKey(apiKey);
1923
+ } catch (err) {
1924
+ result = { kind: "unreachable", detail: formatError(err) };
1925
+ }
1926
+ return result.kind === "rejected" || result.kind === "malformed" ? null : result;
1927
+ };
1928
+ var AGENT_NAMES = {
1929
+ claude_code: "Claude Code",
1930
+ codex: "Codex",
1931
+ gemini_cli: "Gemini CLI",
1932
+ hermes: "Hermes",
1933
+ cursor: "Cursor",
1934
+ vscode: "VS Code",
1935
+ opencode: "OpenCode",
1936
+ fx: "fx",
1937
+ ...compatibleRecord((profile) => profile.label),
1938
+ custom: "Other"
1939
+ };
1940
+ var AGENT_CAPABILITIES = {
1941
+ claude_code: "MCP + hooks + auto-allowed tools",
1942
+ codex: "MCP + native hooks + auto-allowed tools",
1943
+ gemini_cli: "MCP + native hooks + auto-allowed tools",
1944
+ hermes: "native plugin + auto-error notifications",
1945
+ cursor: "plugin + permission gate",
1946
+ vscode: "agent plugin + permission gate",
1947
+ opencode: "generated plugin + permission gate",
1948
+ fx: "MCP through a local bridge + questions via push",
1949
+ ...compatibleRecord((profile) => profile.mcp ? "MCP + native hooks" : "native hooks"),
1950
+ custom: "any MCP or HTTP agent (Windsurf, n8n, custom)"
1951
+ };
1952
+ var failedAgentNames = (failures) => failures.map((failure) => AGENT_NAMES[failure.agent]);
1953
+ var agentFailureNextLine = (next) => next.kind === "retry-agent" ? `${dim(RETRY_AGENT_LABEL)} ${cyan(next.command)}` : next.instruction;
1954
+ var printAgentFailures = (failures) => {
1955
+ for (const failure of describeAgentFailures(failures, (agent) => AGENT_NAMES[agent])) {
1956
+ console.log(` ${yellow("!")} ${failure.summary}`);
1957
+ console.log(` ${agentFailureNextLine(failure.next)}`);
1958
+ }
1959
+ };
1960
+ var NAME_COLUMN = Math.max(...Object.values(AGENT_NAMES).map((name) => name.length)) + 2;
1961
+ var AGENT_LABELS = Object.fromEntries(
1962
+ Object.keys(AGENT_NAMES).map((agent) => [
1963
+ agent,
1964
+ `${AGENT_NAMES[agent].padEnd(NAME_COLUMN)}${dim(AGENT_CAPABILITIES[agent])}`
1965
+ ])
1966
+ );
1967
+ var detectAgents = () => {
1968
+ const map = {};
1969
+ for (const entry of detectAllAgents()) {
1970
+ map[entry.agent] = { found: isDetected(entry.result), evidence: describeDetection(entry.result) };
1971
+ }
1972
+ return map;
1973
+ };
1974
+ var canRunBrowserLogin = () => process.stdout.isTTY === true && !process.env.CI;
1975
+ var canShowPairingQr = () => !process.env.CI;
1976
+ var resolveAgents = async (options) => {
1977
+ if (options.agents.kind === "none") return [];
1978
+ if (options.agents.kind === "explicit") {
1979
+ const detected2 = detectAgents();
1980
+ for (const agent of options.agents.agents) {
1981
+ if (agent !== "custom" && !detected2[agent].found) {
1982
+ console.log(` ${yellow("!")} ${AGENT_NAMES[agent]} was not detected on this machine.`);
1983
+ console.log(` ${dim("Continuing anyway. The steps below say what was configured.")}`);
1984
+ }
1985
+ }
1986
+ return options.agents.agents;
1987
+ }
1988
+ const detected = detectAgents();
1989
+ const wantsAuto = options.agents.kind === "auto" || options.nonInteractive;
1990
+ if (wantsAuto) {
1991
+ const picked = Object.keys(detected).filter((agent) => detected[agent].found);
1992
+ if (picked.length === 0) {
1993
+ console.log(` ${yellow("!")} No agents detected, so nothing was configured. Your key is saved.`);
1994
+ console.log(` ${dim("Install an agent and re-run, name one with --agents <id>, or pass --agents none to accept this.")}`);
1995
+ console.log();
1996
+ process.exit(await abortWith("no-agents-detected", EXIT.PROBLEMS_FOUND));
1997
+ }
1998
+ console.log(` ${check} Selected automatically: ${picked.map((a) => AGENT_NAMES[a]).join(", ")} ${dim("(--agents to choose)")}`);
1999
+ return picked;
2000
+ }
2001
+ const hint = Object.values(detected).some((entry) => entry.found) ? "(detected agents pre-selected)" : "(space = toggle, enter = confirm)";
2002
+ return checkbox({
2003
+ message: "Which agents do you use? " + dim(hint),
2004
+ choices: Object.keys(AGENT_LABELS).map((agent) => ({
2005
+ // The evidence rides along so a detection the user disagrees with is
2006
+ // visible and correctable, rather than an unexplained pre-tick.
2007
+ name: detected[agent].evidence ? `${AGENT_LABELS[agent]} ${dim(`[${detected[agent].evidence}]`)}` : AGENT_LABELS[agent],
2008
+ value: agent,
2009
+ short: AGENT_NAMES[agent],
2010
+ checked: detected[agent].found
2011
+ })),
2012
+ // Today an empty selection skips the loop and still prints "Setup complete."
2013
+ validate: (picked) => picked.length > 0 || "Select at least one agent, or re-run with --agents none."
2014
+ });
2015
+ };
2016
+ var AGENT_TARGETS = {
2017
+ claude_code: [CLAUDE_JSON, CLAUDE_SETTINGS, join3(CLAUDE_SKILL_DIR, "SKILL.md")],
2018
+ codex: [join3(CODEX_HOME, "config.toml"), CODEX_HOOKS_JSON, join3(CODEX_SKILL_DIR, "SKILL.md"), CODEX_AGENTS_MD],
2019
+ gemini_cli: [GEMINI_SETTINGS, GEMINI_MD],
2020
+ hermes: ["the Hermes virtualenv (pip install pushary-hermes)"],
2021
+ cursor: [join3(CURSOR_PLUGIN_DIR, "mcp.json"), CURSOR_USER_HOOKS],
2022
+ vscode: [VSCODE_PLUGIN_MCP, ...vscodeSettingsTargets(existsSync)],
2023
+ opencode: [
2024
+ openCodePluginFile(),
2025
+ join3(openCodePluginDir(), "package.json"),
2026
+ openCodeConfigFile(),
2027
+ OPENCODE_INSTRUCTIONS()
2028
+ ],
2029
+ fx: [fxMcpProfile(), fxSettings(), fxAgentsMd()],
2030
+ ...compatibleRecord((profile) => compatibleTargetFiles(profile).map((file) => join3(homedir(), ...file))),
2031
+ custom: ["nothing (prints connection details only)"]
2032
+ };
2033
+ var reportDryRun = (apiKey, agents, keyCheck, controlMode) => {
2034
+ console.log();
2035
+ console.log(` ${bold("Dry run")} ${dim("\u2014 nothing below has been written")}`);
2036
+ console.log();
2037
+ console.log(
2038
+ apiKey ? ` ${dim("Key:")} ${dim(`${apiKey.slice(0, 8)}...${apiKey.slice(-4)}`)} ${dim(`(${keyCheck?.kind ?? "not verified"})`)}` : ` ${dim("Key:")} ${dim("none on this machine \u2014 a real run would acquire one")}`
2039
+ );
2040
+ console.log(` ${dim("Key store:")} ${PUSHARY_CONFIG_FILE}`);
2041
+ console.log(` ${dim("Control:")} ${controlMode ?? "unchanged (legacy install)"}`);
2042
+ const rc = resolveShellRc();
2043
+ console.log(` ${dim("Shell rc:")} ${rc ? rc.path : dim("none for this shell; the export line would be printed instead")}`);
2044
+ console.log();
2045
+ console.log(` ${bold("Also, on a real run")}`);
2046
+ for (const line of describeSetupSideEffects({
2047
+ globalPackage: plannedGlobalPackage(),
2048
+ version: getPackageVersion(),
2049
+ configFile: PUSHARY_CONFIG_FILE,
2050
+ rcPath: rc?.path ?? null,
2051
+ controlMode,
2052
+ serviceDefinitionPath: daemonServiceDefinitionPath()
2053
+ })) console.log(` ${dim(line)}`);
2054
+ console.log();
2055
+ if (agents.length === 0) {
2056
+ console.log(` ${dim("Agents:")} ${dim("none")}`);
2057
+ } else {
2058
+ console.log(` ${dim("Agents:")} ${agents.map((agent) => AGENT_NAMES[agent]).join(", ")}`);
2059
+ for (const agent of agents) {
2060
+ console.log();
2061
+ console.log(` ${bold(AGENT_NAMES[agent])}`);
2062
+ for (const target of AGENT_TARGETS[agent]) console.log(` ${dim("would write")} ${target}`);
2063
+ }
2064
+ }
2065
+ console.log();
2066
+ console.log(` ${dim("Re-run without --dry-run to apply.")}`);
2067
+ console.log();
2068
+ };
2069
+ var printManualSteps = () => {
2070
+ if (manualSteps.length === 0) return;
2071
+ console.log(` ${bold("Still to do")}`);
2072
+ for (const step of manualSteps) console.log(` ${dim("\u2022")} ${step}`);
2073
+ console.log();
2074
+ };
2075
+ var emitJson = (options, payload) => {
2076
+ if (!options.json) return;
2077
+ process.stdout.write(`${JSON.stringify(payload, null, 2)}
2078
+ `);
2079
+ };
2080
+ var main = async () => {
2081
+ const connector = coworkSelection(process.argv.slice(2));
2082
+ if (connector) {
2083
+ process.stdout.write(coworkOutput(connector.json ? "json" : "guide"));
2084
+ return;
2085
+ }
2086
+ const options = parseSetupOptions({
2087
+ argv: process.argv.slice(2),
2088
+ stdinIsTty: process.stdin.isTTY === true
2089
+ });
2090
+ if (options.help) {
2091
+ process.stdout.write(`${renderCommandHelp("setup", getPackageVersion())}
2092
+ `);
2093
+ process.exit(EXIT.OK);
2094
+ }
2095
+ const controlMode = effectiveControlMode(options, readControlMode(), existsSync(PUSHARY_CONFIG_FILE));
2096
+ recordSetupOptions(options);
2097
+ if (options.json) {
2098
+ jsonMode = true;
2099
+ humanStream = process.stderr;
2100
+ setHumanStream(process.stderr);
2101
+ console.log = (...args) => {
2102
+ process.stderr.write(`${args.map((a) => typeof a === "string" ? a : String(a)).join(" ")}
2103
+ `);
2104
+ };
2105
+ }
2106
+ const version = getPackageVersion();
2107
+ console.log();
2108
+ console.log(` ${bold("Pushary")} ${dim("v" + version)}`);
2109
+ console.log(` ${dim("Push notifications for AI agents")}`);
2110
+ console.log();
2111
+ await checkForUpdates(version, !options.dryRun);
2112
+ markStep("resolve-key");
2113
+ const connectMode = options.connectMode;
2114
+ const flagKey = await readKeyFromStdin(options) ?? options.key ?? void 0;
2115
+ const envKey = process.env.PUSHARY_API_KEY?.trim();
2116
+ let trimmedKey;
2117
+ let keyFromPairing = false;
2118
+ const storedKey = readConfigFileKey();
2119
+ const pairingRequested = connectMode === "app" && options.connectExplicit && !options.skipPhone;
2120
+ const ambientEligible = flagKey === void 0 && !pairingRequested;
2121
+ const envCheck = ambientEligible && envKey && isValidApiKey(envKey) ? await usableStoredKey(envKey) : null;
2122
+ const storedCheck = ambientEligible && envCheck === null && storedKey !== null && isValidApiKey(storedKey) && // login writes both, so without this the same key is probed twice.
2123
+ storedKey !== envKey ? await usableStoredKey(storedKey) : null;
2124
+ const appKey = ambientEligible && envCheck === null && storedCheck === null ? pickAppKey({ fromAgents: AGENT_CHOICES.map(appMcpKeyIfReadable), storedKey, envKey }) : null;
2125
+ const appKeyCheck = appKey === null ? null : await usableStoredKey(appKey);
2126
+ let storedKeyCheck = null;
2127
+ if (flagKey) {
2128
+ const masked = `${flagKey.slice(0, 8)}...${flagKey.slice(-4)}`;
2129
+ console.log(` ${check} Using API key: ${dim(masked)}`);
2130
+ console.log();
2131
+ trimmedKey = flagKey;
2132
+ } else if (storedKey !== null && storedCheck !== null) {
2133
+ const masked = `${storedKey.slice(0, 8)}...${storedKey.slice(-4)}`;
2134
+ console.log(` ${check} Using the key already on this machine: ${dim(masked)}`);
2135
+ console.log(` ${dim(PUSHARY_CONFIG_FILE)}`);
2136
+ console.log();
2137
+ trimmedKey = storedKey;
2138
+ storedKeyCheck = storedCheck;
2139
+ } else if (envKey && envCheck !== null) {
2140
+ const masked = `${envKey.slice(0, 8)}...${envKey.slice(-4)}`;
2141
+ console.log(` ${check} Found API key in environment: ${dim(masked)}`);
2142
+ console.log();
2143
+ const useExisting = options.nonInteractive || options.dryRun ? true : await confirm({ message: "Use this key?", default: true });
2144
+ if (useExisting) {
2145
+ trimmedKey = envKey;
2146
+ storedKeyCheck = envCheck;
2147
+ } else {
2148
+ const apiKey = await input({ message: "API key:" });
2149
+ trimmedKey = apiKey.trim();
2150
+ }
2151
+ } else if (appKey !== null && appKeyCheck !== null) {
2152
+ const masked = `${appKey.slice(0, 8)}...${appKey.slice(-4)}`;
2153
+ console.log(` ${check} Using the key the Pushary Mac app is signed in with: ${dim(masked)}`);
2154
+ console.log();
2155
+ trimmedKey = appKey;
2156
+ storedKeyCheck = appKeyCheck;
2157
+ } else if (options.dryRun) {
2158
+ console.log(` ${dim("No key on this machine. A real run would connect one first.")}`);
2159
+ console.log();
2160
+ markStep("resolve-agents");
2161
+ const plannedAgents = await resolveAgents(options);
2162
+ reportDryRun(null, plannedAgents, null, controlMode);
2163
+ emitJson(options, { ok: true, dryRun: true, agents: plannedAgents, control: controlMode, keyCheck: "skipped", key: "none" });
2164
+ return;
2165
+ } else if (connectMode === "app" && canShowPairingQr() && !options.skipPhone) {
2166
+ console.log(` ${dim("Pairing with your Pushary app.")}`);
2167
+ console.log(` ${dim("No app yet? Get it at")} ${cyan("https://pushary.com/download")}${dim(", this keeps waiting.")}`);
2168
+ console.log(` ${dim("No phone? Ctrl-C, then:")} ${cyan(BROWSER_CONNECT_COMMAND)}`);
2169
+ markStep("pair");
2170
+ const previous = readKeySource();
2171
+ const paired = await connectViaAppPairing({
2172
+ onCancelled: async () => {
2173
+ printPairingEscape();
2174
+ await reportSetupAbort("pairing-cancelled", 130);
2175
+ }
2176
+ });
2177
+ if (paired.kind === "failed") {
2178
+ if (pairingEscapeApplies(paired)) printPairingEscape();
2179
+ process.exit(await abortWith(paired.reason === "cancelled" ? "pairing-cancelled" : "pairing-failed", EXIT.NOT_CONFIGURED));
2180
+ }
2181
+ trimmedKey = paired.apiKey;
2182
+ keyFromPairing = true;
2183
+ if (paired.handle) {
2184
+ console.log(` ${check} Connected to ${cyan("@" + paired.handle)}`);
2185
+ }
2186
+ if (previous.key && previous.key !== trimmedKey) {
2187
+ console.log(` ${yellow("!")} The key this machine used before (${keyPrefix(previous.key)}) is still active.`);
2188
+ console.log(` ${dim("Revoke it at")} ${cyan("pushary.com/dashboard/agent/settings")} ${dim("if nothing else uses it.")}`);
2189
+ }
2190
+ console.log();
2191
+ } else if (!canRunBrowserLogin()) {
2192
+ if (pairingRequested) {
2193
+ console.log(` ${yellow("!")} ${cyan("--connect app")} will not draw a pairing QR in CI.`);
2194
+ console.log(` ${dim("Pass --key <pk_xxx.xxx> or --key-stdin here. Outside CI, setup pairs with no terminal needed.")}`);
2195
+ } else if (process.env.CI) {
2196
+ console.log(` ${yellow("!")} No API key, and setup does not pair in CI.`);
2197
+ console.log(` ${dim("Pass --key <pk_xxx.xxx>, or --key-stdin, or export PUSHARY_API_KEY.")}`);
2198
+ } else {
2199
+ console.log(` ${yellow("!")} No API key, and nothing here can acquire one.`);
2200
+ console.log(` ${dim("Pass --key <pk_xxx.xxx>, or --key-stdin, or export PUSHARY_API_KEY.")}`);
2201
+ console.log(` ${dim("Or drop")} ${cyan("--connect browser")}${dim("/")}${cyan("--skip-phone")} ${dim("and setup will pair with your phone instead.")}`);
2202
+ }
2203
+ console.log();
2204
+ process.exit(await abortWith("no-key", EXIT.NOT_CONFIGURED));
2205
+ } else {
2206
+ markStep("browser-login");
2207
+ const outcome = await runBrowserLogin(
2208
+ { say: (line) => console.log(line), bold, dim, cyan, yellow },
2209
+ { cliVersion: version }
2210
+ );
2211
+ if (outcome.kind === "ok") {
2212
+ trimmedKey = outcome.apiKey;
2213
+ console.log();
2214
+ console.log(` ${check} Signed in${outcome.siteSlug ? ` ${dim(`(${outcome.siteSlug})`)}` : ""}`);
2215
+ console.log();
2216
+ } else if (outcome.kind === "rejected") {
2217
+ console.log();
2218
+ console.log(` ${yellow("!")} That request was rejected in the browser. Nothing was written.`);
2219
+ console.log();
2220
+ process.exit(await abortWith("login-rejected", EXIT.NOT_CONFIGURED));
2221
+ } else {
2222
+ console.log();
2223
+ if (outcome.kind === "unavailable") {
2224
+ console.log(` ${yellow("!")} Couldn't reach the login service ${dim(`(${outcome.detail})`)}`);
2225
+ } else {
2226
+ console.log(` ${yellow("!")} The code expired before it was approved.`);
2227
+ }
2228
+ console.log(` ${dim("Paste a key instead, or press Ctrl-C and re-run setup for a fresh code.")}`);
2229
+ console.log(` ${dim("Keys live at")} ${cyan("pushary.com/dashboard/agent/settings")}`);
2230
+ console.log();
2231
+ const apiKey = await input({ message: "API key:" });
2232
+ trimmedKey = apiKey.trim();
2233
+ }
2234
+ }
2235
+ if (!trimmedKey || !isValidApiKey(trimmedKey)) {
2236
+ console.log(`
2237
+ ${yellow("!")} Invalid key format. Expected: ${dim("pk_xxx.xxx")}`);
2238
+ console.log(` ${dim("Copy your key from")} ${cyan("https://pushary.com/dashboard/agent/settings")}`);
2239
+ console.log(` ${dim("Or sign up at")} ${cyan("https://pushary.com/sign-up?from=ai-coding")}
2240
+ `);
2241
+ process.exit(await abortWith("invalid-key", EXIT.USAGE));
2242
+ }
2243
+ if (!options.dryRun) recordSetupKey(trimmedKey);
2244
+ markStep("verify-key");
2245
+ const keyCheck = await verifyKeyOrExit(trimmedKey, options, storedKeyCheck ?? void 0);
2246
+ markStep("resolve-agents");
2247
+ const agents = await resolveAgents(options);
2248
+ if (options.dryRun) {
2249
+ reportDryRun(trimmedKey, agents, keyCheck, controlMode);
2250
+ emitJson(options, { ok: true, dryRun: true, agents, control: controlMode, keyCheck: keyCheck?.kind ?? "skipped" });
2251
+ return;
2252
+ }
2253
+ markStep("save-key");
2254
+ const transcriptSync = transcriptPreferenceForSetup(options.transcripts);
2255
+ if (transcriptSync) {
2256
+ console.log();
2257
+ console.log(" Transcript sync: enabling locally redacted, encrypted sync for Pushary-launched sessions. Your phone can decrypt it; authorized Pushary compliance admins can recover it for documented legal obligations. Disable with pushary setup --transcripts off.");
2258
+ }
2259
+ const rc = await saveApiKey(trimmedKey, controlMode, transcriptSync);
2260
+ if (keyCheck.kind === "ok") clearDaemonNeedsLogin();
2261
+ const context = { rc };
2262
+ const appBridgeAvailable = resolveBridgeStub(
2263
+ join3(pusharyDir(), "bin", HOOK_LOCATOR_BINARY),
2264
+ liveBridgeProbe()
2265
+ ).kind === "bundle";
2266
+ const accountChecks = /* @__PURE__ */ new Map();
2267
+ if (keyCheck) accountChecks.set(trimmedKey, Promise.resolve(keyCheck));
2268
+ const accountCheckFor = (key) => {
2269
+ const existing = accountChecks.get(key);
2270
+ if (existing) return existing;
2271
+ const pending = checkApiKey(key);
2272
+ accountChecks.set(key, pending);
2273
+ return pending;
2274
+ };
2275
+ const appAccountMatches = async (agent) => {
2276
+ const appKey2 = appMcpKeyFor(agent);
2277
+ if (!appKey2) return false;
2278
+ if (appKey2 === trimmedKey) return true;
2279
+ const [cliAccount, appAccount] = await Promise.all([
2280
+ accountCheckFor(trimmedKey),
2281
+ accountCheckFor(appKey2)
2282
+ ]);
2283
+ return samePusharyAccount(cliAccount, appAccount);
2284
+ };
2285
+ const staleEnvironmentKey = Boolean(envKey && envKey !== trimmedKey);
2286
+ if (envKey && staleEnvironmentKey) {
2287
+ noteManual(
2288
+ `This shell still exports the previous key (${keyPrefix(envKey)}), and it wins over the one just saved. Open a new terminal before starting an agent here, or re-export PUSHARY_API_KEY.`
2289
+ );
2290
+ }
2291
+ const failures = [];
2292
+ const completed = [];
2293
+ const skipped = [];
2294
+ const hookOwners = [];
2295
+ let controlLifecycleFailure = null;
2296
+ if (agents.length === 0) {
2297
+ console.log(`
2298
+ ${check} API key saved. No agents configured.`);
2299
+ console.log(` ${dim("Run setup again with --agents <id> when you want one wired up.")}`);
2300
+ } else {
2301
+ for (const [index, agent] of agents.entries()) {
2302
+ markStep(`configure:${agent}`);
2303
+ try {
2304
+ const ownerBefore = hookOwnerFor(agent, appBridgeAvailable);
2305
+ const plan = planHooks({ owner: ownerBefore, takeOverHooks: options.takeOverHooks, nonInteractive: options.nonInteractive });
2306
+ const keepAppWiring = plan === "keep";
2307
+ if (keepAppWiring) {
2308
+ if (!await appAccountMatches(agent)) {
2309
+ hookOwners.push({ agent, owner: "app", kept: true });
2310
+ const failure = describedAgentFailure(agent, "app-account-mismatch");
2311
+ failures.push(failure);
2312
+ console.log(`
2313
+ ${yellow("!")} ${AGENT_NAMES[agent]}: ${failure.detail}`);
2314
+ console.log(` ${dim("Sign the Mac app into the same account and repair this agent, then re-run setup.")}`);
2315
+ console.log(` ${dim("Or pass --take-over-hooks if you intentionally want the CLI to own these hooks.")}`);
2316
+ continue;
2317
+ }
2318
+ console.log(`
2319
+ ${check} ${AGENT_NAMES[agent]}: ${describeAppOwnership(AGENT_NAMES[agent])} ${dim(KEEP_HINT)}`);
2320
+ hookOwners.push({ agent, owner: "app", kept: true });
2321
+ completed.push(agent);
2322
+ continue;
2323
+ }
2324
+ if (ownerBefore === "app") captureTakeoverReceipt(agent);
2325
+ const outcome = await AGENT_SETUP[agent](trimmedKey, context);
2326
+ if (outcome === "skipped") {
2327
+ skipped.push(agent);
2328
+ } else if (agentIsWired(agent)) {
2329
+ completed.push(agent);
2330
+ hookOwners.push({ agent, owner: hookOwnerFor(agent, appBridgeAvailable), kept: false });
2331
+ } else {
2332
+ const failure = describedAgentFailure(agent, "not-wired");
2333
+ failures.push(failure);
2334
+ console.log(` ${yellow("!")} ${AGENT_NAMES[agent]} ${failure.detail}`);
2335
+ console.log(` ${dim("Re-run setup, then")} ${cyan("npx @pushary/agent-hooks@latest doctor")} ${dim("to see what is missing.")}`);
2336
+ }
2337
+ } catch (err) {
2338
+ const failure = agentFailureFromError(agent, err, formatError);
2339
+ failures.push(failure);
2340
+ console.log(` ${yellow("!")} ${AGENT_NAMES[agent]} setup failed: ${failure.detail}`);
2341
+ const continuation = afterAgentFailureNote(agents.length - index - 1);
2342
+ if (continuation) console.log(` ${dim(continuation)}`);
2343
+ }
2344
+ }
2345
+ if (skipped.length > 0) {
2346
+ console.log();
2347
+ console.log(` ${yellow("!")} Not configured: ${skipped.map((a) => a.replace("_", " ")).join(", ")} ${dim("(not installed on this machine)")}`);
2348
+ }
2349
+ if (failures.length > 0) {
2350
+ console.log();
2351
+ const configured = failedAgentsContext(completed.map((agent) => AGENT_NAMES[agent]));
2352
+ console.log(` ${yellow("!")} Failed: ${failedAgentNames(failures).join(", ")}${configured ? ` ${dim(configured)}` : ""}`);
2353
+ for (const followUp of agentFailureFollowUps(failures)) {
2354
+ if (followUp === "install-node") console.log(` ${INSTALL_NODE_STEP}`);
2355
+ else console.log(` ${dim("Run")} npx @pushary/agent-hooks@latest doctor ${dim("for the reason, or send it to")} ${SUPPORT_EMAIL}`);
2356
+ }
2357
+ }
2358
+ markStep("project-instructions");
2359
+ await offerProjectInstructions(agents, options);
2360
+ if (completed.includes("claude_code")) {
2361
+ await offerClaudeAlias(options);
2362
+ }
2363
+ }
2364
+ if (controlMode === "off") {
2365
+ markStep("control:disable");
2366
+ const disabled = await disableDaemonService();
2367
+ if (disabled.kind !== "ok") {
2368
+ controlLifecycleFailure = disabled.detail;
2369
+ console.log(` ${yellow("!")} Could not fully disable phone-start: ${controlLifecycleFailure}`);
2370
+ } else {
2371
+ console.log(` ${check} Phone-start background service disabled.`);
2372
+ }
2373
+ }
2374
+ markStep("connect-phone");
2375
+ const skipPhone = options.skipPhone;
2376
+ let phone = null;
2377
+ const verifyMode = effectiveVerify(options, { paired: keyFromPairing });
2378
+ const phoneCheck = keyCheck.kind === "unreachable" ? "unverified" : planPhoneCheck({
2379
+ locked: keyCheck?.kind === "locked",
2380
+ skipPhone,
2381
+ verify: verifyMode,
2382
+ stdoutIsTty: process.stdout.isTTY === true,
2383
+ paired: keyFromPairing,
2384
+ connectMode
2385
+ });
2386
+ switch (phoneCheck) {
2387
+ case "unverified":
2388
+ console.log();
2389
+ console.log(` ${dim("Skipping the delivery check: account readiness could not be verified. Re-run setup once online.")}`);
2390
+ break;
2391
+ case "locked":
2392
+ console.log();
2393
+ console.log(` ${dim("Skipping the delivery check: no approval can be delivered until the plan is active.")}`);
2394
+ break;
2395
+ case "skip-phone":
2396
+ console.log();
2397
+ console.log(` ${dim("Skipping the phone check (--skip-phone).")}`);
2398
+ break;
2399
+ case "verify-none":
2400
+ console.log();
2401
+ console.log(` ${dim("Skipping the delivery check (--verify none). Nothing here proves an approval reaches you.")}`);
2402
+ break;
2403
+ case "instructions":
2404
+ phone = await printConnectInstructions(trimmedKey);
2405
+ break;
2406
+ case "confirm-pairing":
2407
+ phone = await confirmAppConnection(trimmedKey);
2408
+ break;
2409
+ case "reuse-app":
2410
+ phone = await printConnectInstructions(trimmedKey);
2411
+ break;
2412
+ case "connect":
2413
+ phone = await connectDevice(trimmedKey, { showCapabilities: !options.connectExplicit });
2414
+ break;
2415
+ }
2416
+ let roundTrip = null;
2417
+ if (verifyMode === "roundtrip" && keyCheck.kind === "ok" && !skipPhone && // Reaching nobody is already reported by the delivery check above, in copy
2418
+ // that names the fix. Asking a question nobody can receive would repeat it
2419
+ // 55 seconds later and add nothing.
2420
+ //
2421
+ // `unattributable` is the exception and the case this is best at. It means
2422
+ // the push landed on a multi-member site through a channel that carries no
2423
+ // owner, so nothing in the counts can say whether it was THIS user's device.
2424
+ // A tap settles it, which is why describeVerifyOutcome already tells people
2425
+ // to run `doctor --roundtrip` here. Doing it inline saves the round trip
2426
+ // being a thing they have to know to run.
2427
+ (phone?.reachesYou || phone?.outcome?.kind === "unattributable")) {
2428
+ console.log();
2429
+ console.log(` ${dim("Test delivery and answer return. Agent hooks are a separate check.")}`);
2430
+ await spinner("Waiting for you to tap the notification", async () => {
2431
+ roundTrip = await verifyRoundTrip(trimmedKey);
2432
+ });
2433
+ const report = describeRoundTrip(roundTrip);
2434
+ console.log(` ${report.ok ? check : yellow("!")} ${report.message}`);
2435
+ for (const line of report.next) console.log(` ${dim(line)}`);
2436
+ } else if (verifyMode === "roundtrip" && keyCheck.kind === "ok") {
2437
+ console.log();
2438
+ console.log(` ${dim("Skipped the round trip: nothing is connected that could answer it.")}`);
2439
+ }
2440
+ console.log();
2441
+ if (keyCheck.kind === "locked" || keyCheck.kind === "unreachable") {
2442
+ const report = describeKeyCheck(keyCheck);
2443
+ const next = [...report.next, "Finish activation: pushary setup (reuses the saved key)."];
2444
+ const exitCode2 = failures.length > 0 || controlLifecycleFailure ? EXIT.PROBLEMS_FOUND : report.exitCode;
2445
+ printManualSteps();
2446
+ console.log(` ${yellow(bold(keyCheck.kind === "locked" ? "Agents configured, approvals inactive." : "Configuration saved, activation unverified."))}`);
2447
+ if (failures.length > 0) {
2448
+ console.log();
2449
+ printAgentFailures(failures);
2450
+ }
2451
+ console.log();
2452
+ console.log(` ${report.message}`);
2453
+ for (const line of next) console.log(` ${dim(line)}`);
2454
+ console.log();
2455
+ emitJson(options, {
2456
+ ok: false,
2457
+ next,
2458
+ reason: keyCheck.kind === "locked" ? "plan_inactive" : "readiness_unverified",
2459
+ configured: completed,
2460
+ skipped,
2461
+ manualSteps,
2462
+ failed: failedAgentNames(failures),
2463
+ agentFailures: failures,
2464
+ keyCheck: keyCheck.kind,
2465
+ rc: rc.kind,
2466
+ control: controlMode,
2467
+ daemon: controlLifecycleFailure ? "unavailable" : "skipped",
2468
+ exitCode: exitCode2
2469
+ });
2470
+ await reportSetupOutcome({
2471
+ options,
2472
+ completed,
2473
+ skipped,
2474
+ failures,
2475
+ closingReason: unverifiedKeyClosingReason({
2476
+ failedCount: failures.length,
2477
+ controlLifecycleFailed: controlLifecycleFailure !== null,
2478
+ keyCheck: keyCheck.kind
2479
+ }),
2480
+ connectOutcome: "skipped",
2481
+ keyCheck: keyCheck.kind,
2482
+ exitCode: exitCode2
2483
+ });
2484
+ process.exit(exitCode2);
2485
+ }
2486
+ printManualSteps();
2487
+ const configuredNothing = agents.length > 0 && completed.length === 0;
2488
+ const agentsOk = failures.length === 0 && !configuredNothing;
2489
+ const reachChecked = phone !== null;
2490
+ const reachesYou = phone?.reachesYou ?? true;
2491
+ markStep("closing");
2492
+ const keyIsTrusted = keyCheck.kind === "ok";
2493
+ const managedControlAgents = ["claude_code", "codex", "gemini_cli"];
2494
+ const configuredControlAgents = managedControlAgents.filter((agent) => agentIsWired(agent));
2495
+ const configuredControlProviders = configuredControlAgents.map(
2496
+ (agent) => agent === "claude_code" ? "claude" : agent === "gemini_cli" ? "gemini" : agent
2497
+ );
2498
+ const controlCapabilities = detectProviderLiveCapabilities(configuredControlProviders);
2499
+ const providerAvailable = hasSpawnProvider(controlCapabilities);
2500
+ const managerAvailable = daemonServiceManagerAvailable();
2501
+ const servicePresent = readDaemonStatus().kind !== "absent" || hasDaemonServiceDefinition();
2502
+ let controlPolicy = resolveControlPolicy({
2503
+ mode: controlMode,
2504
+ providerAvailable,
2505
+ managerAvailable,
2506
+ servicePresent
2507
+ });
2508
+ if (controlPolicy === "legacy") {
2509
+ let legacyRuntimeFailure = null;
2510
+ try {
2511
+ await installGlobally2();
2512
+ const replaced = await replaceDaemonService();
2513
+ if (replaced.kind !== "ok") throw new Error(replaced.detail);
2514
+ } catch (err) {
2515
+ legacyRuntimeFailure = formatError(err);
2516
+ }
2517
+ let retired = legacyRuntimeFailure !== null;
2518
+ let stopped = retired ? await disableDaemonService() : await stopDaemonService();
2519
+ if (!retired && stopped.kind !== "ok") {
2520
+ legacyRuntimeFailure = stopped.detail;
2521
+ retired = true;
2522
+ stopped = await disableDaemonService();
2523
+ }
2524
+ if (stopped.kind !== "ok") {
2525
+ controlLifecycleFailure = [legacyRuntimeFailure, stopped.detail].filter(Boolean).join("; ");
2526
+ console.log(` ${yellow("!")} Could not make the legacy phone-start service safe: ${controlLifecycleFailure}`);
2527
+ } else if (retired) {
2528
+ controlPolicy = resolveControlPolicy({
2529
+ mode: controlMode,
2530
+ providerAvailable,
2531
+ managerAvailable,
2532
+ servicePresent: false
2533
+ });
2534
+ console.log(` ${yellow("!")} Retired the legacy phone-start service because it could not be safely repinned: ${legacyRuntimeFailure}`);
2535
+ }
2536
+ if (!retired && stopped.kind === "ok") warnVersionManagerPin(process.execPath);
2537
+ }
2538
+ if (controlMode === "on" && controlPolicy !== "enabled") {
2539
+ console.log(` ${yellow("!")} Phone-start was required: ${!providerAvailable ? "install a supported Claude Code, Codex or Gemini provider" : "no native background-service manager is available in this OS session"}.`);
2540
+ } else if (controlPolicy === "legacy") {
2541
+ console.log(` ${yellow("!")} A legacy phone-start service cannot be safely adopted in this session. Disable it with --control off or restore its provider and native service manager.`);
2542
+ }
2543
+ const daemon = keyIsTrusted && controlPolicy === "enabled" ? await (async () => {
2544
+ try {
2545
+ await installGlobally2();
2546
+ const service = await replaceDaemonService();
2547
+ if (service.kind === "ok") warnVersionManagerPin(process.execPath);
2548
+ return service.kind === "ok" ? ensureDaemonRunning({ spawnDaemon: () => {
2549
+ } }) : { kind: "unavailable", detail: service.detail };
2550
+ } catch (err) {
2551
+ return { kind: "unavailable", detail: formatError(err) };
2552
+ }
2553
+ })() : controlMode === "on" || controlPolicy === "legacy" ? { kind: "unavailable", detail: "Managed control is required but no usable provider/service-manager pair is available" } : null;
2554
+ const localControlProof = daemon?.kind === "running" || daemon?.kind === "started" ? await waitForDaemonRemoteProof() : null;
2555
+ const serverControlProof = localControlProof && daemonHasRemoteProof(localControlProof) ? await waitForMachineReadiness(trimmedKey, ["spawn", ...controlCapabilities]) : null;
2556
+ const daemonReady = daemon === null ? null : localControlProof !== null && daemonHasRemoteProof(localControlProof) && (serverControlProof?.kind === "ready" || serverControlProof?.kind === "unsupported");
2557
+ if (serverControlProof?.kind === "unsupported") {
2558
+ console.log(` ${yellow("!")} This server predates control-readiness reporting; local heartbeat and queue checks passed.`);
2559
+ } else if (serverControlProof && serverControlProof.kind !== "ready") {
2560
+ console.log(` ${yellow("!")} Managed control could not be verified by the server: ${serverControlProof.detail}`);
2561
+ }
2562
+ const managedControl = readManagedControlEvidence(trimmedKey, configuredControlProviders, controlLifecycleFailure ? null : serverControlProof);
2563
+ const closing = describeClosing({
2564
+ staleEnvironmentKey,
2565
+ agentsOk,
2566
+ configuredNothing,
2567
+ completed,
2568
+ failed: failedAgentNames(failures),
2569
+ reachChecked,
2570
+ reachesYou,
2571
+ managedControlReady: !controlLifecycleFailure && managedControl.ready,
2572
+ daemonReady: controlLifecycleFailure ? false : daemonReady,
2573
+ transcriptSync,
2574
+ // null when the check did not run, so it changes nothing. Only an outright
2575
+ // `false` is allowed to hold back "Setup complete.".
2576
+ roundTripAnswered: roundTrip === null ? null : roundTrip.kind === "answered" ? true : roundTrip.kind === "unanswered" ? false : null
2577
+ });
2578
+ console.log(` ${closing.ok ? green(bold(closing.headline)) : yellow(bold(closing.headline))}`);
2579
+ if (failures.length > 0) {
2580
+ console.log();
2581
+ for (const agent of completed) console.log(` ${check} ${AGENT_NAMES[agent] ?? agent}`);
2582
+ printAgentFailures(failures);
2583
+ }
2584
+ const ownershipLine = describeHookOwnership(hookOwners, AGENT_NAMES);
2585
+ if (ownershipLine) console.log(` ${dim(ownershipLine)}`);
2586
+ console.log();
2587
+ for (const line of closing.next) console.log(` ${line}`);
2588
+ console.log();
2589
+ console.log(` ${dim("Useful:")}`);
2590
+ for (const line of renderCommandSurface()) console.log(` ${dim(line)}`);
2591
+ console.log();
2592
+ const exitCode = closing.exitCode;
2593
+ await reportSetupOutcome({
2594
+ options,
2595
+ completed,
2596
+ skipped,
2597
+ failures,
2598
+ closingReason: closing.reason,
2599
+ // 'answered' is finally reachable. It was in the enum from the start and
2600
+ // nothing produced it, because setup stopped at a delivered notification, so
2601
+ // the one value meaning "a human tapped" was never emitted by the command
2602
+ // whose job is to prove a human can.
2603
+ connectOutcome: roundTripOutcome(roundTrip) ?? (phone === null ? "skipped" : phone.reachesYou ? "delivered" : "unreachable"),
2604
+ keyCheck: keyCheck?.kind ?? "skipped",
2605
+ exitCode
2606
+ });
2607
+ emitJson(options, {
2608
+ ok: closing.ok,
2609
+ configured: completed,
2610
+ skipped,
2611
+ manualSteps,
2612
+ failed: failedAgentNames(failures),
2613
+ agentFailures: failures,
2614
+ hookOwners,
2615
+ keyCheck: keyCheck?.kind ?? "skipped",
2616
+ reach: phone === null ? "skipped" : phone.reachesYou ? "yours" : phone.outcome?.kind ?? phone.detail ?? "unreachable",
2617
+ // Named separately from `reach` so a caller can tell "a push landed" from
2618
+ // "a person answered" without inferring it. 'skipped' means the check did
2619
+ // not run, never that it failed.
2620
+ roundTrip: roundTrip === null ? "skipped" : roundTrip.kind,
2621
+ roundTripEvidence: roundTripEvidence(roundTrip),
2622
+ hookActivation: "unverified",
2623
+ rc: rc.kind,
2624
+ rcPath: rc.path,
2625
+ daemon: controlLifecycleFailure ? "unavailable" : daemon?.kind ?? "skipped",
2626
+ control: controlMode,
2627
+ controlPolicy,
2628
+ controlCapabilities,
2629
+ managedControl,
2630
+ exitCode
2631
+ });
2632
+ if (exitCode !== EXIT.OK) {
2633
+ if (configuredNothing && failures.length === 0) {
2634
+ console.log(` ${yellow("!")} Nothing was configured: none of the agents you asked for are installed here.`);
2635
+ console.log(` ${dim("Your key is saved. Install one and re-run, or pass --agents none to accept this.")}`);
2636
+ console.log();
2637
+ }
2638
+ process.exit(exitCode);
2639
+ }
2640
+ };
2641
+ main().catch(async (err) => {
2642
+ if (err instanceof UsageError) {
2643
+ console.log();
2644
+ console.log(` ${yellow("!")} ${err.message}`);
2645
+ console.log(` ${dim("Run")} ${cyan("pushary setup --help")} ${dim("for the full list.")}`);
2646
+ console.log();
2647
+ process.exit(EXIT.USAGE);
2648
+ }
2649
+ if (isExitPromptError(err)) {
2650
+ console.log();
2651
+ console.log(` ${dim("Cancelled. Nothing further was changed.")}`);
2652
+ console.log();
2653
+ process.exit(await abortWith("interrupted", 130));
2654
+ }
2655
+ console.log();
2656
+ console.log(` ${yellow("!")} Setup failed: ${formatError(err)}`);
2657
+ console.log(` ${dim("Run")} ${cyan(`${NPX_LATEST_COMMAND} doctor`)} ${dim("after fixing the issue, then rerun setup.")}`);
2658
+ console.log();
2659
+ process.exit(await abortWith("crashed", EXIT.FAILED));
2660
+ });