@pushary/agent-hooks 0.9.1 → 0.10.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.
@@ -20,6 +20,156 @@ import { execSync } from "child_process";
20
20
  import { checkbox, input, confirm } from "@inquirer/prompts";
21
21
  import { fileURLToPath } from "url";
22
22
  import { parse as parseTOML, stringify as stringifyTOML } from "smol-toml";
23
+
24
+ // src/onboarding.ts
25
+ import qrcodeTerminal from "qrcode-terminal";
26
+ var dim = (s) => `\x1B[2m${s}\x1B[0m`;
27
+ var bold = (s) => `\x1B[1m${s}\x1B[0m`;
28
+ var green = (s) => `\x1B[32m${s}\x1B[0m`;
29
+ var cyan = (s) => `\x1B[36m${s}\x1B[0m`;
30
+ var yellow = (s) => `\x1B[33m${s}\x1B[0m`;
31
+ var check = green("\u2713");
32
+ var API_BASE = process.env.PUSHARY_API_URL?.trim() || "https://pushary.com";
33
+ var CONNECT_POLL_INTERVAL_MS = 2500;
34
+ var CONNECT_TIMEOUT_MS = 18e4;
35
+ var DELIVERY_POLL_INTERVAL_MS = 1500;
36
+ var DELIVERY_TIMEOUT_MS = 2e4;
37
+ var authHeaders = (apiKey) => ({
38
+ "Content-Type": "application/json",
39
+ Authorization: `Bearer ${apiKey}`
40
+ });
41
+ var getJson = async (url, apiKey) => {
42
+ try {
43
+ const res = await fetch(url, { headers: authHeaders(apiKey), signal: AbortSignal.timeout(1e4) });
44
+ if (!res.ok) return null;
45
+ return await res.json().catch(() => null);
46
+ } catch {
47
+ return null;
48
+ }
49
+ };
50
+ var fetchSite = async (apiKey) => {
51
+ const data = await getJson(`${API_BASE}/api/v1/server/site`, apiKey);
52
+ if (!data || typeof data.slug !== "string" || typeof data.subscribeUrl !== "string") return null;
53
+ return { slug: data.slug, name: typeof data.name === "string" ? data.name : data.slug, subscribeUrl: data.subscribeUrl };
54
+ };
55
+ var activeSubscriberCount = async (apiKey) => {
56
+ const data = await getJson(`${API_BASE}/api/v1/server/subscribers/count`, apiKey);
57
+ return data && typeof data.active === "number" ? data.active : 0;
58
+ };
59
+ var sendTestNotification = async (apiKey) => {
60
+ const res = await fetch(`${API_BASE}/api/v1/server/send`, {
61
+ method: "POST",
62
+ headers: authHeaders(apiKey),
63
+ body: JSON.stringify({
64
+ title: "Pushary is connected",
65
+ body: "Your AI agent can now reach you right here.",
66
+ metadata: { source: "cli-onboarding" }
67
+ }),
68
+ signal: AbortSignal.timeout(15e3)
69
+ });
70
+ const data = await res.json().catch(() => ({}));
71
+ if (!res.ok) throw new Error(typeof data.error === "string" ? data.error : res.statusText);
72
+ return Array.isArray(data.notificationIds) ? data.notificationIds : [];
73
+ };
74
+ var isDelivered = async (apiKey, id) => {
75
+ const data = await getJson(`${API_BASE}/api/v1/server/notifications/${id}`, apiKey);
76
+ return !!data && (data.status === "delivered" || data.deliveredAt != null);
77
+ };
78
+ var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
79
+ var startSpinner = (getLabel) => {
80
+ const frames = [" ", ". ", ".. ", "..."];
81
+ let i = 0;
82
+ const interval = setInterval(() => {
83
+ process.stdout.write(`\r ${dim(frames[i++ % frames.length])} ${getLabel()}`);
84
+ }, 200);
85
+ return (finalGlyph, finalLabel) => {
86
+ clearInterval(interval);
87
+ process.stdout.write(`\r ${finalGlyph} ${finalLabel}\x1B[K
88
+ `);
89
+ };
90
+ };
91
+ var printQr = (url) => new Promise((resolve) => {
92
+ qrcodeTerminal.generate(url, { small: true }, (qr) => {
93
+ process.stdout.write("\n" + qr.split("\n").map((line) => " " + line).join("\n") + "\n");
94
+ resolve();
95
+ });
96
+ });
97
+ var waitForDevice = async (apiKey) => {
98
+ const deadline = Date.now() + CONNECT_TIMEOUT_MS;
99
+ const stop = startSpinner(() => {
100
+ const left = Math.max(0, Math.ceil((deadline - Date.now()) / 1e3));
101
+ return `Waiting for your phone ${dim(`(${left}s)`)}`;
102
+ });
103
+ try {
104
+ while (Date.now() < deadline) {
105
+ if (await activeSubscriberCount(apiKey) > 0) {
106
+ stop(check, "Phone connected");
107
+ return true;
108
+ }
109
+ await sleep(CONNECT_POLL_INTERVAL_MS);
110
+ }
111
+ stop(yellow("!"), "Didn't detect a connection yet");
112
+ return false;
113
+ } catch (err) {
114
+ stop(yellow("!"), `Couldn't check connection ${dim(`(${err instanceof Error ? err.message : "network error"})`)}`);
115
+ return false;
116
+ }
117
+ };
118
+ var sendAndConfirm = async (apiKey) => {
119
+ let notificationIds = [];
120
+ const stopSend = startSpinner(() => "Sending a test notification");
121
+ try {
122
+ notificationIds = await sendTestNotification(apiKey);
123
+ stopSend(check, "Test notification sent");
124
+ } catch (err) {
125
+ stopSend(yellow("!"), `Couldn't send test notification ${dim(`(${err instanceof Error ? err.message : "error"})`)}`);
126
+ return;
127
+ }
128
+ if (notificationIds.length === 0) return;
129
+ const deadline = Date.now() + DELIVERY_TIMEOUT_MS;
130
+ const stop = startSpinner(() => "Confirming it reached your phone");
131
+ while (Date.now() < deadline) {
132
+ const results = await Promise.all(notificationIds.map((id) => isDelivered(apiKey, id)));
133
+ if (results.some(Boolean)) {
134
+ stop(check, "Received on your phone");
135
+ return;
136
+ }
137
+ await sleep(DELIVERY_POLL_INTERVAL_MS);
138
+ }
139
+ stop(yellow("!"), "Sent \u2014 couldn't confirm receipt; check your phone");
140
+ };
141
+ var connectDevice = async (apiKey) => {
142
+ const site = await fetchSite(apiKey);
143
+ if (!site) {
144
+ console.log(` ${yellow("!")} Couldn't load your subscribe link. Connect your phone later at ${cyan("pushary.com")}.`);
145
+ return false;
146
+ }
147
+ if (await activeSubscriberCount(apiKey) > 0) {
148
+ console.log(` ${check} Phone already connected`);
149
+ await sendAndConfirm(apiKey);
150
+ return true;
151
+ }
152
+ console.log();
153
+ console.log(` ${bold("Connect your phone")}`);
154
+ console.log(` ${dim("Scan this with your phone camera to get push notifications:")}`);
155
+ await printQr(site.subscribeUrl);
156
+ console.log(` ${dim("or open")} ${cyan(site.subscribeUrl)}`);
157
+ console.log();
158
+ const connected = await waitForDevice(apiKey);
159
+ if (!connected) {
160
+ console.log(` ${dim("No rush \u2014 open")} ${cyan(site.subscribeUrl)} ${dim("on your phone whenever you're ready.")}`);
161
+ return false;
162
+ }
163
+ await sendAndConfirm(apiKey);
164
+ return true;
165
+ };
166
+ var printConnectInstructions = async (apiKey) => {
167
+ const site = await fetchSite(apiKey);
168
+ const target = site?.subscribeUrl ?? "https://pushary.com";
169
+ console.log(` ${dim("Connect your phone for push notifications at")} ${cyan(target)}`);
170
+ };
171
+
172
+ // bin/pushary-setup.ts
23
173
  var CLAUDE_SETTINGS = join(homedir(), ".claude", "settings.json");
24
174
  var CLAUDE_JSON = join(homedir(), ".claude.json");
25
175
  var CURSOR_MCP = join(".cursor", "mcp.json");
@@ -27,12 +177,12 @@ var CURSOR_RULES_DIR = join(".cursor", "rules");
27
177
  var CLAUDE_SKILL_DIR = join(homedir(), ".claude", "skills", "pushary");
28
178
  var CODEX_SKILL_DIR = join(homedir(), ".codex", "skills", "pushary");
29
179
  var SHELL_FILES = [".zshrc", ".zprofile", ".bashrc", ".bash_profile"].map((f) => join(homedir(), f));
30
- var dim = (s) => `\x1B[2m${s}\x1B[0m`;
31
- var bold = (s) => `\x1B[1m${s}\x1B[0m`;
32
- var green = (s) => `\x1B[32m${s}\x1B[0m`;
33
- var cyan = (s) => `\x1B[36m${s}\x1B[0m`;
34
- var yellow = (s) => `\x1B[33m${s}\x1B[0m`;
35
- var check = green("\u2713");
180
+ var dim2 = (s) => `\x1B[2m${s}\x1B[0m`;
181
+ var bold2 = (s) => `\x1B[1m${s}\x1B[0m`;
182
+ var green2 = (s) => `\x1B[32m${s}\x1B[0m`;
183
+ var cyan2 = (s) => `\x1B[36m${s}\x1B[0m`;
184
+ var yellow2 = (s) => `\x1B[33m${s}\x1B[0m`;
185
+ var check2 = green2("\u2713");
36
186
  var parseKeyFlag = () => {
37
187
  const args = process.argv.slice(2);
38
188
  for (let i = 0; i < args.length; i++) {
@@ -76,16 +226,16 @@ var spinner = async (label, fn, options = {}) => {
76
226
  const frames = [" ", ". ", ".. ", "..."];
77
227
  let i = 0;
78
228
  const interval = setInterval(() => {
79
- process.stdout.write(`\r ${dim(frames[i++ % frames.length])} ${label}`);
229
+ process.stdout.write(`\r ${dim2(frames[i++ % frames.length])} ${label}`);
80
230
  }, 200);
81
231
  try {
82
232
  await fn();
83
233
  clearInterval(interval);
84
- process.stdout.write(`\r ${check} ${label}
234
+ process.stdout.write(`\r ${check2} ${label}
85
235
  `);
86
236
  } catch (err) {
87
237
  clearInterval(interval);
88
- process.stdout.write(`\r ${yellow("!")} ${label} ${dim(`(${formatError(err)})`)}
238
+ process.stdout.write(`\r ${yellow2("!")} ${label} ${dim2(`(${formatError(err)})`)}
89
239
  `);
90
240
  if (!options.optional) throw err;
91
241
  }
@@ -107,8 +257,8 @@ var checkForUpdates = async (current) => {
107
257
  const data = await res.json();
108
258
  const latest = data.version;
109
259
  if (latest && latest !== current) {
110
- console.log(` ${yellow("!")} Update available: ${dim(current)} \u2192 ${green(latest)}`);
111
- console.log(` ${dim("Run:")} npx @pushary/agent-hooks@${latest} setup`);
260
+ console.log(` ${yellow2("!")} Update available: ${dim2(current)} \u2192 ${green2(latest)}`);
261
+ console.log(` ${dim2("Run:")} npx @pushary/agent-hooks@${latest} setup`);
112
262
  console.log();
113
263
  }
114
264
  } catch {
@@ -158,7 +308,7 @@ ${body}`;
158
308
  };
159
309
  var setupClaudeCode = async (apiKey) => {
160
310
  console.log(`
161
- ${bold("Setting up Claude Code")}
311
+ ${bold2("Setting up Claude Code")}
162
312
  `);
163
313
  const settings = readJson(CLAUDE_SETTINGS);
164
314
  await spinner("Adding MCP server (type: http)", async () => {
@@ -184,11 +334,11 @@ var setupClaudeCode = async (apiKey) => {
184
334
  });
185
335
  await installSkillToDir(CLAUDE_SKILL_DIR, "Installing Pushary skill");
186
336
  console.log();
187
- console.log(` ${dim("What this configured:")}`);
188
- console.log(` ${dim("\u2022")} MCP server: your agent can send notifications and ask questions`);
189
- console.log(` ${dim("\u2022")} Skill: teaches your agent when and how to use Pushary`);
190
- console.log(` ${dim("\u2022")} Hooks: route permission approvals through push notifications`);
191
- console.log(` ${dim("\u2022")} Auto-allowed tools: no permission prompts for Pushary MCP calls`);
337
+ console.log(` ${dim2("What this configured:")}`);
338
+ console.log(` ${dim2("\u2022")} MCP server: your agent can send notifications and ask questions`);
339
+ console.log(` ${dim2("\u2022")} Skill: teaches your agent when and how to use Pushary`);
340
+ console.log(` ${dim2("\u2022")} Hooks: route permission approvals through push notifications`);
341
+ console.log(` ${dim2("\u2022")} Auto-allowed tools: no permission prompts for Pushary MCP calls`);
192
342
  };
193
343
  var findPython310Plus = () => {
194
344
  const candidates = ["python3.13", "python3.12", "python3.11", "python3.10", "python3", "python"];
@@ -218,11 +368,11 @@ var installPythonPlugin = (pythonBin) => {
218
368
  };
219
369
  var setupHermes = async (_apiKey) => {
220
370
  console.log(`
221
- ${bold("Setting up Hermes Agent")}
371
+ ${bold2("Setting up Hermes Agent")}
222
372
  `);
223
373
  if (!isInstalled("hermes")) {
224
- console.log(` ${yellow("!")} Hermes CLI not found. Skipping.`);
225
- console.log(` ${dim("Install Hermes and re-run setup to configure.")}`);
374
+ console.log(` ${yellow2("!")} Hermes CLI not found. Skipping.`);
375
+ console.log(` ${dim2("Install Hermes and re-run setup to configure.")}`);
226
376
  return;
227
377
  }
228
378
  await spinner("Installing hermes-plugin-pushary", async () => {
@@ -248,14 +398,14 @@ var setupHermes = async (_apiKey) => {
248
398
  } catch {
249
399
  }
250
400
  } else if (process.platform === "linux") {
251
- for (const [check2, install] of [
401
+ for (const [check3, install] of [
252
402
  ["which apt-get", "sudo apt-get update -qq && sudo apt-get install -y -qq python3 python3-pip"],
253
403
  ["which dnf", "sudo dnf install -y -q python3 python3-pip"],
254
404
  ["which yum", "sudo yum install -y -q python3 python3-pip"],
255
405
  ["which pacman", "sudo pacman -S --noconfirm python python-pip"]
256
406
  ]) {
257
407
  try {
258
- execSync(check2, { stdio: "ignore", timeout: 5e3 });
408
+ execSync(check3, { stdio: "ignore", timeout: 5e3 });
259
409
  execSync(install, { stdio: "pipe", timeout: 3e5 });
260
410
  python = findPython310Plus();
261
411
  if (python) break;
@@ -288,17 +438,17 @@ var setupHermes = async (_apiKey) => {
288
438
  execSync("hermes plugins enable pushary", { stdio: "ignore", timeout: 1e4 });
289
439
  });
290
440
  console.log();
291
- console.log(` ${dim("What this configured:")}`);
292
- console.log(` ${dim("\u2022")} Native tools: pushary_notify, pushary_ask, pushary_wait, pushary_cancel`);
293
- console.log(` ${dim("\u2022")} Auto-notifications: push alert when tools return errors`);
441
+ console.log(` ${dim2("What this configured:")}`);
442
+ console.log(` ${dim2("\u2022")} Native tools: pushary_notify, pushary_ask, pushary_wait, pushary_cancel`);
443
+ console.log(` ${dim2("\u2022")} Auto-notifications: push alert when tools return errors`);
294
444
  };
295
445
  var setupCodex = async (_apiKey) => {
296
446
  console.log(`
297
- ${bold("Setting up Codex")}
447
+ ${bold2("Setting up Codex")}
298
448
  `);
299
449
  if (!isInstalled("codex")) {
300
- console.log(` ${yellow("!")} Codex CLI not found. Skipping.`);
301
- console.log(` ${dim("Install Codex and re-run setup to configure.")}`);
450
+ console.log(` ${yellow2("!")} Codex CLI not found. Skipping.`);
451
+ console.log(` ${dim2("Install Codex and re-run setup to configure.")}`);
302
452
  return;
303
453
  }
304
454
  await installGlobally();
@@ -346,14 +496,14 @@ var setupCodex = async (_apiKey) => {
346
496
  });
347
497
  await installSkillToDir(CODEX_SKILL_DIR, "Installing Pushary skill");
348
498
  console.log();
349
- console.log(` ${dim("What this configured:")}`);
350
- console.log(` ${dim("\u2022")} MCP server: Codex can send notifications and ask questions`);
351
- console.log(` ${dim("\u2022")} Auto-allowed tools: no permission prompts for Pushary MCP calls`);
352
- console.log(` ${dim("\u2022")} Notify handler: captures turn completions and approval requests`);
499
+ console.log(` ${dim2("What this configured:")}`);
500
+ console.log(` ${dim2("\u2022")} MCP server: Codex can send notifications and ask questions`);
501
+ console.log(` ${dim2("\u2022")} Auto-allowed tools: no permission prompts for Pushary MCP calls`);
502
+ console.log(` ${dim2("\u2022")} Notify handler: captures turn completions and approval requests`);
353
503
  };
354
504
  var setupCursor = async (apiKey) => {
355
505
  console.log(`
356
- ${bold("Setting up Cursor")}
506
+ ${bold2("Setting up Cursor")}
357
507
  `);
358
508
  await spinner("Adding MCP server to .cursor/mcp.json", async () => {
359
509
  const config = readJson(CURSOR_MCP);
@@ -383,43 +533,6 @@ export PUSHARY_API_KEY='${apiKey}'
383
533
  process.env.PUSHARY_API_KEY = apiKey;
384
534
  });
385
535
  };
386
- var sendTestNotification = async (apiKey) => {
387
- const frames = [" ", ". ", ".. ", "..."];
388
- let i = 0;
389
- const interval = setInterval(() => {
390
- process.stdout.write(`\r ${dim(frames[i++ % frames.length])} Sending test notification`);
391
- }, 200);
392
- try {
393
- const response = await fetch("https://pushary.com/api/v1/server/send", {
394
- method: "POST",
395
- headers: {
396
- "Content-Type": "application/json",
397
- "Authorization": `Bearer ${apiKey}`
398
- },
399
- body: JSON.stringify({
400
- title: "Pushary is working",
401
- body: "Your AI agent can now send you push notifications."
402
- })
403
- });
404
- const data = await response.json().catch(() => ({}));
405
- clearInterval(interval);
406
- if (!response.ok) {
407
- const reason = data.error ?? response.statusText;
408
- process.stdout.write(`\r ${yellow("!")} Sending test notification ${dim(`(${reason})`)}
409
- `);
410
- console.log(` ${dim("Make sure you enabled notifications at")} ${cyan("pushary.com")}`);
411
- } else {
412
- process.stdout.write(`\r ${check} Sending test notification
413
- `);
414
- console.log(` ${dim("Check your phone!")}`);
415
- }
416
- } catch (err) {
417
- clearInterval(interval);
418
- const msg = err instanceof Error ? err.message : "network error";
419
- process.stdout.write(`\r ${yellow("!")} Sending test notification ${dim(`(${msg})`)}
420
- `);
421
- }
422
- };
423
536
  var AGENT_SETUP = {
424
537
  claude_code: setupClaudeCode,
425
538
  codex: setupCodex,
@@ -429,8 +542,8 @@ var AGENT_SETUP = {
429
542
  var main = async () => {
430
543
  const version = getPackageVersion();
431
544
  console.log();
432
- console.log(` ${bold("Pushary")} ${dim("v" + version)}`);
433
- console.log(` ${dim("Push notifications for AI coding agents")}`);
545
+ console.log(` ${bold2("Pushary")} ${dim2("v" + version)}`);
546
+ console.log(` ${dim2("Push notifications for AI coding agents")}`);
434
547
  console.log();
435
548
  await checkForUpdates(version);
436
549
  const flagKey = parseKeyFlag();
@@ -438,12 +551,12 @@ var main = async () => {
438
551
  let trimmedKey;
439
552
  if (flagKey && isValidApiKey(flagKey)) {
440
553
  const masked = `${flagKey.slice(0, 8)}...${flagKey.slice(-4)}`;
441
- console.log(` ${check} Using API key: ${dim(masked)}`);
554
+ console.log(` ${check2} Using API key: ${dim2(masked)}`);
442
555
  console.log();
443
556
  trimmedKey = flagKey;
444
557
  } else if (envKey && isValidApiKey(envKey)) {
445
558
  const masked = `${envKey.slice(0, 8)}...${envKey.slice(-4)}`;
446
- console.log(` ${check} Found API key in environment: ${dim(masked)}`);
559
+ console.log(` ${check2} Found API key in environment: ${dim2(masked)}`);
447
560
  console.log();
448
561
  const useExisting = await confirm({ message: "Use this key?", default: true });
449
562
  if (useExisting) {
@@ -453,17 +566,17 @@ var main = async () => {
453
566
  trimmedKey = apiKey.trim();
454
567
  }
455
568
  } else {
456
- console.log(` ${dim("Paste your API key from the onboarding page.")}`);
457
- console.log(` ${dim("Can't find it? Copy it from:")} ${cyan("pushary.com/dashboard/agent/settings")}`);
569
+ console.log(` ${dim2("Paste your API key from the onboarding page.")}`);
570
+ console.log(` ${dim2("Can't find it? Copy it from:")} ${cyan2("pushary.com/dashboard/agent/settings")}`);
458
571
  console.log();
459
572
  const apiKey = await input({ message: "API key:" });
460
573
  trimmedKey = apiKey.trim();
461
574
  }
462
575
  if (!trimmedKey || !isValidApiKey(trimmedKey)) {
463
576
  console.log(`
464
- ${yellow("!")} Invalid key format. Expected: ${dim("pk_xxx.xxx")}`);
465
- console.log(` ${dim("Copy your key from")} ${cyan("https://pushary.com/dashboard/agent/settings")}`);
466
- console.log(` ${dim("Or sign up at")} ${cyan("https://pushary.com/sign-up?from=ai-coding")}
577
+ ${yellow2("!")} Invalid key format. Expected: ${dim2("pk_xxx.xxx")}`);
578
+ console.log(` ${dim2("Copy your key from")} ${cyan2("https://pushary.com/dashboard/agent/settings")}`);
579
+ console.log(` ${dim2("Or sign up at")} ${cyan2("https://pushary.com/sign-up?from=ai-coding")}
467
580
  `);
468
581
  process.exit(1);
469
582
  }
@@ -475,18 +588,18 @@ var main = async () => {
475
588
  };
476
589
  const hint = Object.values(detected).some(Boolean) ? "(detected agents pre-selected)" : "(space = toggle, enter = confirm)";
477
590
  const agents = await checkbox({
478
- message: "Which agents do you use? " + dim(hint),
591
+ message: "Which agents do you use? " + dim2(hint),
479
592
  choices: [
480
- { name: `Claude Code ${dim("MCP + hooks + auto-allowed tools")}`, value: "claude_code", checked: detected.claude_code },
481
- { name: `Codex ${dim("MCP + notify handler + auto-allowed tools")}`, value: "codex", checked: detected.codex },
482
- { name: `Hermes ${dim("native plugin + auto-error notifications")}`, value: "hermes", checked: detected.hermes },
483
- { name: `Cursor ${dim("MCP server")}`, value: "cursor", checked: detected.cursor }
593
+ { name: `Claude Code ${dim2("MCP + hooks + auto-allowed tools")}`, value: "claude_code", checked: detected.claude_code },
594
+ { name: `Codex ${dim2("MCP + notify handler + auto-allowed tools")}`, value: "codex", checked: detected.codex },
595
+ { name: `Hermes ${dim2("native plugin + auto-error notifications")}`, value: "hermes", checked: detected.hermes },
596
+ { name: `Cursor ${dim2("MCP server")}`, value: "cursor", checked: detected.cursor }
484
597
  ]
485
598
  });
486
599
  await saveApiKey(trimmedKey);
487
600
  if (agents.length === 0) {
488
601
  console.log(`
489
- ${dim("No agents selected. API key saved.")}`);
602
+ ${dim2("No agents selected. API key saved.")}`);
490
603
  } else {
491
604
  const failed = [];
492
605
  for (const agent of agents) {
@@ -494,33 +607,34 @@ var main = async () => {
494
607
  await AGENT_SETUP[agent](trimmedKey);
495
608
  } catch (err) {
496
609
  failed.push(agent.replace("_", " "));
497
- console.log(` ${yellow("!")} ${agent.replace("_", " ")} setup failed: ${formatError(err)}`);
498
- console.log(` ${dim("Other agents will continue. Re-run setup to retry.")}`);
610
+ console.log(` ${yellow2("!")} ${agent.replace("_", " ")} setup failed: ${formatError(err)}`);
611
+ console.log(` ${dim2("Other agents will continue. Re-run setup to retry.")}`);
499
612
  }
500
613
  }
501
614
  if (failed.length > 0) {
502
615
  console.log();
503
- console.log(` ${yellow("!")} Failed: ${failed.join(", ")} ${dim("(others completed successfully)")}`);
616
+ console.log(` ${yellow2("!")} Failed: ${failed.join(", ")} ${dim2("(others completed successfully)")}`);
504
617
  }
505
618
  }
506
- const sendTest = await confirm({ message: "Send a test notification?", default: true });
507
- if (sendTest) {
508
- await sendTestNotification(trimmedKey);
619
+ const skipPhone = process.argv.includes("--skip-phone");
620
+ if (skipPhone || !process.stdout.isTTY) {
621
+ await printConnectInstructions(trimmedKey);
622
+ } else {
623
+ await connectDevice(trimmedKey);
509
624
  }
510
625
  console.log();
511
- console.log(` ${green(bold("Setup complete."))}`);
626
+ console.log(` ${green2(bold2("Setup complete."))}`);
512
627
  console.log();
513
- console.log(` ${dim("Next:")}`);
514
- console.log(` ${dim("1.")} Load your API key: ${cyan("source ~/.zshrc")} ${dim("(or open a new terminal)")}`);
515
- console.log(` ${dim("2.")} Enable notifications on your phone at ${cyan("pushary.com")}`);
516
- console.log(` ${dim("3.")} Restart your agent to load the new config`);
517
- console.log(` ${dim("4.")} Run ${cyan("npx @pushary/agent-hooks doctor")} to verify`);
628
+ console.log(` ${dim2("Next:")}`);
629
+ console.log(` ${dim2("1.")} Load your API key: ${cyan2("source ~/.zshrc")} ${dim2("(or open a new terminal)")}`);
630
+ console.log(` ${dim2("2.")} Restart your agent to load the new config`);
631
+ console.log(` ${dim2("3.")} Run ${cyan2("npx @pushary/agent-hooks doctor")} to verify`);
518
632
  console.log();
519
633
  };
520
634
  main().catch((err) => {
521
635
  console.log();
522
- console.log(` ${yellow("!")} Setup failed: ${formatError(err)}`);
523
- console.log(` ${dim("Run")} ${cyan("npx @pushary/agent-hooks doctor")} ${dim("after fixing the issue, then rerun setup.")}`);
636
+ console.log(` ${yellow2("!")} Setup failed: ${formatError(err)}`);
637
+ console.log(` ${dim2("Run")} ${cyan2("npx @pushary/agent-hooks doctor")} ${dim2("after fixing the issue, then rerun setup.")}`);
524
638
  console.log();
525
639
  process.exit(1);
526
640
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pushary/agent-hooks",
3
- "version": "0.9.1",
3
+ "version": "0.10.0",
4
4
  "description": "Permission hooks for AI coding agents: route tool approvals through Pushary push notifications",
5
5
  "author": "Pushary <business@pushary.com>",
6
6
  "homepage": "https://pushary.com",
@@ -36,6 +36,7 @@
36
36
  },
37
37
  "dependencies": {
38
38
  "@inquirer/prompts": "^8.4.2",
39
+ "qrcode-terminal": "^0.12.0",
39
40
  "smol-toml": "^1.6.1"
40
41
  },
41
42
  "devDependencies": {