@pushary/agent-hooks 0.60.0 → 0.61.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 (53) hide show
  1. package/CHANGELOG.md +113 -0
  2. package/README.md +52 -9
  3. package/dist/bin/pushary-claude.js +3 -3
  4. package/dist/bin/pushary-clean.js +26 -11
  5. package/dist/bin/pushary-codex-hook.js +3 -3
  6. package/dist/bin/pushary-codex.js +3 -3
  7. package/dist/bin/pushary-connect.d.ts +1 -0
  8. package/dist/bin/pushary-connect.js +92 -0
  9. package/dist/bin/pushary-daemon.js +7 -2
  10. package/dist/bin/pushary-doctor.js +142 -46
  11. package/dist/bin/pushary-gemini-hook.js +3 -3
  12. package/dist/bin/pushary-hook.js +9 -4
  13. package/dist/bin/pushary-login.d.ts +1 -0
  14. package/dist/bin/pushary-login.js +156 -0
  15. package/dist/bin/pushary-logout.d.ts +1 -0
  16. package/dist/bin/pushary-logout.js +102 -0
  17. package/dist/bin/pushary-mode.js +24 -13
  18. package/dist/bin/pushary-notification-hook.js +3 -3
  19. package/dist/bin/pushary-permission-denied-hook.js +4 -4
  20. package/dist/bin/pushary-permission-hook.js +4 -4
  21. package/dist/bin/pushary-post-hook.js +3 -3
  22. package/dist/bin/pushary-prompt-hook.js +3 -3
  23. package/dist/bin/pushary-session-end-hook.js +3 -3
  24. package/dist/bin/pushary-session-start-hook.js +3 -3
  25. package/dist/bin/pushary-setup.js +858 -547
  26. package/dist/bin/pushary-stats.js +6 -1
  27. package/dist/bin/pushary-status.d.ts +1 -0
  28. package/dist/bin/pushary-status.js +206 -0
  29. package/dist/bin/pushary-stop-hook.js +3 -3
  30. package/dist/bin/pushary-stopfailure-hook.js +3 -3
  31. package/dist/bin/pushary-suggestions.js +10 -5
  32. package/dist/bin/pushary-upgrade.js +11 -6
  33. package/dist/bin/pushary-wait.js +21 -12
  34. package/dist/bin/pushary.js +42 -60
  35. package/dist/{chunk-NKXSILEW.js → chunk-2UMNXADU.js} +18 -2
  36. package/dist/chunk-3EGEA4KH.js +44 -0
  37. package/dist/chunk-7QLSKOSU.js +19 -0
  38. package/dist/chunk-B26DNXCA.js +235 -0
  39. package/dist/chunk-GX64YGU3.js +544 -0
  40. package/dist/{chunk-BE5X3WXL.js → chunk-HI4AGGE6.js} +2 -2
  41. package/dist/{chunk-DWED7BS3.js → chunk-HNTKTK5B.js} +1 -1
  42. package/dist/chunk-HUJQSP4F.js +18 -0
  43. package/dist/{chunk-J7JWI3KU.js → chunk-MUEW424A.js} +19 -1
  44. package/dist/chunk-N4DA4CJB.js +57 -0
  45. package/dist/chunk-QHOVJXOT.js +255 -0
  46. package/dist/chunk-R6C4USIV.js +44 -0
  47. package/dist/{chunk-BQLCELYC.js → chunk-S4TFBJ5O.js} +7 -0
  48. package/dist/chunk-SML23YOT.js +216 -0
  49. package/dist/chunk-TX7KBKT7.js +79 -0
  50. package/dist/chunk-UKNAAVEE.js +217 -0
  51. package/dist/{chunk-WNRYRN2R.js → chunk-VFBYRR2N.js} +2 -2
  52. package/dist/src/index.js +4 -4
  53. package/package.json +11 -4
@@ -1,8 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
- installClaudeAlias,
4
- resolveShellRc
5
- } from "../chunk-BC3VCZ3E.js";
3
+ runBrowserLogin
4
+ } from "../chunk-QHOVJXOT.js";
5
+ import {
6
+ addClaudeMcpServer,
7
+ addPusharyHooks,
8
+ addPusharyToolPermissions
9
+ } from "../chunk-CAJZAFVS.js";
6
10
  import {
7
11
  GEMINI_HOOK_BINARY,
8
12
  addCodexHookTrust,
@@ -13,365 +17,351 @@ import {
13
17
  renderAgentInstructions,
14
18
  renderProjectAgentInstructions,
15
19
  writeInstructionBlock
16
- } from "../chunk-BQLCELYC.js";
20
+ } from "../chunk-S4TFBJ5O.js";
21
+ import {
22
+ confirmAppConnection,
23
+ connectDevice,
24
+ connectViaAppPairing,
25
+ printConnectInstructions,
26
+ setHumanStream
27
+ } from "../chunk-GX64YGU3.js";
28
+ import "../chunk-3EGEA4KH.js";
29
+ import {
30
+ parseFlags
31
+ } from "../chunk-N4DA4CJB.js";
32
+ import {
33
+ KEY_FILE_MODE,
34
+ backupFile,
35
+ exportLine,
36
+ readJsonSafe,
37
+ writeFileAtomic,
38
+ writeJsonAtomic,
39
+ writeKey
40
+ } from "../chunk-B26DNXCA.js";
41
+ import {
42
+ checkApiKey,
43
+ describeKeyCheck
44
+ } from "../chunk-UKNAAVEE.js";
45
+ import {
46
+ UsageError
47
+ } from "../chunk-7QLSKOSU.js";
48
+ import {
49
+ installClaudeAlias,
50
+ resolveShellRc
51
+ } from "../chunk-BC3VCZ3E.js";
17
52
  import {
18
- execNpm,
53
+ installGlobally,
19
54
  npmErrorMessage,
20
55
  quoteIfNeeded,
21
56
  resolveGlobalBinDir,
22
57
  resolveGlobalBinary
23
- } from "../chunk-J7JWI3KU.js";
58
+ } from "../chunk-MUEW424A.js";
24
59
  import {
25
- addClaudeMcpServer,
26
- addPusharyHooks,
27
- addPusharyToolPermissions
28
- } from "../chunk-CAJZAFVS.js";
60
+ EXIT,
61
+ getPackageVersion,
62
+ renderCommandHelp
63
+ } from "../chunk-SML23YOT.js";
29
64
  import {
30
65
  reportEvent
31
- } from "../chunk-WNRYRN2R.js";
66
+ } from "../chunk-VFBYRR2N.js";
32
67
  import "../chunk-RN3NOEJF.js";
33
- import "../chunk-DWED7BS3.js";
68
+ import "../chunk-HNTKTK5B.js";
34
69
  import {
35
70
  isValidApiKey
36
71
  } from "../chunk-DQAN3JQP.js";
37
- import "../chunk-NKXSILEW.js";
72
+ import "../chunk-2UMNXADU.js";
38
73
 
39
74
  // bin/pushary-setup.ts
40
- import { existsSync, readFileSync, writeFileSync, appendFileSync, mkdirSync, cpSync, rmSync, chmodSync } from "fs";
41
- import { join, dirname, basename } from "path";
42
- import { homedir } from "os";
43
- import { execSync as execSync2 } from "child_process";
75
+ import { existsSync as existsSync2, readFileSync, writeFileSync, mkdirSync, cpSync, rmSync, renameSync } from "fs";
76
+ import { join as join2, dirname, basename } from "path";
77
+ import { homedir as homedir2, tmpdir } from "os";
78
+ import { execSync as execSync3 } from "child_process";
44
79
  import { checkbox, input, confirm } from "@inquirer/prompts";
45
80
  import { fileURLToPath } from "url";
46
81
  import { parse as parseTOML, stringify as stringifyTOML } from "smol-toml";
47
82
 
48
- // src/onboarding.ts
49
- import qrcodeTerminal from "qrcode-terminal";
50
-
51
- // src/pairing.ts
52
- import crypto from "crypto";
53
- var X25519_SPKI_PREFIX = Buffer.from("302a300506032b656e032100", "hex");
54
- var HKDF_INFO = Buffer.from("pushary-pair-v1");
55
- var HEADER_BYTES = 32 + 12 + 16;
56
- var rawToPublicKey = (raw) => crypto.createPublicKey({
57
- key: Buffer.concat([X25519_SPKI_PREFIX, raw]),
58
- format: "der",
59
- type: "spki"
60
- });
61
- var publicKeyToRaw = (key) => key.export({ format: "der", type: "spki" }).subarray(-32);
62
- var generateKeypair = () => {
63
- const { publicKey, privateKey } = crypto.generateKeyPairSync("x25519");
64
- const publicKeyRaw = publicKeyToRaw(publicKey);
65
- return { publicKeyB64: publicKeyRaw.toString("base64"), publicKeyRaw, privateKey };
66
- };
67
- var publicKeyFingerprint = (b64) => {
68
- const raw = Buffer.from(b64, "base64");
69
- const digest = crypto.createHash("sha256").update(raw).digest("hex");
70
- return (digest.slice(0, 8).match(/.{2}/g) ?? []).join(" ").toUpperCase();
71
- };
72
- var openSealedKey = (blobB64, keypair) => {
73
- const blob = Buffer.from(blobB64, "base64");
74
- if (blob.length <= HEADER_BYTES) throw new Error("sealed payload too short");
75
- const ephemeralRaw = blob.subarray(0, 32);
76
- const nonce = blob.subarray(32, 44);
77
- const tag = blob.subarray(44, 60);
78
- const ciphertext = blob.subarray(60);
79
- const shared = crypto.diffieHellman({
80
- privateKey: keypair.privateKey,
81
- publicKey: rawToPublicKey(ephemeralRaw)
82
- });
83
- const salt = Buffer.concat([ephemeralRaw, keypair.publicKeyRaw]);
84
- const key = Buffer.from(crypto.hkdfSync("sha256", shared, salt, HKDF_INFO, 32));
85
- const decipher = crypto.createDecipheriv("aes-256-gcm", key, nonce);
86
- decipher.setAuthTag(tag);
87
- return Buffer.concat([decipher.update(ciphertext), decipher.final()]).toString("utf8");
88
- };
89
-
90
- // src/onboarding.ts
91
- var dim = (s) => `\x1B[2m${s}\x1B[0m`;
92
- var bold = (s) => `\x1B[1m${s}\x1B[0m`;
93
- var green = (s) => `\x1B[32m${s}\x1B[0m`;
94
- var cyan = (s) => `\x1B[36m${s}\x1B[0m`;
95
- var yellow = (s) => `\x1B[33m${s}\x1B[0m`;
96
- var check = green("\u2713");
97
- var API_BASE = process.env.PUSHARY_API_URL?.trim() || "https://pushary.com";
98
- var CONNECT_POLL_INTERVAL_MS = 2500;
99
- var CONNECT_TIMEOUT_MS = 18e4;
100
- var DELIVERY_POLL_INTERVAL_MS = 1500;
101
- var DELIVERY_TIMEOUT_MS = 2e4;
102
- var authHeaders = (apiKey) => ({
103
- "Content-Type": "application/json",
104
- Authorization: `Bearer ${apiKey}`
105
- });
106
- var getJson = async (url, apiKey) => {
107
- try {
108
- const res = await fetch(url, { headers: authHeaders(apiKey), signal: AbortSignal.timeout(1e4) });
109
- if (!res.ok) return null;
110
- return await res.json().catch(() => null);
111
- } catch {
112
- return null;
113
- }
114
- };
115
- var fetchSite = async (apiKey) => {
116
- const data = await getJson(`${API_BASE}/api/v1/server/site`, apiKey);
117
- if (!data || typeof data.slug !== "string" || typeof data.subscribeUrl !== "string") return null;
118
- return { slug: data.slug, name: typeof data.name === "string" ? data.name : data.slug, subscribeUrl: data.subscribeUrl };
119
- };
120
- var activeSubscriberCount = async (apiKey) => {
121
- const data = await getJson(`${API_BASE}/api/v1/server/subscribers/count`, apiKey);
122
- return data && typeof data.active === "number" ? data.active : 0;
123
- };
124
- var sendTestNotification = async (apiKey) => {
125
- const res = await fetch(`${API_BASE}/api/v1/server/send`, {
126
- method: "POST",
127
- headers: authHeaders(apiKey),
128
- body: JSON.stringify({
129
- title: "Pushary is connected",
130
- body: "Your AI agent can now reach you right here.",
131
- metadata: { source: "cli-onboarding" }
132
- }),
133
- signal: AbortSignal.timeout(15e3)
134
- });
135
- const data = await res.json().catch(() => ({}));
136
- if (!res.ok) throw new Error(typeof data.error === "string" ? data.error : res.statusText);
137
- return Array.isArray(data.notificationIds) ? data.notificationIds : [];
138
- };
139
- var isDelivered = async (apiKey, id) => {
140
- const data = await getJson(`${API_BASE}/api/v1/server/notifications/${id}`, apiKey);
141
- return !!data && (data.status === "delivered" || data.deliveredAt != null);
142
- };
143
- var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
144
- var startSpinner = (getLabel) => {
145
- const frames = [" ", ". ", ".. ", "..."];
146
- let i = 0;
147
- const interval = setInterval(() => {
148
- process.stdout.write(`\r ${dim(frames[i++ % frames.length])} ${getLabel()}`);
149
- }, 200);
150
- return (finalGlyph, finalLabel) => {
151
- clearInterval(interval);
152
- process.stdout.write(`\r ${finalGlyph} ${finalLabel}\x1B[K
153
- `);
154
- };
155
- };
156
- var printQr = (url) => new Promise((resolve, reject) => {
157
- try {
158
- qrcodeTerminal.generate(url, { small: true }, (qr) => {
159
- process.stdout.write("\n" + qr.split("\n").map((line) => " " + line).join("\n") + "\n");
160
- resolve();
161
- });
162
- } catch (err) {
163
- reject(err);
83
+ // src/setup/closing.ts
84
+ var ACTIVITY_URL = "pushary.com/dashboard/agent/activity";
85
+ var nextSteps = (facts, reachable) => {
86
+ const lines = [];
87
+ if (facts.completed.includes("claude_code")) {
88
+ lines.push("pushary claude run Claude Code so your phone can start and steer it");
164
89
  }
165
- });
166
- var waitForDevice = async (apiKey) => {
167
- const deadline = Date.now() + CONNECT_TIMEOUT_MS;
168
- const stop = startSpinner(() => {
169
- const left = Math.max(0, Math.ceil((deadline - Date.now()) / 1e3));
170
- return `Waiting for your phone ${dim(`(${left}s)`)}`;
171
- });
172
- try {
173
- while (Date.now() < deadline) {
174
- if (await activeSubscriberCount(apiKey) > 0) {
175
- stop(check, "Phone connected");
176
- return true;
177
- }
178
- await sleep(CONNECT_POLL_INTERVAL_MS);
179
- }
180
- stop(yellow("!"), "Didn't detect a connection yet");
181
- return false;
182
- } catch (err) {
183
- stop(yellow("!"), `Couldn't check connection ${dim(`(${err instanceof Error ? err.message : "network error"})`)}`);
184
- return false;
90
+ if (reachable) {
91
+ lines.push("pushary daemon keep this machine reachable with nothing running");
92
+ } else {
93
+ lines.push("pushary connect connect a phone without re-running setup");
185
94
  }
95
+ lines.push("pushary doctor re-check everything");
96
+ return lines;
186
97
  };
187
- var sendAndConfirm = async (apiKey) => {
188
- let notificationIds = [];
189
- const stopSend = startSpinner(() => "Sending a test notification");
190
- try {
191
- notificationIds = await sendTestNotification(apiKey);
192
- stopSend(check, "Test notification sent");
193
- } catch (err) {
194
- stopSend(yellow("!"), `Couldn't send test notification ${dim(`(${err instanceof Error ? err.message : "error"})`)}`);
195
- return;
98
+ var describeClosing = (facts) => {
99
+ const reachable = facts.reachChecked && facts.reachesYou;
100
+ if (facts.configuredNothing) {
101
+ return {
102
+ ok: false,
103
+ headline: "Setup failed.",
104
+ next: ["Nothing was configured. Run pushary doctor to see why."],
105
+ exitCode: EXIT.FAILED
106
+ };
196
107
  }
197
- if (notificationIds.length === 0) return;
198
- const deadline = Date.now() + DELIVERY_TIMEOUT_MS;
199
- const stop = startSpinner(() => "Confirming it reached your phone");
200
- while (Date.now() < deadline) {
201
- const results = await Promise.all(notificationIds.map((id) => isDelivered(apiKey, id)));
202
- if (results.some(Boolean)) {
203
- stop(check, "Received on your phone");
204
- return;
205
- }
206
- await sleep(DELIVERY_POLL_INTERVAL_MS);
108
+ if (facts.failed.length > 0) {
109
+ const count = facts.failed.length;
110
+ return {
111
+ ok: false,
112
+ headline: `Setup finished with ${count} problem${count === 1 ? "" : "s"}.`,
113
+ next: [
114
+ ...nextSteps(facts, reachable),
115
+ "",
116
+ `Approvals: ${ACTIVITY_URL}`
117
+ ],
118
+ exitCode: EXIT.PROBLEMS_FOUND
119
+ };
207
120
  }
208
- stop(yellow("!"), "Sent \u2014 couldn't confirm receipt; check your phone");
209
- };
210
- var connectDevice = async (apiKey) => {
211
- const site = await fetchSite(apiKey);
212
- if (!site) {
213
- console.log(` ${yellow("!")} Couldn't load your subscribe link. Connect your phone later at ${cyan("pushary.com")}.`);
214
- return false;
121
+ if (facts.reachChecked && !facts.reachesYou) {
122
+ return {
123
+ ok: false,
124
+ headline: "Agents configured. Your phone is not connected yet.",
125
+ next: [
126
+ "Approvals fall back to the terminal until a device of yours can answer.",
127
+ "",
128
+ ...nextSteps(facts, false)
129
+ ],
130
+ exitCode: EXIT.NO_DEVICE
131
+ };
215
132
  }
216
- if (await activeSubscriberCount(apiKey) > 0) {
217
- console.log(` ${check} Phone already connected`);
218
- await sendAndConfirm(apiKey);
219
- return true;
133
+ if (!facts.reachChecked) {
134
+ return {
135
+ ok: true,
136
+ headline: "Setup complete.",
137
+ next: [
138
+ "The phone check was skipped, so nothing here proves an approval reaches you.",
139
+ "",
140
+ ...nextSteps(facts, false)
141
+ ],
142
+ exitCode: EXIT.OK
143
+ };
220
144
  }
221
- console.log();
222
- console.log(` ${bold("Connect your phone")}`);
223
- console.log(` ${dim("Scan this with your phone camera to get push notifications:")}`);
224
- await printQr(site.subscribeUrl);
225
- console.log(` ${dim("or open")} ${cyan(site.subscribeUrl)}`);
226
- console.log();
227
- const connected = await waitForDevice(apiKey);
228
- if (!connected) {
229
- console.log(` ${dim("No rush \u2014 open")} ${cyan(site.subscribeUrl)} ${dim("on your phone whenever you're ready.")}`);
230
- return false;
231
- }
232
- await sendAndConfirm(apiKey);
233
- return true;
234
- };
235
- var printConnectInstructions = async (apiKey) => {
236
- const site = await fetchSite(apiKey);
237
- const target = site?.subscribeUrl ?? "https://pushary.com";
238
- console.log(` ${dim("Connect your phone for push notifications at")} ${cyan(target)}`);
145
+ return {
146
+ ok: true,
147
+ headline: "Setup complete.",
148
+ next: [...nextSteps(facts, true), "", `Approvals: ${ACTIVITY_URL}`],
149
+ exitCode: EXIT.OK
150
+ };
239
151
  };
240
- var PAIR_POLL_INTERVAL_MS = 2e3;
241
- var PAIR_TIMEOUT_MS = 18e4;
242
- var startPairing = async (cliPublicKey) => {
152
+
153
+ // src/skills-cli.ts
154
+ import { execSync } from "child_process";
155
+ var installSkillViaSkillsCli = (agent) => {
243
156
  try {
244
- const res = await fetch(`${API_BASE}/api/mobile/pair/start`, {
245
- method: "POST",
246
- headers: { "Content-Type": "application/json" },
247
- body: JSON.stringify({ cliPublicKey }),
248
- signal: AbortSignal.timeout(1e4)
157
+ execSync(`npx -y skills@latest add Pushary/pushary-skill@pushary -y -g --agent ${agent}`, {
158
+ stdio: "pipe",
159
+ timeout: 9e4
249
160
  });
250
- if (!res.ok) return null;
251
- const data = await res.json().catch(() => null);
252
- return data && typeof data.pairId === "string" ? data.pairId : null;
161
+ return true;
253
162
  } catch {
254
- return null;
163
+ return false;
255
164
  }
256
165
  };
257
- var claimPairing = async (pairId) => {
258
- try {
259
- const res = await fetch(`${API_BASE}/api/mobile/pair/claim?id=${encodeURIComponent(pairId)}`, {
260
- signal: AbortSignal.timeout(1e4)
261
- });
262
- if (!res.ok) return null;
263
- return await res.json().catch(() => null);
264
- } catch {
265
- return null;
166
+
167
+ // src/setup/options.ts
168
+ var AGENT_CHOICES = ["claude_code", "codex", "gemini_cli", "hermes", "cursor", "custom"];
169
+ var SETUP_FLAGS = {
170
+ bools: ["key-stdin", "skip-phone", "yes", "y", "json", "dry-run", "help", "h"],
171
+ values: ["key", "connect", "agents"]
172
+ };
173
+ var isAgentChoice = (value) => AGENT_CHOICES.includes(value);
174
+ var parseAgentSelection = (raw, yes) => {
175
+ if (raw === void 0) {
176
+ return yes ? { kind: "auto" } : { kind: "ask" };
266
177
  }
178
+ const value = raw.trim().toLowerCase();
179
+ if (value === "auto") return { kind: "auto" };
180
+ if (value === "none") return { kind: "none" };
181
+ const parts = value.split(",").map((part) => part.trim()).filter((part) => part !== "");
182
+ if (parts.length === 0) throw new UsageError("--agents needs a value");
183
+ const unknown = parts.filter((part) => !isAgentChoice(part));
184
+ if (unknown.length > 0) {
185
+ throw new UsageError(
186
+ `--agents does not know: ${unknown.join(", ")}. Use auto, none, or any of: ${AGENT_CHOICES.join(", ")}`
187
+ );
188
+ }
189
+ return { kind: "explicit", agents: [...new Set(parts)] };
267
190
  };
268
- var connectViaAppPairing = async () => {
269
- const keypair = generateKeypair();
270
- const pairId = await startPairing(keypair.publicKeyB64);
271
- if (!pairId) {
272
- console.log(` ${yellow("!")} Couldn't start pairing. Check your connection and try again.`);
273
- return null;
191
+ var parseSetupOptions = (input2) => {
192
+ const flags = parseFlags(input2.argv, SETUP_FLAGS);
193
+ const stray = [
194
+ ...flags.command !== void 0 && flags.command !== "setup" ? [flags.command] : [],
195
+ ...flags.rest
196
+ ];
197
+ if (stray.length > 0) {
198
+ throw new UsageError(
199
+ `setup takes no positional arguments (got ${stray.join(", ")}). Did you mean --agents ${stray[0]}?`
200
+ );
274
201
  }
275
- const deepLink = `pushary://pair?pk=${encodeURIComponent(keypair.publicKeyB64)}&id=${encodeURIComponent(pairId)}`;
276
- console.log();
277
- console.log(` ${bold("Connect your Pushary app")}`);
278
- console.log(` ${dim("Open the Pushary app, tap Connect agent, and scan this:")}`);
279
- await printQr(deepLink);
280
- console.log(` ${dim("No camera handy? Open this link on the phone instead:")}`);
281
- console.log(` ${cyan(deepLink)}`);
282
- console.log(` ${dim("Confirm the app shows fingerprint")} ${cyan(publicKeyFingerprint(keypair.publicKeyB64))}`);
283
- console.log();
284
- const deadline = Date.now() + PAIR_TIMEOUT_MS;
285
- const stop = startSpinner(() => {
286
- const left = Math.max(0, Math.ceil((deadline - Date.now()) / 1e3));
287
- return `Waiting for the app to authorize ${dim(`(${left}s)`)}`;
288
- });
289
- try {
290
- while (Date.now() < deadline) {
291
- const claim = await claimPairing(pairId);
292
- if (claim?.status === "authorized") {
293
- let apiKey;
294
- try {
295
- apiKey = openSealedKey(claim.sealed, keypair);
296
- } catch {
297
- stop(yellow("!"), "Pairing failed to decrypt \u2014 re-run setup to try again");
298
- return null;
299
- }
300
- stop(check, "App connected");
301
- return { apiKey, handle: claim.siteSlug };
302
- }
303
- if (claim?.status === "expired") {
304
- stop(yellow("!"), "Pairing expired \u2014 re-run setup to try again");
305
- return null;
306
- }
307
- await sleep(PAIR_POLL_INTERVAL_MS);
308
- }
309
- stop(yellow("!"), "Timed out waiting for the app");
310
- return null;
311
- } catch (err) {
312
- stop(yellow("!"), `Pairing error ${dim(`(${err instanceof Error ? err.message : "network"})`)}`);
313
- return null;
202
+ const yes = flags.bools.has("yes") || flags.bools.has("y");
203
+ const help = flags.bools.has("help") || flags.bools.has("h");
204
+ const agentsFlag = flags.values.get("agents");
205
+ const agents = parseAgentSelection(agentsFlag, yes);
206
+ const connectRaw = flags.values.get("connect");
207
+ if (connectRaw !== void 0 && connectRaw !== "web" && connectRaw !== "app") {
208
+ throw new UsageError(`--connect must be one of: web, app (got ${connectRaw})`);
209
+ }
210
+ const key = flags.values.get("key")?.trim() || null;
211
+ const keyStdin = flags.bools.has("key-stdin");
212
+ if (key && keyStdin) {
213
+ throw new UsageError("Pass either --key or --key-stdin, not both");
314
214
  }
215
+ return {
216
+ connectMode: connectRaw === "app" ? "app" : "web",
217
+ connectExplicit: connectRaw !== void 0,
218
+ skipPhone: flags.bools.has("skip-phone"),
219
+ // Anything that means "there is nobody to answer a prompt". A non-TTY stdin
220
+ // is included because that is the case that used to hang: it is better to
221
+ // take the defaults and say so than to wait forever for a keypress.
222
+ nonInteractive: yes || agentsFlag !== void 0 || !input2.stdinIsTty,
223
+ json: flags.bools.has("json"),
224
+ dryRun: flags.bools.has("dry-run"),
225
+ help,
226
+ agents,
227
+ key,
228
+ keyStdin
229
+ };
315
230
  };
316
231
 
317
- // src/skills-cli.ts
318
- import { execSync } from "child_process";
319
- var installSkillViaSkillsCli = (agent) => {
232
+ // src/setup/detect.ts
233
+ import { execSync as execSync2 } from "child_process";
234
+ import { existsSync } from "fs";
235
+ import { homedir } from "os";
236
+ import { join } from "path";
237
+ var whichCommand = () => process.platform === "win32" ? "where" : "which";
238
+ var binaryOnPath = (binary) => {
320
239
  try {
321
- execSync(`npx -y skills@latest add Pushary/pushary-skill@pushary -y -g --agent ${agent}`, {
322
- stdio: "pipe",
323
- timeout: 9e4
324
- });
240
+ execSync2(`${whichCommand()} ${binary}`, { stdio: "ignore", timeout: 5e3 });
325
241
  return true;
326
242
  } catch {
327
243
  return false;
328
244
  }
329
245
  };
246
+ var defaultDeps = { onPath: binaryOnPath, exists: existsSync };
247
+ var detectAgent = (probe, deps = defaultDeps) => {
248
+ const checked = [];
249
+ if (probe.binary) {
250
+ checked.push(`${whichCommand()} ${probe.binary}`);
251
+ if (deps.onPath(probe.binary)) {
252
+ return { kind: "installed", evidence: `${probe.binary} on PATH`, strong: true };
253
+ }
254
+ }
255
+ for (const path of probe.paths) {
256
+ checked.push(path);
257
+ if (deps.exists(path)) return { kind: "installed", evidence: path, strong: true };
258
+ }
259
+ for (const marker of probe.configMarkers) {
260
+ checked.push(marker);
261
+ if (deps.exists(marker)) return { kind: "installed", evidence: marker, strong: false };
262
+ }
263
+ if (probe.binary === null && probe.paths.length === 0 && probe.configMarkers.length === 0) {
264
+ return { kind: "unknown" };
265
+ }
266
+ return { kind: "not-found", checked };
267
+ };
268
+ var agentProbes = (home = homedir()) => ({
269
+ claude_code: {
270
+ binary: "claude",
271
+ // The native installer puts it here, outside a default non-login PATH.
272
+ paths: [join(home, ".local", "bin", "claude")],
273
+ configMarkers: [join(home, ".claude")]
274
+ },
275
+ codex: {
276
+ binary: "codex",
277
+ paths: [],
278
+ configMarkers: [process.env.CODEX_HOME?.trim() || join(home, ".codex")]
279
+ },
280
+ gemini_cli: {
281
+ binary: "gemini",
282
+ paths: [],
283
+ configMarkers: [join(home, ".gemini")]
284
+ },
285
+ hermes: {
286
+ binary: "hermes",
287
+ paths: [],
288
+ configMarkers: [join(home, ".hermes")]
289
+ },
290
+ cursor: {
291
+ // GUI editor. `cursor` on PATH only exists if the user ran "Install 'cursor'
292
+ // command in PATH" from the command palette, which most never do.
293
+ binary: "cursor",
294
+ paths: process.platform === "darwin" ? ["/Applications/Cursor.app"] : process.platform === "win32" ? [join(process.env.LOCALAPPDATA ?? join(home, "AppData", "Local"), "Programs", "cursor")] : ["/usr/share/cursor", join(home, ".local", "share", "cursor")],
295
+ configMarkers: [join(home, ".cursor")]
296
+ },
297
+ // "Other" is a set of printed instructions for any MCP or HTTP client. There is
298
+ // nothing to detect, and it must never be auto-selected.
299
+ custom: { binary: null, paths: [], configMarkers: [] }
300
+ });
301
+ var detectAllAgents = (deps = defaultDeps, home) => {
302
+ const probes = agentProbes(home);
303
+ return Object.keys(probes).map((agent) => ({
304
+ agent,
305
+ result: detectAgent(probes[agent], deps)
306
+ }));
307
+ };
308
+ var isDetected = (result) => result.kind === "installed";
309
+ var shortenHome = (path, home = homedir()) => home && path.startsWith(home) ? `~${path.slice(home.length)}` : path;
310
+ var describeDetection = (result, home) => {
311
+ if (result.kind !== "installed") return null;
312
+ const evidence = shortenHome(result.evidence, home);
313
+ return result.strong ? evidence : `${evidence}, config only`;
314
+ };
330
315
 
331
316
  // bin/pushary-setup.ts
332
- var CLAUDE_SETTINGS = join(homedir(), ".claude", "settings.json");
333
- var CLAUDE_JSON = join(homedir(), ".claude.json");
334
- var CURSOR_PLUGIN_DIR = join(homedir(), ".cursor", "plugins", "local", "pushary");
335
- var CURSOR_USER_HOOKS = join(homedir(), ".cursor", "hooks.json");
336
- var CLAUDE_SKILL_DIR = join(homedir(), ".claude", "skills", "pushary");
337
- var CODEX_HOME = process.env.CODEX_HOME?.trim() || join(homedir(), ".codex");
338
- var CODEX_SKILL_DIR = join(CODEX_HOME, "skills", "pushary");
339
- var CODEX_AGENTS_MD = join(CODEX_HOME, "AGENTS.md");
340
- var GEMINI_SETTINGS = join(homedir(), ".gemini", "settings.json");
341
- var GEMINI_MD = join(homedir(), ".gemini", "GEMINI.md");
342
- var PUSHARY_CONFIG_DIR = join(homedir(), ".pushary");
343
- var PUSHARY_CONFIG_FILE = join(PUSHARY_CONFIG_DIR, "config.json");
344
- var SHELL_FILES = [".zshrc", ".zprofile", ".bashrc", ".bash_profile"].map((f) => join(homedir(), f));
345
- var dim2 = (s) => `\x1B[2m${s}\x1B[0m`;
346
- var bold2 = (s) => `\x1B[1m${s}\x1B[0m`;
347
- var green2 = (s) => `\x1B[32m${s}\x1B[0m`;
348
- var cyan2 = (s) => `\x1B[36m${s}\x1B[0m`;
349
- var yellow2 = (s) => `\x1B[33m${s}\x1B[0m`;
350
- var check2 = green2("\u2713");
351
- var parseKeyFlag = () => {
352
- const args = process.argv.slice(2);
353
- for (let i = 0; i < args.length; i++) {
354
- if (args[i] === "--key" && args[i + 1]) return args[i + 1].trim();
355
- if (args[i].startsWith("--key=")) return args[i].slice(6).trim();
356
- }
357
- return void 0;
317
+ var CLAUDE_SETTINGS = join2(homedir2(), ".claude", "settings.json");
318
+ var CLAUDE_JSON = join2(homedir2(), ".claude.json");
319
+ var CURSOR_PLUGIN_DIR = join2(homedir2(), ".cursor", "plugins", "local", "pushary");
320
+ var CURSOR_USER_HOOKS = join2(homedir2(), ".cursor", "hooks.json");
321
+ var CLAUDE_SKILL_DIR = join2(homedir2(), ".claude", "skills", "pushary");
322
+ var CODEX_HOME = process.env.CODEX_HOME?.trim() || join2(homedir2(), ".codex");
323
+ var CODEX_SKILL_DIR = join2(CODEX_HOME, "skills", "pushary");
324
+ var CODEX_AGENTS_MD = join2(CODEX_HOME, "AGENTS.md");
325
+ var GEMINI_SETTINGS = join2(homedir2(), ".gemini", "settings.json");
326
+ var GEMINI_MD = join2(homedir2(), ".gemini", "GEMINI.md");
327
+ var PUSHARY_CONFIG_FILE = join2(homedir2(), ".pushary", "config.json");
328
+ var manualSteps = [];
329
+ var noteManual = (step) => {
330
+ manualSteps.push(step);
358
331
  };
359
- var parseConnectFlag = () => {
360
- const args = process.argv.slice(2);
361
- for (let i = 0; i < args.length; i++) {
362
- if (args[i] === "--connect" && args[i + 1]) {
363
- return args[i + 1].trim() === "app" ? "app" : "web";
364
- }
365
- if (args[i].startsWith("--connect=")) {
366
- return args[i].slice(10).trim() === "app" ? "app" : "web";
367
- }
332
+ var humanStream = process.stdout;
333
+ var writeHuman = (text) => {
334
+ humanStream.write(text);
335
+ };
336
+ var dim = (s) => `\x1B[2m${s}\x1B[0m`;
337
+ var bold = (s) => `\x1B[1m${s}\x1B[0m`;
338
+ var green = (s) => `\x1B[32m${s}\x1B[0m`;
339
+ var cyan = (s) => `\x1B[36m${s}\x1B[0m`;
340
+ var yellow = (s) => `\x1B[33m${s}\x1B[0m`;
341
+ var check = green("\u2713");
342
+ var readKeyFromStdin = async (options) => {
343
+ if (!options.keyStdin) return void 0;
344
+ const chunks = [];
345
+ for await (const chunk of process.stdin) chunks.push(chunk);
346
+ const first = Buffer.concat(chunks).toString("utf-8").split("\n", 1)[0]?.trim();
347
+ if (!first) {
348
+ console.log(`
349
+ ${yellow("!")} --key-stdin was passed but stdin was empty.`);
350
+ process.exit(2);
368
351
  }
369
- return "web";
352
+ if (!isValidApiKey(first)) {
353
+ console.log(`
354
+ ${yellow("!")} The key on stdin is not a valid Pushary key. Expected: ${dim("pk_xxx.xxx")}`);
355
+ console.log(` ${dim("Copy your key from")} ${cyan("https://pushary.com/dashboard/agent/settings")}
356
+ `);
357
+ process.exit(2);
358
+ }
359
+ return first;
370
360
  };
371
361
  var isInstalled = (command) => {
372
362
  const whichCmd = process.platform === "win32" ? "where" : "which";
373
363
  try {
374
- execSync2(`${whichCmd} ${command}`, { stdio: "ignore", timeout: 5e3 });
364
+ execSync3(`${whichCmd} ${command}`, { stdio: "ignore", timeout: 5e3 });
375
365
  return true;
376
366
  } catch {
377
367
  return false;
@@ -384,11 +374,40 @@ var readJson = (filePath) => {
384
374
  return {};
385
375
  }
386
376
  };
377
+ var readAgentJson = (filePath) => {
378
+ const result = readJsonSafe(filePath);
379
+ if (result.kind === "ok") return result.value;
380
+ if (result.kind === "missing") return {};
381
+ const backup = backupFile(filePath);
382
+ const reason = result.kind === "unparseable" ? "is not valid JSON" : "could not be read";
383
+ throw new Error(
384
+ `${filePath} ${reason}, so it was left untouched. A copy is at ${backup}. Fix or remove the original, then re-run setup.`
385
+ );
386
+ };
387
387
  var writeJson = (filePath, data) => {
388
388
  const dir = dirname(filePath);
389
- if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
389
+ if (!existsSync2(dir)) mkdirSync(dir, { recursive: true });
390
390
  writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n", "utf-8");
391
391
  };
392
+ var readAgentToml = (filePath) => {
393
+ let raw;
394
+ try {
395
+ raw = readFileSync(filePath, "utf-8");
396
+ } catch (err) {
397
+ if (err.code === "ENOENT") return {};
398
+ throw new Error(`${filePath} could not be read, so it was left untouched.`);
399
+ }
400
+ if (raw.trim() === "") return {};
401
+ try {
402
+ return parseTOML(raw);
403
+ } catch {
404
+ const backup = backupFile(filePath);
405
+ throw new Error(
406
+ `${filePath} is not valid TOML, so it was left untouched. A copy is at ${backup}. Fix or remove the original, then re-run setup.`
407
+ );
408
+ }
409
+ };
410
+ var isCancellation = (err) => err instanceof Error && (err.name === "ExitPromptError" || /force closed the prompt/i.test(err.message));
392
411
  var formatError = (err) => {
393
412
  if (err instanceof Error) {
394
413
  const errWithSignal = err;
@@ -403,29 +422,20 @@ var spinner = async (label, fn, options = {}) => {
403
422
  const frames = [" ", ". ", ".. ", "..."];
404
423
  let i = 0;
405
424
  const interval = setInterval(() => {
406
- process.stdout.write(`\r ${dim2(frames[i++ % frames.length])} ${label}`);
425
+ writeHuman(`\r ${dim(frames[i++ % frames.length])} ${label}`);
407
426
  }, 200);
408
427
  try {
409
428
  await fn();
410
429
  clearInterval(interval);
411
- process.stdout.write(`\r ${check2} ${label}
430
+ writeHuman(`\r ${check} ${label}
412
431
  `);
413
432
  } catch (err) {
414
433
  clearInterval(interval);
415
- process.stdout.write(`\r ${yellow2("!")} ${label} ${dim2(`(${formatError(err)})`)}
434
+ writeHuman(`\r ${yellow("!")} ${label} ${dim(`(${formatError(err)})`)}
416
435
  `);
417
436
  if (!options.optional) throw err;
418
437
  }
419
438
  };
420
- var getPackageVersion = () => {
421
- try {
422
- const __dirname = dirname(fileURLToPath(import.meta.url));
423
- const pkg = JSON.parse(readFileSync(join(__dirname, "..", "..", "package.json"), "utf-8"));
424
- return pkg.version ?? "0.0.0";
425
- } catch {
426
- return "0.0.0";
427
- }
428
- };
429
439
  var checkForUpdates = async (current) => {
430
440
  try {
431
441
  const res = await fetch("https://registry.npmjs.org/@pushary/agent-hooks/latest", {
@@ -434,17 +444,18 @@ var checkForUpdates = async (current) => {
434
444
  const data = await res.json();
435
445
  const latest = data.version;
436
446
  if (latest && latest !== current) {
437
- console.log(` ${yellow2("!")} Update available: ${dim2(current)} \u2192 ${green2(latest)}`);
438
- console.log(` ${dim2("Run:")} npx @pushary/agent-hooks@latest upgrade`);
447
+ console.log(` ${yellow("!")} Update available: ${dim(current)} \u2192 ${green(latest)}`);
448
+ console.log(` ${dim("Run:")} npx @pushary/agent-hooks@latest upgrade`);
439
449
  console.log();
440
450
  }
441
451
  } catch {
442
452
  }
443
453
  };
444
- var installGlobally = async () => {
445
- await spinner("Installing pushary-hook globally", async () => {
454
+ var installGlobally2 = async () => {
455
+ const version = getPackageVersion();
456
+ await spinner(`Installing pushary-hook globally (${version})`, async () => {
446
457
  try {
447
- execNpm("install -g --no-workspaces @pushary/agent-hooks@latest", { timeout: 12e4 });
458
+ await installGlobally({ version });
448
459
  } catch (err) {
449
460
  throw new Error(npmErrorMessage(err));
450
461
  }
@@ -455,118 +466,129 @@ var fetchSkillContent = async () => {
455
466
  if (_cachedSkillContent) return _cachedSkillContent;
456
467
  const __dirname = dirname(fileURLToPath(import.meta.url));
457
468
  const candidates = [
458
- join(__dirname, "..", "..", "data", "SKILL.md"),
459
- join(__dirname, "..", "data", "SKILL.md")
469
+ join2(__dirname, "..", "..", "data", "SKILL.md"),
470
+ join2(__dirname, "..", "data", "SKILL.md")
460
471
  ];
461
- const source = candidates.find((path) => existsSync(path));
472
+ const source = candidates.find((path) => existsSync2(path));
462
473
  if (!source) throw new Error("packaged skill not found");
463
474
  _cachedSkillContent = readFileSync(source, "utf-8");
464
475
  return _cachedSkillContent;
465
476
  };
466
- var skillsCliInUse = () => isInstalled("skills") || existsSync(join(homedir(), ".agents", "skills"));
477
+ var skillsCliInUse = () => isInstalled("skills") || existsSync2(join2(homedir2(), ".agents", "skills"));
467
478
  var installSkill = async (agent, fallbackDir) => {
468
479
  await spinner("Installing Pushary skill", async () => {
469
480
  if (skillsCliInUse() && installSkillViaSkillsCli(agent)) return;
470
481
  const content = await fetchSkillContent();
471
- if (!existsSync(fallbackDir)) mkdirSync(fallbackDir, { recursive: true });
472
- writeFileSync(join(fallbackDir, "SKILL.md"), content, "utf-8");
482
+ if (!existsSync2(fallbackDir)) mkdirSync(fallbackDir, { recursive: true });
483
+ writeFileSync(join2(fallbackDir, "SKILL.md"), content, "utf-8");
473
484
  });
474
485
  };
475
486
  var setupClaudeCode = async (apiKey) => {
476
487
  console.log(`
477
- ${bold2("Setting up Claude Code")}
488
+ ${bold("Setting up Claude Code")}
478
489
  `);
479
- const settings = readJson(CLAUDE_SETTINGS);
490
+ await installGlobally2();
491
+ const claudeJson = readAgentJson(CLAUDE_JSON);
492
+ const settings = readAgentJson(CLAUDE_SETTINGS);
480
493
  await spinner("Adding MCP server (type: http)", async () => {
481
- const data = readJson(CLAUDE_JSON);
482
- addClaudeMcpServer(data, apiKey);
483
- writeJson(CLAUDE_JSON, data);
494
+ addClaudeMcpServer(claudeJson, apiKey);
484
495
  });
485
496
  await spinner("Auto-allowing Pushary tools", async () => {
486
497
  addPusharyToolPermissions(settings);
487
498
  });
488
- await installGlobally();
489
499
  await spinner("Adding hooks (approvals, activity, presence, and lifecycle events)", async () => {
490
500
  addPusharyHooks(settings, resolveGlobalBinDir("pushary-hook"));
491
501
  });
502
+ await spinner(`Writing ${CLAUDE_JSON}`, async () => {
503
+ writeJsonAtomic(CLAUDE_JSON, claudeJson, 384);
504
+ });
492
505
  await spinner(`Writing ${CLAUDE_SETTINGS}`, async () => {
493
- writeJson(CLAUDE_SETTINGS, settings);
506
+ writeJsonAtomic(CLAUDE_SETTINGS, settings);
494
507
  });
495
508
  await installSkill("claude-code", CLAUDE_SKILL_DIR);
496
509
  console.log();
497
- console.log(` ${dim2("What this configured:")}`);
498
- console.log(` ${dim2("\u2022")} MCP server: your agent can send notifications and ask questions`);
499
- console.log(` ${dim2("\u2022")} Skill: teaches your agent when and how to use Pushary`);
500
- console.log(` ${dim2("\u2022")} Hooks: route permission approvals through push notifications`);
501
- console.log(` ${dim2("\u2022")} Auto-allowed tools: no permission prompts for Pushary MCP calls`);
510
+ console.log(` ${dim("What this configured:")}`);
511
+ console.log(` ${dim("\u2022")} MCP server: your agent can send notifications and ask questions`);
512
+ console.log(` ${dim("\u2022")} Skill: teaches your agent when and how to use Pushary`);
513
+ console.log(` ${dim("\u2022")} Hooks: route permission approvals through push notifications`);
514
+ console.log(` ${dim("\u2022")} Auto-allowed tools: no permission prompts for Pushary MCP calls`);
502
515
  };
516
+ var IS_WINDOWS = process.platform === "win32";
503
517
  var resolveHermesPython = () => {
504
518
  try {
505
- const launcher = execSync2("command -v hermes", { encoding: "utf-8", stdio: "pipe", timeout: 5e3 }).trim();
506
- if (launcher) {
519
+ const probe = IS_WINDOWS ? "where hermes" : "command -v hermes";
520
+ const launcher = execSync3(probe, { encoding: "utf-8", stdio: "pipe", timeout: 5e3 }).split("\n")[0].trim();
521
+ if (launcher && !IS_WINDOWS) {
507
522
  const shebang = readFileSync(launcher, "utf-8").split("\n", 1)[0];
508
523
  if (shebang.startsWith("#!")) {
509
524
  const interpreter = shebang.slice(2).trim().split(/\s+/)[0];
510
- if (interpreter && /python/i.test(interpreter) && existsSync(interpreter)) return interpreter;
525
+ if (interpreter && /python/i.test(interpreter) && existsSync2(interpreter)) return interpreter;
511
526
  }
512
527
  }
513
528
  } catch {
514
529
  }
515
- const venvPython = join(homedir(), ".hermes", "hermes-agent", "venv", "bin", "python3");
516
- if (existsSync(venvPython)) return venvPython;
517
- return null;
530
+ const venvRoot = join2(homedir2(), ".hermes", "hermes-agent", "venv");
531
+ const candidates = IS_WINDOWS ? [join2(venvRoot, "Scripts", "python.exe"), join2(venvRoot, "Scripts", "python3.exe")] : [join2(venvRoot, "bin", "python3"), join2(venvRoot, "bin", "python")];
532
+ return candidates.find(existsSync2) ?? null;
518
533
  };
519
534
  var ensurePip = (python) => {
520
535
  try {
521
- execSync2(`"${python}" -m pip --version`, { stdio: "ignore", timeout: 15e3 });
536
+ execSync3(`"${python}" -m pip --version`, { stdio: "ignore", timeout: 15e3 });
522
537
  return;
523
538
  } catch {
524
539
  }
525
540
  try {
526
- execSync2(`"${python}" -m ensurepip --upgrade`, { stdio: "pipe", timeout: 6e4 });
541
+ execSync3(`"${python}" -m ensurepip --upgrade`, { stdio: "pipe", timeout: 6e4 });
527
542
  } catch {
528
543
  }
529
544
  };
530
545
  var enablePusharyPlugin = (python) => {
531
546
  const snippet = 'from hermes_cli.config import load_config, save_config; c = load_config(); p = c.get("plugins") if isinstance(c.get("plugins"), dict) else {}; e = p.get("enabled") if isinstance(p.get("enabled"), list) else []; p["enabled"] = (e + ["pushary"]) if "pushary" not in e else e; c["plugins"] = p; a = c.get("agent") if isinstance(c.get("agent"), dict) else {}; d = a.get("disabled_toolsets") if isinstance(a.get("disabled_toolsets"), list) else []; a["disabled_toolsets"] = (d + ["clarify"]) if "clarify" not in d else d; c["agent"] = a; save_config(c)';
547
+ const scriptPath = join2(tmpdir(), `pushary-hermes-${process.pid}.py`);
532
548
  try {
533
- execSync2(`"${python}" -c '${snippet}'`, { stdio: "pipe", timeout: 15e3 });
549
+ writeFileSync(scriptPath, snippet.split("; ").join("\n"), "utf-8");
550
+ execSync3(`"${python}" "${scriptPath}"`, { stdio: "pipe", timeout: 15e3 });
534
551
  return;
535
552
  } catch {
553
+ execSync3("hermes plugins enable pushary", { stdio: "ignore", timeout: 1e4 });
554
+ } finally {
555
+ try {
556
+ rmSync(scriptPath, { force: true });
557
+ } catch {
558
+ }
536
559
  }
537
- execSync2("hermes plugins enable pushary", { stdio: "ignore", timeout: 1e4 });
538
560
  };
539
561
  var setupHermes = async (_apiKey) => {
540
562
  console.log(`
541
- ${bold2("Setting up Hermes Agent")}
563
+ ${bold("Setting up Hermes Agent")}
542
564
  `);
543
565
  if (!isInstalled("hermes")) {
544
- console.log(` ${yellow2("!")} Hermes CLI not found. Skipping.`);
545
- console.log(` ${dim2("Install Hermes and re-run setup to configure.")}`);
546
- return;
566
+ console.log(` ${yellow("!")} Hermes CLI not found. Skipping.`);
567
+ console.log(` ${dim("Install Hermes and re-run setup to configure.")}`);
568
+ return "skipped";
547
569
  }
548
570
  const python = resolveHermesPython();
549
571
  if (!python) {
550
- console.log(` ${yellow2("!")} Could not locate the Python that Hermes runs in. Skipping.`);
551
- console.log(` ${dim2("Install manually: <hermes-python> -m pip install hermes-plugin-pushary")}`);
552
- return;
572
+ console.log(` ${yellow("!")} Could not locate the Python that Hermes runs in. Skipping.`);
573
+ noteManual("Install the Hermes plugin by hand: <hermes-python> -m pip install hermes-plugin-pushary");
574
+ return "skipped";
553
575
  }
554
576
  await spinner("Installing hermes-plugin-pushary into Hermes\u2019 environment", async () => {
555
577
  ensurePip(python);
556
- execSync2(`"${python}" -m pip install --upgrade hermes-plugin-pushary`, { stdio: "pipe", timeout: 18e4 });
578
+ execSync3(`"${python}" -m pip install --upgrade hermes-plugin-pushary`, { stdio: "pipe", timeout: 18e4 });
557
579
  });
558
580
  await spinner("Enabling plugin + routing questions to push", async () => {
559
581
  enablePusharyPlugin(python);
560
582
  });
561
583
  console.log();
562
- console.log(` ${dim2("What this configured:")}`);
563
- console.log(` ${dim2("\u2022")} Native tools: pushary_notify, pushary_ask, pushary_wait, pushary_cancel`);
564
- console.log(` ${dim2("\u2022")} Questions go to your phone: Hermes' terminal ${bold2("clarify")} tool is disabled, so pushary_ask handles every question`);
565
- console.log(` ${dim2("\u2022")} Auto-notifications: push alert when tools return errors`);
566
- console.log(` ${dim2("\u2022")} Permission gating: set ${bold2("PUSHARY_GATE_TOOLS")} to require lock-screen approval for risky tools`);
567
- console.log(` ${dim2("To re-enable terminal prompts:")} remove ${bold2("clarify")} from ${dim2("agent.disabled_toolsets")} in ~/.hermes/config.yaml`);
584
+ console.log(` ${dim("What this configured:")}`);
585
+ console.log(` ${dim("\u2022")} Native tools: pushary_notify, pushary_ask, pushary_wait, pushary_cancel`);
586
+ console.log(` ${dim("\u2022")} Questions go to your phone: Hermes' terminal ${bold("clarify")} tool is disabled, so pushary_ask handles every question`);
587
+ console.log(` ${dim("\u2022")} Auto-notifications: push alert when tools return errors`);
588
+ console.log(` ${dim("\u2022")} Permission gating: set ${bold("PUSHARY_GATE_TOOLS")} to require lock-screen approval for risky tools`);
589
+ console.log(` ${dim("To re-enable terminal prompts:")} remove ${bold("clarify")} from ${dim("agent.disabled_toolsets")} in ~/.hermes/config.yaml`);
568
590
  };
569
- var CODEX_HOOKS_JSON = join(CODEX_HOME, "hooks.json");
591
+ var CODEX_HOOKS_JSON = join2(CODEX_HOME, "hooks.json");
570
592
  var CODEX_HOOKS_MIN_VERSION = [0, 122, 0];
571
593
  var CODEX_TRUST_VERIFIED_MAX = [0, 142, 2];
572
594
  var parseCodexVersion = (raw) => {
@@ -582,7 +604,7 @@ var compareCodexVersion = (a, b) => {
582
604
  };
583
605
  var readCodexVersion = () => {
584
606
  try {
585
- return parseCodexVersion(execSync2("codex --version", { encoding: "utf-8", stdio: "pipe", timeout: 1e4 }));
607
+ return parseCodexVersion(execSync3("codex --version", { encoding: "utf-8", stdio: "pipe", timeout: 1e4 }));
586
608
  } catch {
587
609
  return null;
588
610
  }
@@ -625,24 +647,19 @@ var addCodexNotifyEntry = (codexConfig) => {
625
647
  };
626
648
  var setupCodex = async (apiKey) => {
627
649
  console.log(`
628
- ${bold2("Setting up Codex")}
650
+ ${bold("Setting up Codex")}
629
651
  `);
630
652
  if (!isInstalled("codex")) {
631
- console.log(` ${yellow2("!")} Codex CLI not found. Skipping.`);
632
- console.log(` ${dim2("Install Codex and re-run setup to configure.")}`);
633
- return;
653
+ console.log(` ${yellow("!")} Codex CLI not found. Skipping.`);
654
+ console.log(` ${dim("Install Codex and re-run setup to configure.")}`);
655
+ return "skipped";
634
656
  }
635
- await installGlobally();
636
- const codexConfig = join(homedir(), ".codex", "config.toml");
657
+ await installGlobally2();
658
+ const codexConfig = join2(homedir2(), ".codex", "config.toml");
637
659
  await spinner("Adding Pushary MCP server (key embedded, auto-allowed)", async () => {
638
- let raw = "";
639
- try {
640
- raw = readFileSync(codexConfig, "utf-8");
641
- } catch {
642
- }
643
- const config = raw ? parseTOML(raw) : {};
660
+ const config = readAgentToml(codexConfig);
644
661
  addCodexMcpServer(config, apiKey);
645
- writeFileSync(codexConfig, stringifyTOML(config), "utf-8");
662
+ writeFileAtomic(codexConfig, stringifyTOML(config), KEY_FILE_MODE);
646
663
  });
647
664
  const codexVersion = readCodexVersion();
648
665
  const hooksSupported = codexSupportsHooks(codexVersion);
@@ -651,7 +668,7 @@ var setupCodex = async (apiKey) => {
651
668
  if (hooksSupported) {
652
669
  const hookCommand = quoteIfNeeded(resolveGlobalBinary("pushary-codex-hook") ?? "pushary-codex-hook");
653
670
  await spinner("Adding native hooks (~/.codex/hooks.json)", async () => {
654
- const hooksConfig = readJson(CODEX_HOOKS_JSON);
671
+ const hooksConfig = readAgentJson(CODEX_HOOKS_JSON);
655
672
  addCodexHooks(hooksConfig, hookCommand);
656
673
  writeJson(CODEX_HOOKS_JSON, hooksConfig);
657
674
  });
@@ -672,9 +689,9 @@ var setupCodex = async (apiKey) => {
672
689
  removeCodexNotifyEntry(codexConfig);
673
690
  });
674
691
  } else {
675
- console.log(` ${yellow2("!")} This Codex version predates native hooks (needs ${CODEX_HOOKS_MIN_VERSION.join(".")}+).`);
676
- console.log(` ${dim2("Installing the deprecated notify handler instead. Upgrade Codex and re-run setup")}`);
677
- console.log(` ${dim2("to get policy enforcement, phone approvals, and session tracking.")}`);
692
+ console.log(` ${yellow("!")} This Codex version predates native hooks (needs ${CODEX_HOOKS_MIN_VERSION.join(".")}+).`);
693
+ console.log(` ${dim("Installing the deprecated notify handler instead. Upgrade Codex and re-run setup")}`);
694
+ console.log(` ${dim("to get policy enforcement, phone approvals, and session tracking.")}`);
678
695
  await spinner("Adding notify handler for Codex events (deprecated)", async () => {
679
696
  addCodexNotifyEntry(codexConfig);
680
697
  });
@@ -684,48 +701,49 @@ var setupCodex = async (apiKey) => {
684
701
  writeInstructionBlock(CODEX_AGENTS_MD, renderAgentInstructions("Codex"));
685
702
  });
686
703
  console.log();
687
- console.log(` ${dim2("What this configured:")}`);
688
- console.log(` ${dim2("\u2022")} MCP server: Codex can send notifications and ask questions`);
689
- console.log(` ${dim2("\u2022")} API key embedded in config.toml: works without a shell reload or export`);
690
- console.log(` ${dim2("\u2022")} Proactive instructions: ~/.codex/AGENTS.md tells Codex to ask via push, not the terminal`);
691
- console.log(` ${dim2("\u2022")} Auto-allowed tools: no permission prompts for Pushary MCP calls`);
704
+ console.log(` ${dim("What this configured:")}`);
705
+ console.log(` ${dim("\u2022")} MCP server: Codex can send notifications and ask questions`);
706
+ console.log(` ${dim("\u2022")} API key embedded in config.toml: works without a shell reload or export`);
707
+ console.log(` ${dim("\u2022")} Proactive instructions: ~/.codex/AGENTS.md tells Codex to ask via push, not the terminal`);
708
+ console.log(` ${dim("\u2022")} Auto-allowed tools: no permission prompts for Pushary MCP calls`);
692
709
  if (hooksSupported) {
693
- console.log(` ${dim2("\u2022")} Native hooks: phone approvals, policy enforcement, kill switch, session tracking`);
710
+ console.log(` ${dim("\u2022")} Native hooks: phone approvals, policy enforcement, kill switch, session tracking`);
694
711
  if (trusted) {
695
- console.log(` ${dim2("\u2022")} Hook pre-trusted: no ${cyan2("/hooks")} step, phone approvals work right away`);
712
+ console.log(` ${dim("\u2022")} Hook pre-trusted: no ${cyan("/hooks")} step, phone approvals work right away`);
696
713
  } else {
697
714
  console.log();
698
- console.log(` ${bold2("One step left, and it happens inside Codex")}`);
699
- console.log(` ${dim2("Codex makes you trust any hook before it runs. This is Codex's own")}`);
700
- console.log(` ${dim2("security check, not a Pushary thing, and you only do it once.")}`);
715
+ console.log(` ${bold("One step left, and it happens inside Codex")}`);
716
+ console.log(` ${dim("Codex makes you trust any hook before it runs. This is Codex's own")}`);
717
+ console.log(` ${dim("security check, not a Pushary thing, and you only do it once.")}`);
701
718
  console.log();
702
- console.log(` ${cyan2("1.")} Open Codex and type ${cyan2("/hooks")}`);
703
- console.log(` ${cyan2("2.")} You will see the Pushary hook commands. Trust them.`);
719
+ console.log(` ${cyan("1.")} Open Codex and type ${cyan("/hooks")}`);
720
+ console.log(` ${cyan("2.")} You will see the Pushary hook commands. Trust them.`);
704
721
  console.log();
705
- console.log(` ${dim2("Until you do, Codex just runs as usual without the phone approvals.")}`);
706
- console.log(` ${dim2("To confirm:")} run a command that needs approval and watch your phone.`);
707
- console.log(` ${dim2("Pushary updates keep the same trust, so Codex will not ask again.")}`);
722
+ console.log(` ${dim("Until you do, Codex just runs as usual without the phone approvals.")}`);
723
+ console.log(` ${dim("To confirm:")} run a command that needs approval and watch your phone.`);
724
+ console.log(` ${dim("Pushary updates keep the same trust, so Codex will not ask again.")}`);
725
+ noteManual("Open Codex, type /hooks, and trust the Pushary hook. Until then Codex runs without phone approvals.");
708
726
  }
709
727
  } else {
710
- console.log(` ${dim2("\u2022")} Notify handler (deprecated): captures turn completions and approval requests`);
728
+ console.log(` ${dim("\u2022")} Notify handler (deprecated): captures turn completions and approval requests`);
711
729
  }
712
730
  };
713
731
  var resolveBundledPlugin = () => {
714
732
  const dir = dirname(fileURLToPath(import.meta.url));
715
733
  const candidates = [
716
- join(dir, "..", "..", "data", "cursor-plugin"),
717
- join(dir, "..", "data", "cursor-plugin"),
718
- join(dir, "..", "..", "..", "cursor-plugin"),
719
- join(dir, "..", "..", "cursor-plugin")
734
+ join2(dir, "..", "..", "data", "cursor-plugin"),
735
+ join2(dir, "..", "data", "cursor-plugin"),
736
+ join2(dir, "..", "..", "..", "cursor-plugin"),
737
+ join2(dir, "..", "..", "cursor-plugin")
720
738
  ];
721
- return candidates.find((p) => existsSync(join(p, ".cursor-plugin", "plugin.json"))) ?? null;
739
+ return candidates.find((p) => existsSync2(join2(p, ".cursor-plugin", "plugin.json"))) ?? null;
722
740
  };
723
741
  var installCursorUserHooks = (gateScript) => {
724
- const template = readJson(join(CURSOR_PLUGIN_DIR, "hooks", "hooks.json")).hooks?.beforeShellExecution?.[0];
742
+ const template = readJson(join2(CURSOR_PLUGIN_DIR, "hooks", "hooks.json")).hooks?.beforeShellExecution?.[0];
725
743
  if (!template) throw new Error("bundled Cursor hooks.json missing a beforeShellExecution entry");
726
744
  const entry = { ...template, command: `node "${gateScript}"` };
727
745
  let userHooks = {};
728
- if (existsSync(CURSOR_USER_HOOKS)) {
746
+ if (existsSync2(CURSOR_USER_HOOKS)) {
729
747
  try {
730
748
  userHooks = JSON.parse(readFileSync(CURSOR_USER_HOOKS, "utf-8"));
731
749
  } catch {
@@ -743,8 +761,8 @@ var installCursorUserHooks = (gateScript) => {
743
761
  writeJson(CURSOR_USER_HOOKS, { ...userHooks, version: userHooks.version ?? 1, hooks });
744
762
  };
745
763
  var neutralizePluginGate = () => {
746
- const path = join(CURSOR_PLUGIN_DIR, "hooks", "hooks.json");
747
- if (!existsSync(path)) return;
764
+ const path = join2(CURSOR_PLUGIN_DIR, "hooks", "hooks.json");
765
+ if (!existsSync2(path)) return;
748
766
  const data = readJson(path);
749
767
  if (data.hooks && "beforeShellExecution" in data.hooks) {
750
768
  delete data.hooks.beforeShellExecution;
@@ -753,85 +771,122 @@ var neutralizePluginGate = () => {
753
771
  };
754
772
  var setupCursor = async (apiKey) => {
755
773
  console.log(`
756
- ${bold2("Setting up Cursor")}
774
+ ${bold("Setting up Cursor")}
757
775
  `);
758
776
  const source = resolveBundledPlugin();
759
777
  if (!source) throw new Error("bundled Cursor plugin not found in this package");
760
778
  await spinner("Installing Pushary plugin", async () => {
761
- rmSync(CURSOR_PLUGIN_DIR, { recursive: true, force: true });
762
- cpSync(source, CURSOR_PLUGIN_DIR, {
763
- recursive: true,
764
- filter: (p) => !["tools", "node_modules", ".git", ".DS_Store"].includes(basename(p))
765
- });
779
+ const staging = join2(dirname(CURSOR_PLUGIN_DIR), `.pushary-staging-${process.pid}`);
780
+ const backup = `${CURSOR_PLUGIN_DIR}.pushary-backup-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}`;
781
+ rmSync(staging, { recursive: true, force: true });
782
+ try {
783
+ cpSync(source, staging, {
784
+ recursive: true,
785
+ filter: (p) => !["tools", "node_modules", ".git", ".DS_Store"].includes(basename(p))
786
+ });
787
+ const staged = readJsonSafe(join2(staging, ".cursor-plugin", "plugin.json"));
788
+ if (staged.kind !== "ok") {
789
+ throw new Error("staged Cursor plugin is missing or has an unreadable plugin.json");
790
+ }
791
+ const hadExisting = existsSync2(CURSOR_PLUGIN_DIR);
792
+ if (hadExisting) renameSync(CURSOR_PLUGIN_DIR, backup);
793
+ try {
794
+ renameSync(staging, CURSOR_PLUGIN_DIR);
795
+ } catch (err) {
796
+ if (hadExisting) renameSync(backup, CURSOR_PLUGIN_DIR);
797
+ throw err;
798
+ }
799
+ if (hadExisting) rmSync(backup, { recursive: true, force: true });
800
+ } finally {
801
+ rmSync(staging, { recursive: true, force: true });
802
+ }
766
803
  });
767
804
  await spinner("Linking your API key", async () => {
768
- const mcpPath = join(CURSOR_PLUGIN_DIR, "mcp.json");
769
- const mcp = readJson(mcpPath);
805
+ const mcpPath = join2(CURSOR_PLUGIN_DIR, "mcp.json");
806
+ const mcp = readAgentJson(mcpPath);
770
807
  const servers = mcp.mcpServers ?? {};
771
808
  if (servers.pushary) {
772
809
  servers.pushary.headers = { ...servers.pushary.headers, Authorization: `Bearer ${apiKey}` };
773
810
  mcp.mcpServers = servers;
774
- writeJson(mcpPath, mcp);
811
+ writeJsonAtomic(mcpPath, mcp, KEY_FILE_MODE);
775
812
  }
776
813
  });
777
814
  await spinner("Registering permission gate (~/.cursor/hooks.json)", async () => {
778
- installCursorUserHooks(join(CURSOR_PLUGIN_DIR, "scripts", "pushary-gate.mjs"));
815
+ installCursorUserHooks(join2(CURSOR_PLUGIN_DIR, "scripts", "pushary-gate.mjs"));
779
816
  neutralizePluginGate();
780
817
  });
781
818
  console.log();
782
- console.log(` ${dim2("What this configured:")}`);
783
- console.log(` ${dim2("\u2022")} Plugin installed to ~/.cursor/plugins/local/pushary (MCP tools, rule, skill)`);
784
- console.log(` ${dim2("\u2022")} Permission gate registered once in ~/.cursor/hooks.json`);
785
- console.log(` ${dim2("\u2022")} Risky shell commands route to push approval before they run`);
786
- console.log(` ${dim2("\u2022")} Fully quit and reopen Cursor to load it (a Reload Window may not be enough)`);
819
+ console.log(` ${dim("What this configured:")}`);
820
+ console.log(` ${dim("\u2022")} Plugin installed to ~/.cursor/plugins/local/pushary (MCP tools, rule, skill)`);
821
+ console.log(` ${dim("\u2022")} Permission gate registered once in ~/.cursor/hooks.json`);
822
+ console.log(` ${dim("\u2022")} Risky shell commands route to push approval before they run`);
823
+ console.log(` ${dim("\u2022")} Fully quit and reopen Cursor to load it (a Reload Window may not be enough)`);
824
+ noteManual("Fully quit and reopen Cursor. A Reload Window may not be enough.");
787
825
  };
788
826
  var saveApiKey = async (apiKey) => {
827
+ let result;
789
828
  await spinner("Saving your API key", async () => {
790
- if (!existsSync(PUSHARY_CONFIG_DIR)) mkdirSync(PUSHARY_CONFIG_DIR, { recursive: true });
791
- const config = readJson(PUSHARY_CONFIG_FILE);
792
- writeFileSync(PUSHARY_CONFIG_FILE, JSON.stringify({ ...config, apiKey }, null, 2) + "\n", "utf-8");
793
- try {
794
- chmodSync(PUSHARY_CONFIG_FILE, 384);
795
- } catch {
796
- }
797
- const exportLine = `
798
- export PUSHARY_API_KEY='${apiKey}'
799
- `;
800
- const shellFile = SHELL_FILES.find((f) => existsSync(f));
801
- if (shellFile) {
802
- const content = readFileSync(shellFile, "utf-8");
803
- if (!content.includes("PUSHARY_API_KEY")) {
804
- appendFileSync(shellFile, exportLine, "utf-8");
805
- }
806
- }
807
- process.env.PUSHARY_API_KEY = apiKey;
829
+ result = writeKey(apiKey);
808
830
  });
831
+ if (!result) throw new Error("Could not save the API key");
832
+ if (result.configBackupPath) {
833
+ console.log(` ${yellow("!")} ${PUSHARY_CONFIG_FILE} did not parse. Saved a copy at ${dim(result.configBackupPath)}`);
834
+ }
835
+ const rc = result.rc;
836
+ switch (rc.kind) {
837
+ case "updated":
838
+ console.log(` ${check} Replaced the old key in ${dim(rc.path ?? "")}${rc.previousKeyPrefix ? dim(` (was ${rc.previousKeyPrefix})`) : ""}`);
839
+ break;
840
+ case "declined":
841
+ console.log(` ${yellow("!")} ${rc.path}:${rc.declinedLine} already sets ${cyan("PUSHARY_API_KEY")} in a form Pushary did not write.`);
842
+ console.log(` ${dim("It was left alone, and it will shadow the new key in new shells. Update it to:")}`);
843
+ console.log(` ${cyan(exportLine(apiKey))}`);
844
+ break;
845
+ case "unavailable":
846
+ console.log(` ${yellow("!")} Could not update ${rc.path}${rc.detail ? dim(` (${rc.detail})`) : ""}. Add this line yourself:`);
847
+ console.log(` ${cyan(exportLine(apiKey))}`);
848
+ break;
849
+ case "no-rc":
850
+ console.log(` ${yellow("!")} No shell profile found for this shell. Add this line yourself:`);
851
+ console.log(` ${cyan(exportLine(apiKey))}`);
852
+ break;
853
+ case "written":
854
+ case "unchanged":
855
+ break;
856
+ }
857
+ return rc;
809
858
  };
810
- var setupCustom = async (_apiKey) => {
859
+ var setupCustom = async (apiKey, context) => {
811
860
  console.log(`
812
- ${bold2("Connect any other agent (MCP or HTTP)")}
861
+ ${bold("Connect any other agent (MCP or HTTP)")}
813
862
  `);
814
- console.log(` ${dim2("Your key is saved as")} ${cyan2("$PUSHARY_API_KEY")} ${dim2("in your shell profile.")}`);
863
+ const wroteExport = context ? ["written", "updated", "unchanged"].includes(context.rc.kind) : true;
864
+ if (wroteExport) {
865
+ console.log(` ${dim("Your key is saved as")} ${cyan("$PUSHARY_API_KEY")} ${dim("in your shell profile.")}`);
866
+ } else {
867
+ console.log(` ${dim("Add this to your shell profile so")} ${cyan("$PUSHARY_API_KEY")} ${dim("resolves:")}`);
868
+ console.log(` ${cyan(exportLine(apiKey))}`);
869
+ }
815
870
  console.log();
816
- console.log(` ${dim2("MCP clients (Windsurf, LangChain, custom) \u2014 add this server:")}`);
817
- console.log(` ${cyan2("https://pushary.com/api/mcp/mcp")} ${dim2("with header")} ${cyan2("Authorization: Bearer $PUSHARY_API_KEY")}`);
871
+ console.log(` ${dim("MCP clients (Windsurf, LangChain, custom) \u2014 add this server:")}`);
872
+ console.log(` ${cyan("https://pushary.com/api/mcp/mcp")} ${dim("with header")} ${cyan("Authorization: Bearer $PUSHARY_API_KEY")}`);
818
873
  console.log();
819
- console.log(` ${dim2("No-code / HTTP (n8n, Zapier, scripts) \u2014 ask a human and wait:")}`);
820
- console.log(` ${dim2("POST")} ${cyan2("https://pushary.com/api/v1/server/ask")} ${dim2("body")} ${cyan2('{"question":"Approve?","type":"confirm","wait":true}')}`);
874
+ console.log(` ${dim("No-code / HTTP (n8n, Zapier, scripts) \u2014 ask a human and wait:")}`);
875
+ console.log(` ${dim("POST")} ${cyan("https://pushary.com/api/v1/server/ask")} ${dim("body")} ${cyan('{"question":"Approve?","type":"confirm","wait":true}')}`);
821
876
  console.log();
822
- console.log(` ${dim2("Full guide:")} ${cyan2("https://pushary.com/docs/agents/connect-any-agent")}`);
877
+ console.log(` ${dim("Full guide:")} ${cyan("https://pushary.com/docs/agents/connect-any-agent")}`);
823
878
  };
824
879
  var setupGemini = async (apiKey) => {
825
880
  console.log(`
826
- ${bold2("Setting up Gemini CLI")}
881
+ ${bold("Setting up Gemini CLI")}
827
882
  `);
828
883
  if (!isInstalled("gemini")) {
829
- console.log(` ${yellow2("!")} Gemini CLI not found. Skipping.`);
830
- console.log(` ${dim2("Install Gemini CLI and re-run setup to configure.")}`);
831
- return;
884
+ console.log(` ${yellow("!")} Gemini CLI not found. Skipping.`);
885
+ console.log(` ${dim("Install Gemini CLI and re-run setup to configure.")}`);
886
+ return "skipped";
832
887
  }
833
- await installGlobally();
834
- const settings = readJson(GEMINI_SETTINGS);
888
+ await installGlobally2();
889
+ const settings = readAgentJson(GEMINI_SETTINGS);
835
890
  await spinner("Adding MCP server (~/.gemini/settings.json)", async () => {
836
891
  addGeminiMcpServer(settings, apiKey);
837
892
  });
@@ -839,19 +894,20 @@ var setupGemini = async (apiKey) => {
839
894
  addGeminiHooks(settings, quoteIfNeeded(resolveGlobalBinary(GEMINI_HOOK_BINARY) ?? GEMINI_HOOK_BINARY));
840
895
  });
841
896
  await spinner(`Writing ${GEMINI_SETTINGS}`, async () => {
842
- writeJson(GEMINI_SETTINGS, settings);
897
+ writeJsonAtomic(GEMINI_SETTINGS, settings, KEY_FILE_MODE);
843
898
  });
844
899
  await spinner("Teaching Gemini to ask via push (~/.gemini/GEMINI.md)", async () => {
845
900
  writeInstructionBlock(GEMINI_MD, renderAgentInstructions("Gemini"));
846
901
  });
847
902
  console.log();
848
- console.log(` ${dim2("What this configured:")}`);
849
- console.log(` ${dim2("\u2022")} MCP server: Gemini can send notifications and ask questions`);
850
- console.log(` ${dim2("\u2022")} Proactive instructions: ~/.gemini/GEMINI.md tells Gemini to ask via push, not the terminal`);
851
- console.log(` ${dim2("\u2022")} Hooks: phone approvals, policy enforcement, kill switch, session tracking`);
852
- console.log(` ${dim2("\u2022")} Gate covers ${bold2("run_shell_command")}, ${bold2("write_file")}, and ${bold2("replace")}`);
853
- console.log(` ${dim2("\u2022")} Auto-allowed tools: Pushary MCP calls run without a confirmation prompt`);
854
- console.log(` ${dim2("Restart Gemini CLI to load the new config.")}`);
903
+ console.log(` ${dim("What this configured:")}`);
904
+ console.log(` ${dim("\u2022")} MCP server: Gemini can send notifications and ask questions`);
905
+ console.log(` ${dim("\u2022")} Proactive instructions: ~/.gemini/GEMINI.md tells Gemini to ask via push, not the terminal`);
906
+ console.log(` ${dim("\u2022")} Hooks: phone approvals, policy enforcement, kill switch, session tracking`);
907
+ console.log(` ${dim("\u2022")} Gate covers ${bold("run_shell_command")}, ${bold("write_file")}, and ${bold("replace")}`);
908
+ console.log(` ${dim("\u2022")} Auto-allowed tools: Pushary MCP calls run without a confirmation prompt`);
909
+ console.log(` ${dim("Restart Gemini CLI to load the new config.")}`);
910
+ noteManual("Restart Gemini CLI to load the new config.");
855
911
  };
856
912
  var AGENT_SETUP = {
857
913
  claude_code: setupClaudeCode,
@@ -865,9 +921,9 @@ var PROJECT_INSTRUCTION_TARGETS = {
865
921
  claude_code: { file: "CLAUDE.md", label: "Claude Code" },
866
922
  codex: { file: "AGENTS.md", label: "Codex" }
867
923
  };
868
- var offerProjectInstructions = async (agents) => {
924
+ var offerProjectInstructions = async (agents, options) => {
869
925
  const targets = agents.map((agent) => PROJECT_INSTRUCTION_TARGETS[agent]).filter((target) => target !== void 0);
870
- if (targets.length === 0 || !process.stdout.isTTY) return;
926
+ if (targets.length === 0 || !process.stdout.isTTY || options.nonInteractive) return;
871
927
  console.log();
872
928
  const wanted = await confirm({
873
929
  message: "Add Pushary instructions to this project's CLAUDE.md/AGENTS.md so your whole team's agents can use phone approvals?",
@@ -875,133 +931,310 @@ var offerProjectInstructions = async (agents) => {
875
931
  });
876
932
  if (!wanted) return;
877
933
  for (const target of targets) {
878
- await spinner(`Writing managed block to ${join(process.cwd(), target.file)}`, async () => {
879
- writeInstructionBlock(join(process.cwd(), target.file), renderProjectAgentInstructions(target.label));
934
+ await spinner(`Writing managed block to ${join2(process.cwd(), target.file)}`, async () => {
935
+ writeInstructionBlock(join2(process.cwd(), target.file), renderProjectAgentInstructions(target.label));
880
936
  }, { optional: true });
881
937
  }
882
- console.log(` ${dim2("Commit the file to share it. Teammates without a key fall back to the terminal.")}`);
938
+ console.log(` ${dim("Commit the file to share it. Teammates without a key fall back to the terminal.")}`);
883
939
  };
884
- var offerClaudeAlias = async () => {
885
- if (!process.stdout.isTTY) return;
940
+ var offerClaudeAlias = async (options) => {
941
+ if (!process.stdout.isTTY || options.nonInteractive) return;
886
942
  const rc = resolveShellRc();
887
943
  console.log();
888
944
  if (!rc) {
889
- console.log(` ${dim2("Tip: alias")} ${cyan2("claude='pushary claude'")} ${dim2("in your shell to make plain")} ${cyan2("claude")} ${dim2("reachable from your phone.")}`);
945
+ console.log(` ${dim("Tip: alias")} ${cyan("claude='pushary claude'")} ${dim("in your shell to make plain")} ${cyan("claude")} ${dim("reachable from your phone.")}`);
890
946
  return;
891
947
  }
892
948
  const wanted = await confirm({
893
- message: `Make plain ${bold2("claude")} reachable from your phone by default? (adds an alias to ${basename(rc.path)}, reversible)`,
949
+ message: `Make plain ${bold("claude")} reachable from your phone by default? (adds an alias to ${basename(rc.path)}, reversible)`,
894
950
  default: false
895
951
  });
896
952
  if (!wanted) return;
897
953
  const result = installClaudeAlias();
898
954
  if (result.installed) {
899
- console.log(` ${check2} Aliased ${cyan2("claude")} \u2192 ${cyan2("pushary claude")} in ${dim2(result.path ?? rc.path)}`);
900
- console.log(` ${dim2("Open a new terminal (or")} ${cyan2(`source ${rc.path}`)}${dim2(") to use it. Undo any time with")} ${cyan2("pushary clean")}${dim2(".")}`);
955
+ console.log(` ${check} Aliased ${cyan("claude")} \u2192 ${cyan("pushary claude")} in ${dim(result.path ?? rc.path)}`);
956
+ console.log(` ${dim("Open a new terminal (or")} ${cyan(`source ${rc.path}`)}${dim(") to use it. Undo any time with")} ${cyan("pushary clean")}${dim(".")}`);
901
957
  } else if (result.reason === "exists") {
902
- console.log(` ${check2} ${dim2("The")} ${cyan2("claude")} ${dim2("alias is already set.")}`);
958
+ console.log(` ${check} ${dim("The")} ${cyan("claude")} ${dim("alias is already set.")}`);
903
959
  }
904
960
  };
961
+ var verifyKeyOrExit = async (apiKey, options) => {
962
+ const live = process.stdout.isTTY === true && !options.json;
963
+ if (live) writeHuman(` ${dim("...")} Verifying your key with pushary.com`);
964
+ let result;
965
+ try {
966
+ result = await checkApiKey(apiKey);
967
+ } catch (err) {
968
+ result = { kind: "unreachable", detail: formatError(err) };
969
+ }
970
+ if (live) writeHuman("\r\x1B[2K");
971
+ const report = describeKeyCheck(result);
972
+ if (report.fatal) {
973
+ console.log(` ${yellow("!")} ${report.message}`);
974
+ for (const line of report.next) console.log(` ${dim(line)}`);
975
+ console.log();
976
+ process.exit(report.exitCode);
977
+ }
978
+ if (result.kind === "ok") {
979
+ const { siteSlug, siteName, plan } = result.identity;
980
+ console.log(` ${check} Key verified ${dim(`(${siteSlug || siteName} \xB7 plan ${plan})`)}`);
981
+ } else {
982
+ console.log(` ${yellow("!")} ${report.message}`);
983
+ for (const line of report.next) console.log(` ${dim(line)}`);
984
+ }
985
+ return result;
986
+ };
987
+ var AGENT_NAMES = {
988
+ claude_code: "Claude Code",
989
+ codex: "Codex",
990
+ gemini_cli: "Gemini CLI",
991
+ hermes: "Hermes",
992
+ cursor: "Cursor",
993
+ custom: "Other"
994
+ };
995
+ var AGENT_CAPABILITIES = {
996
+ claude_code: "MCP + hooks + auto-allowed tools",
997
+ codex: "MCP + native hooks + auto-allowed tools",
998
+ gemini_cli: "MCP + native hooks + auto-allowed tools",
999
+ hermes: "native plugin + auto-error notifications",
1000
+ cursor: "plugin + permission gate",
1001
+ custom: "any MCP or HTTP agent (Windsurf, n8n, custom)"
1002
+ };
1003
+ var NAME_COLUMN = Math.max(...Object.values(AGENT_NAMES).map((name) => name.length)) + 2;
1004
+ var AGENT_LABELS = Object.fromEntries(
1005
+ Object.keys(AGENT_NAMES).map((agent) => [
1006
+ agent,
1007
+ `${AGENT_NAMES[agent].padEnd(NAME_COLUMN)}${dim(AGENT_CAPABILITIES[agent])}`
1008
+ ])
1009
+ );
1010
+ var detectAgents = () => {
1011
+ const map = {};
1012
+ for (const entry of detectAllAgents()) {
1013
+ map[entry.agent] = { found: isDetected(entry.result), evidence: describeDetection(entry.result) };
1014
+ }
1015
+ return map;
1016
+ };
1017
+ var canRunBrowserLogin = () => process.stdout.isTTY === true && !process.env.CI;
1018
+ var resolveAgents = async (options) => {
1019
+ if (options.agents.kind === "none") return [];
1020
+ if (options.agents.kind === "explicit") {
1021
+ const detected2 = detectAgents();
1022
+ for (const agent of options.agents.agents) {
1023
+ if (agent !== "custom" && !detected2[agent].found) {
1024
+ console.log(` ${yellow("!")} ${AGENT_NAMES[agent]} was not detected on this machine.`);
1025
+ console.log(` ${dim("Continuing anyway. The steps below say what was configured.")}`);
1026
+ }
1027
+ }
1028
+ return options.agents.agents;
1029
+ }
1030
+ const detected = detectAgents();
1031
+ if (options.agents.kind === "auto") {
1032
+ const picked = Object.keys(detected).filter((agent) => detected[agent].found);
1033
+ if (picked.length === 0) {
1034
+ console.log(` ${yellow("!")} No agents detected, so nothing was configured. Your key is saved.`);
1035
+ console.log(` ${dim("Install an agent and re-run, name one with --agents <id>, or pass --agents none to accept this.")}`);
1036
+ console.log();
1037
+ process.exit(EXIT.PROBLEMS_FOUND);
1038
+ }
1039
+ console.log(` ${check} Selected automatically: ${picked.map((a) => AGENT_NAMES[a]).join(", ")} ${dim("(--agents to choose)")}`);
1040
+ return picked;
1041
+ }
1042
+ const hint = Object.values(detected).some((entry) => entry.found) ? "(detected agents pre-selected)" : "(space = toggle, enter = confirm)";
1043
+ return checkbox({
1044
+ message: "Which agents do you use? " + dim(hint),
1045
+ choices: Object.keys(AGENT_LABELS).map((agent) => ({
1046
+ // The evidence rides along so a detection the user disagrees with is
1047
+ // visible and correctable, rather than an unexplained pre-tick.
1048
+ name: detected[agent].evidence ? `${AGENT_LABELS[agent]} ${dim(`[${detected[agent].evidence}]`)}` : AGENT_LABELS[agent],
1049
+ value: agent,
1050
+ checked: detected[agent].found
1051
+ })),
1052
+ // Today an empty selection skips the loop and still prints "Setup complete."
1053
+ validate: (picked) => picked.length > 0 || "Select at least one agent, or re-run with --agents none."
1054
+ });
1055
+ };
1056
+ var AGENT_TARGETS = {
1057
+ claude_code: [CLAUDE_JSON, CLAUDE_SETTINGS, join2(CLAUDE_SKILL_DIR, "SKILL.md")],
1058
+ codex: [join2(CODEX_HOME, "config.toml"), CODEX_HOOKS_JSON, join2(CODEX_SKILL_DIR, "SKILL.md"), CODEX_AGENTS_MD],
1059
+ gemini_cli: [GEMINI_SETTINGS, GEMINI_MD],
1060
+ hermes: ["the Hermes virtualenv (pip install pushary-hermes)"],
1061
+ cursor: [join2(CURSOR_PLUGIN_DIR, "mcp.json"), CURSOR_USER_HOOKS],
1062
+ custom: ["nothing (prints connection details only)"]
1063
+ };
1064
+ var reportDryRun = (apiKey, agents, keyCheck) => {
1065
+ console.log();
1066
+ console.log(` ${bold("Dry run")} ${dim("\u2014 nothing below has been written")}`);
1067
+ console.log();
1068
+ console.log(` ${dim("Key:")} ${dim(`${apiKey.slice(0, 8)}...${apiKey.slice(-4)}`)} ${dim(`(${keyCheck?.kind ?? "not verified"})`)}`);
1069
+ console.log(` ${dim("Key store:")} ${PUSHARY_CONFIG_FILE}`);
1070
+ const rc = resolveShellRc();
1071
+ console.log(` ${dim("Shell rc:")} ${rc ? rc.path : dim("none for this shell; the export line would be printed instead")}`);
1072
+ console.log();
1073
+ if (agents.length === 0) {
1074
+ console.log(` ${dim("Agents:")} ${dim("none")}`);
1075
+ } else {
1076
+ console.log(` ${dim("Agents:")} ${agents.map((a) => a.replace("_", " ")).join(", ")}`);
1077
+ for (const agent of agents) {
1078
+ console.log();
1079
+ console.log(` ${bold(agent.replace("_", " "))}`);
1080
+ for (const target of AGENT_TARGETS[agent]) console.log(` ${dim("would write")} ${target}`);
1081
+ }
1082
+ }
1083
+ console.log();
1084
+ console.log(` ${dim("Re-run without --dry-run to apply.")}`);
1085
+ console.log();
1086
+ };
1087
+ var printManualSteps = () => {
1088
+ if (manualSteps.length === 0) return;
1089
+ console.log(` ${bold("Still to do")}`);
1090
+ for (const step of manualSteps) console.log(` ${dim("\u2022")} ${step}`);
1091
+ console.log();
1092
+ };
1093
+ var emitJson = (options, payload) => {
1094
+ if (!options.json) return;
1095
+ process.stdout.write(`${JSON.stringify(payload, null, 2)}
1096
+ `);
1097
+ };
905
1098
  var main = async () => {
1099
+ const options = parseSetupOptions({
1100
+ argv: process.argv.slice(2),
1101
+ stdinIsTty: process.stdin.isTTY === true
1102
+ });
1103
+ if (options.help) {
1104
+ process.stdout.write(`${renderCommandHelp("setup", getPackageVersion())}
1105
+ `);
1106
+ process.exit(EXIT.OK);
1107
+ }
1108
+ if (options.json) {
1109
+ humanStream = process.stderr;
1110
+ setHumanStream(process.stderr);
1111
+ console.log = (...args) => {
1112
+ process.stderr.write(`${args.map((a) => typeof a === "string" ? a : String(a)).join(" ")}
1113
+ `);
1114
+ };
1115
+ }
906
1116
  const version = getPackageVersion();
907
1117
  console.log();
908
- console.log(` ${bold2("Pushary")} ${dim2("v" + version)}`);
909
- console.log(` ${dim2("Push notifications for AI coding agents")}`);
1118
+ console.log(` ${bold("Pushary")} ${dim("v" + version)}`);
1119
+ console.log(` ${dim("Push notifications for AI agents")}`);
910
1120
  console.log();
911
1121
  await checkForUpdates(version);
912
- const connectMode = parseConnectFlag();
913
- const flagKey = parseKeyFlag();
1122
+ const connectMode = options.connectMode;
1123
+ const flagKey = await readKeyFromStdin(options) ?? options.key ?? void 0;
914
1124
  const envKey = process.env.PUSHARY_API_KEY?.trim();
915
1125
  let trimmedKey;
916
1126
  if (connectMode === "app") {
917
- console.log(` ${dim2("Pairing with your Pushary app.")}`);
1127
+ console.log(` ${dim("Pairing with your Pushary app.")}`);
918
1128
  const paired = await connectViaAppPairing();
919
1129
  if (!paired) {
920
1130
  console.log();
921
- console.log(` ${yellow2("!")} Pairing didn't complete. Re-run setup when you're ready to scan.`);
922
- console.log(` ${dim2("No app yet? Get it at")} ${cyan2("https://pushary.com/app")}`);
1131
+ console.log(` ${yellow("!")} Pairing didn't complete. Re-run setup when you're ready to scan.`);
1132
+ console.log(` ${dim("No app yet? Get it at")} ${cyan("https://pushary.com/download")}`);
923
1133
  process.exit(1);
924
1134
  }
925
1135
  trimmedKey = paired.apiKey;
926
1136
  if (paired.handle) {
927
- console.log(` ${check2} Connected to ${cyan2("@" + paired.handle)}`);
1137
+ console.log(` ${check} Connected to ${cyan("@" + paired.handle)}`);
928
1138
  }
929
1139
  console.log();
930
1140
  } else if (flagKey && isValidApiKey(flagKey)) {
931
1141
  const masked = `${flagKey.slice(0, 8)}...${flagKey.slice(-4)}`;
932
- console.log(` ${check2} Using API key: ${dim2(masked)}`);
1142
+ console.log(` ${check} Using API key: ${dim(masked)}`);
933
1143
  console.log();
934
1144
  trimmedKey = flagKey;
935
1145
  } else if (envKey && isValidApiKey(envKey)) {
936
1146
  const masked = `${envKey.slice(0, 8)}...${envKey.slice(-4)}`;
937
- console.log(` ${check2} Found API key in environment: ${dim2(masked)}`);
1147
+ console.log(` ${check} Found API key in environment: ${dim(masked)}`);
938
1148
  console.log();
939
- const useExisting = await confirm({ message: "Use this key?", default: true });
1149
+ const useExisting = options.nonInteractive ? true : await confirm({ message: "Use this key?", default: true });
940
1150
  if (useExisting) {
941
1151
  trimmedKey = envKey;
942
1152
  } else {
943
1153
  const apiKey = await input({ message: "API key:" });
944
1154
  trimmedKey = apiKey.trim();
945
1155
  }
946
- } else {
947
- console.log(` ${dim2("Paste your API key from the onboarding page.")}`);
948
- console.log(` ${dim2("Can't find it? Copy it from:")} ${cyan2("pushary.com/dashboard/agent/settings")}`);
1156
+ } else if (!canRunBrowserLogin()) {
1157
+ console.log(` ${yellow("!")} No API key, and no terminal to sign in from.`);
1158
+ console.log(` ${dim("Pass --key <pk_xxx.xxx>, or --key-stdin, or export PUSHARY_API_KEY.")}`);
1159
+ console.log(` ${dim("On a machine with a terminal, run")} ${cyan("pushary login")} ${dim("once and re-run this.")}`);
949
1160
  console.log();
950
- const apiKey = await input({ message: "API key:" });
951
- trimmedKey = apiKey.trim();
1161
+ process.exit(EXIT.NOT_CONFIGURED);
1162
+ } else {
1163
+ const outcome = await runBrowserLogin(
1164
+ { say: (line) => console.log(line), bold, dim, cyan, yellow },
1165
+ { cliVersion: version }
1166
+ );
1167
+ if (outcome.kind === "ok") {
1168
+ trimmedKey = outcome.apiKey;
1169
+ console.log();
1170
+ console.log(` ${check} Signed in${outcome.siteSlug ? ` ${dim(`(${outcome.siteSlug})`)}` : ""}`);
1171
+ console.log();
1172
+ } else if (outcome.kind === "rejected") {
1173
+ console.log();
1174
+ console.log(` ${yellow("!")} That request was rejected in the browser. Nothing was written.`);
1175
+ console.log();
1176
+ process.exit(EXIT.NOT_CONFIGURED);
1177
+ } else {
1178
+ console.log();
1179
+ if (outcome.kind === "unavailable") {
1180
+ console.log(` ${yellow("!")} Couldn't reach the login service ${dim(`(${outcome.detail})`)}`);
1181
+ } else {
1182
+ console.log(` ${yellow("!")} The code expired before it was approved.`);
1183
+ }
1184
+ console.log(` ${dim("Paste a key instead, or press Ctrl-C and re-run setup for a fresh code.")}`);
1185
+ console.log(` ${dim("Keys live at")} ${cyan("pushary.com/dashboard/agent/settings")}`);
1186
+ console.log();
1187
+ const apiKey = await input({ message: "API key:" });
1188
+ trimmedKey = apiKey.trim();
1189
+ }
952
1190
  }
953
1191
  if (!trimmedKey || !isValidApiKey(trimmedKey)) {
954
1192
  console.log(`
955
- ${yellow2("!")} Invalid key format. Expected: ${dim2("pk_xxx.xxx")}`);
956
- console.log(` ${dim2("Copy your key from")} ${cyan2("https://pushary.com/dashboard/agent/settings")}`);
957
- console.log(` ${dim2("Or sign up at")} ${cyan2("https://pushary.com/sign-up?from=ai-coding")}
1193
+ ${yellow("!")} Invalid key format. Expected: ${dim("pk_xxx.xxx")}`);
1194
+ console.log(` ${dim("Copy your key from")} ${cyan("https://pushary.com/dashboard/agent/settings")}`);
1195
+ console.log(` ${dim("Or sign up at")} ${cyan("https://pushary.com/sign-up?from=ai-coding")}
958
1196
  `);
959
- process.exit(1);
960
- }
961
- const detected = {
962
- claude_code: isInstalled("claude"),
963
- codex: isInstalled("codex"),
964
- gemini_cli: isInstalled("gemini"),
965
- hermes: isInstalled("hermes"),
966
- cursor: isInstalled("cursor"),
967
- custom: false
968
- };
969
- const hint = Object.values(detected).some(Boolean) ? "(detected agents pre-selected)" : "(space = toggle, enter = confirm)";
970
- const agents = await checkbox({
971
- message: "Which agents do you use? " + dim2(hint),
972
- choices: [
973
- { name: `Claude Code ${dim2("MCP + hooks + auto-allowed tools")}`, value: "claude_code", checked: detected.claude_code },
974
- { name: `Codex ${dim2("MCP + native hooks + auto-allowed tools")}`, value: "codex", checked: detected.codex },
975
- { name: `Gemini CLI ${dim2("MCP + native hooks + auto-allowed tools")}`, value: "gemini_cli", checked: detected.gemini_cli },
976
- { name: `Hermes ${dim2("native plugin + auto-error notifications")}`, value: "hermes", checked: detected.hermes },
977
- { name: `Cursor ${dim2("plugin + permission gate")}`, value: "cursor", checked: detected.cursor },
978
- { name: `Other ${dim2("any MCP or HTTP agent (Windsurf, n8n, custom)")}`, value: "custom", checked: false }
979
- ]
980
- });
981
- await saveApiKey(trimmedKey);
1197
+ process.exit(EXIT.USAGE);
1198
+ }
1199
+ const keyCheck = connectMode === "app" ? null : await verifyKeyOrExit(trimmedKey, options);
1200
+ const agents = await resolveAgents(options);
1201
+ if (options.dryRun) {
1202
+ reportDryRun(trimmedKey, agents, keyCheck);
1203
+ emitJson(options, { ok: true, dryRun: true, agents, keyCheck: keyCheck?.kind ?? "skipped" });
1204
+ process.exit(EXIT.OK);
1205
+ }
1206
+ const rc = await saveApiKey(trimmedKey);
1207
+ const context = { rc };
1208
+ const failed = [];
1209
+ const completed = [];
1210
+ const skipped = [];
982
1211
  if (agents.length === 0) {
983
1212
  console.log(`
984
- ${dim2("No agents selected. API key saved.")}`);
1213
+ ${check} API key saved. No agents configured.`);
1214
+ console.log(` ${dim("Run setup again with --agents <id> when you want one wired up.")}`);
985
1215
  } else {
986
- const failed = [];
987
- const completed = [];
988
1216
  for (const agent of agents) {
989
1217
  try {
990
- await AGENT_SETUP[agent](trimmedKey);
991
- completed.push(agent);
1218
+ const outcome = await AGENT_SETUP[agent](trimmedKey, context);
1219
+ if (outcome === "skipped") skipped.push(agent);
1220
+ else completed.push(agent);
992
1221
  } catch (err) {
993
1222
  failed.push(agent.replace("_", " "));
994
- console.log(` ${yellow2("!")} ${agent.replace("_", " ")} setup failed: ${formatError(err)}`);
995
- console.log(` ${dim2("Other agents will continue. Re-run setup to retry.")}`);
1223
+ console.log(` ${yellow("!")} ${agent.replace("_", " ")} setup failed: ${formatError(err)}`);
1224
+ console.log(` ${dim("Other agents will continue. Re-run setup to retry.")}`);
996
1225
  }
997
1226
  }
1227
+ if (skipped.length > 0) {
1228
+ console.log();
1229
+ console.log(` ${yellow("!")} Not configured: ${skipped.map((a) => a.replace("_", " ")).join(", ")} ${dim("(not installed on this machine)")}`);
1230
+ }
998
1231
  if (failed.length > 0) {
999
1232
  console.log();
1000
- console.log(` ${yellow2("!")} Failed: ${failed.join(", ")} ${dim2("(others completed successfully)")}`);
1233
+ console.log(` ${yellow("!")} Failed: ${failed.join(", ")} ${dim("(others completed successfully)")}`);
1001
1234
  }
1002
- await offerProjectInstructions(agents);
1235
+ await offerProjectInstructions(agents, options);
1003
1236
  if (completed.includes("claude_code")) {
1004
- await offerClaudeAlias();
1237
+ await offerClaudeAlias(options);
1005
1238
  }
1006
1239
  if (completed.length > 0) {
1007
1240
  try {
@@ -1013,26 +1246,104 @@ var main = async () => {
1013
1246
  }
1014
1247
  }
1015
1248
  }
1016
- if (connectMode !== "app") {
1017
- const skipPhone = process.argv.includes("--skip-phone");
1018
- if (skipPhone || !process.stdout.isTTY) {
1019
- await printConnectInstructions(trimmedKey);
1020
- } else {
1021
- await connectDevice(trimmedKey);
1022
- }
1249
+ const skipPhone = options.skipPhone;
1250
+ let phone = null;
1251
+ if (keyCheck?.kind === "locked") {
1252
+ console.log();
1253
+ console.log(` ${dim("Skipping the delivery check: no approval can be delivered until the plan is active.")}`);
1254
+ } else if (skipPhone) {
1255
+ console.log();
1256
+ console.log(` ${dim("Skipping the phone check (--skip-phone).")}`);
1257
+ } else if (!process.stdout.isTTY) {
1258
+ phone = await printConnectInstructions(trimmedKey);
1259
+ } else if (connectMode === "app") {
1260
+ phone = await confirmAppConnection(trimmedKey);
1261
+ } else {
1262
+ phone = await connectDevice(trimmedKey, { showCapabilities: !options.connectExplicit });
1023
1263
  }
1024
1264
  console.log();
1025
- console.log(` ${green2(bold2("Setup complete."))}`);
1265
+ if (keyCheck?.kind === "locked") {
1266
+ const report = describeKeyCheck(keyCheck);
1267
+ printManualSteps();
1268
+ console.log(` ${yellow(bold("Agents configured, approvals inactive."))}`);
1269
+ console.log();
1270
+ console.log(` ${report.message}`);
1271
+ for (const line of report.next) console.log(` ${dim(line)}`);
1272
+ console.log();
1273
+ emitJson(options, {
1274
+ ok: false,
1275
+ reason: "plan_inactive",
1276
+ configured: completed,
1277
+ skipped,
1278
+ manualSteps,
1279
+ failed,
1280
+ keyCheck: "locked",
1281
+ rc: rc.kind,
1282
+ exitCode: report.exitCode
1283
+ });
1284
+ process.exit(report.exitCode);
1285
+ }
1286
+ printManualSteps();
1287
+ const configuredNothing = agents.length > 0 && completed.length === 0;
1288
+ const agentsOk = failed.length === 0 && !configuredNothing;
1289
+ const reachChecked = phone !== null;
1290
+ const reachesYou = phone?.reachesYou ?? true;
1291
+ const closing = describeClosing({
1292
+ agentsOk,
1293
+ configuredNothing,
1294
+ completed,
1295
+ failed,
1296
+ reachChecked,
1297
+ reachesYou
1298
+ });
1299
+ console.log(` ${closing.ok ? green(bold(closing.headline)) : yellow(bold(closing.headline))}`);
1300
+ if (failed.length > 0) {
1301
+ console.log();
1302
+ for (const agent of completed) console.log(` ${check} ${AGENT_NAMES[agent] ?? agent}`);
1303
+ for (const item of failed) console.log(` ${yellow("!")} ${item}`);
1304
+ }
1026
1305
  console.log();
1027
- console.log(` ${dim2("Next:")}`);
1028
- console.log(` ${dim2("1.")} Restart your agent to load the new config`);
1029
- console.log(` ${dim2("2.")} Run ${cyan2("npx @pushary/agent-hooks doctor")} to verify`);
1306
+ for (const line of closing.next) console.log(` ${line}`);
1030
1307
  console.log();
1308
+ const exitCode = closing.exitCode;
1309
+ emitJson(options, {
1310
+ ok: closing.ok,
1311
+ configured: completed,
1312
+ skipped,
1313
+ manualSteps,
1314
+ failed,
1315
+ keyCheck: keyCheck?.kind ?? "skipped",
1316
+ reach: phone === null ? "skipped" : phone.reachesYou ? "yours" : phone.outcome?.kind ?? phone.detail ?? "unreachable",
1317
+ rc: rc.kind,
1318
+ rcPath: rc.path,
1319
+ exitCode
1320
+ });
1321
+ if (exitCode !== EXIT.OK) {
1322
+ if (configuredNothing && failed.length === 0) {
1323
+ console.log(` ${yellow("!")} Nothing was configured: none of the agents you asked for are installed here.`);
1324
+ console.log(` ${dim("Your key is saved. Install one and re-run, or pass --agents none to accept this.")}`);
1325
+ console.log();
1326
+ }
1327
+ process.exit(exitCode);
1328
+ }
1031
1329
  };
1032
1330
  main().catch((err) => {
1331
+ if (err instanceof UsageError) {
1332
+ console.log();
1333
+ console.log(` ${yellow("!")} ${err.message}`);
1334
+ console.log(` ${dim("Run")} ${cyan("pushary setup --help")} ${dim("for the full list.")}`);
1335
+ console.log();
1336
+ process.exit(EXIT.USAGE);
1337
+ }
1338
+ if (isCancellation(err)) {
1339
+ console.log();
1340
+ console.log(` ${dim("Cancelled. Nothing further was changed.")}`);
1341
+ console.log();
1342
+ process.exit(130);
1343
+ }
1033
1344
  console.log();
1034
- console.log(` ${yellow2("!")} Setup failed: ${formatError(err)}`);
1035
- console.log(` ${dim2("Run")} ${cyan2("npx @pushary/agent-hooks doctor")} ${dim2("after fixing the issue, then rerun setup.")}`);
1345
+ console.log(` ${yellow("!")} Setup failed: ${formatError(err)}`);
1346
+ console.log(` ${dim("Run")} ${cyan("npx @pushary/agent-hooks doctor")} ${dim("after fixing the issue, then rerun setup.")}`);
1036
1347
  console.log();
1037
1348
  process.exit(1);
1038
1349
  });