@pushary/agent-hooks 0.60.0 → 0.62.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 (57) hide show
  1. package/CHANGELOG.md +161 -0
  2. package/README.md +52 -9
  3. package/dist/bin/pushary-claude.js +6 -6
  4. package/dist/bin/pushary-clean.js +83 -38
  5. package/dist/bin/pushary-codex-hook.js +6 -6
  6. package/dist/bin/pushary-codex.js +4 -4
  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 +42 -5
  10. package/dist/bin/pushary-doctor.js +246 -71
  11. package/dist/bin/pushary-gemini-hook.js +6 -6
  12. package/dist/bin/pushary-hook.js +10 -5
  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 +144 -0
  17. package/dist/bin/pushary-mode.js +24 -13
  18. package/dist/bin/pushary-notification-hook.js +4 -4
  19. package/dist/bin/pushary-permission-denied-hook.js +5 -5
  20. package/dist/bin/pushary-permission-hook.js +5 -5
  21. package/dist/bin/pushary-post-hook.js +4 -4
  22. package/dist/bin/pushary-prompt-hook.js +4 -4
  23. package/dist/bin/pushary-session-end-hook.js +4 -4
  24. package/dist/bin/pushary-session-start-hook.js +4 -4
  25. package/dist/bin/pushary-setup.js +769 -518
  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 +4 -4
  30. package/dist/bin/pushary-stopfailure-hook.js +4 -4
  31. package/dist/bin/pushary-suggestions.js +10 -5
  32. package/dist/bin/pushary-upgrade.js +184 -14
  33. package/dist/bin/pushary-wait.js +21 -12
  34. package/dist/bin/pushary.js +42 -60
  35. package/dist/chunk-247C4RE5.js +236 -0
  36. package/dist/{chunk-NKXSILEW.js → chunk-2UMNXADU.js} +18 -2
  37. package/dist/chunk-3EGEA4KH.js +44 -0
  38. package/dist/chunk-3O27ZSRE.js +569 -0
  39. package/dist/{chunk-BE5X3WXL.js → chunk-5SO3CEGJ.js} +5 -5
  40. package/dist/chunk-7QLSKOSU.js +19 -0
  41. package/dist/chunk-E2U35RLD.js +108 -0
  42. package/dist/chunk-ER657KRJ.js +168 -0
  43. package/dist/{chunk-WNRYRN2R.js → chunk-HIIBSYFJ.js} +5 -5
  44. package/dist/{chunk-DWED7BS3.js → chunk-HNTKTK5B.js} +1 -1
  45. package/dist/chunk-IIZZYUWK.js +18 -0
  46. package/dist/{chunk-J7JWI3KU.js → chunk-MUEW424A.js} +19 -1
  47. package/dist/chunk-N4DA4CJB.js +57 -0
  48. package/dist/chunk-N7XJ4L2W.js +79 -0
  49. package/dist/chunk-ONVEYEVR.js +44 -0
  50. package/dist/chunk-QHOVJXOT.js +255 -0
  51. package/dist/chunk-SP7OZXCQ.js +224 -0
  52. package/dist/chunk-TX7KBKT7.js +79 -0
  53. package/dist/chunk-UXXRRXUS.js +93 -0
  54. package/dist/chunk-YJ7YZRVV.js +217 -0
  55. package/dist/src/index.js +5 -5
  56. package/package.json +11 -4
  57. package/dist/chunk-BQLCELYC.js +0 -332
@@ -4,14 +4,20 @@ import {
4
4
  } from "../chunk-XHKBHWLX.js";
5
5
  import {
6
6
  resolveGlobalBinary
7
- } from "../chunk-J7JWI3KU.js";
7
+ } from "../chunk-MUEW424A.js";
8
+ import {
9
+ exitOnHelpFlag
10
+ } from "../chunk-IIZZYUWK.js";
11
+ import {
12
+ EXIT
13
+ } from "../chunk-SP7OZXCQ.js";
8
14
  import {
9
15
  getMachineId
10
16
  } from "../chunk-RN3NOEJF.js";
11
17
  import {
12
18
  getApiKey,
13
19
  getBaseUrl
14
- } from "../chunk-NKXSILEW.js";
20
+ } from "../chunk-2UMNXADU.js";
15
21
 
16
22
  // src/spawn-daemon.ts
17
23
  import { spawn } from "child_process";
@@ -26,6 +32,19 @@ var MAX_ERROR_STREAK = 6;
26
32
  var REQUEST_TIMEOUT_MS = 1e4;
27
33
  var MAX_SPAWNS_PER_WINDOW = 5;
28
34
  var SPAWN_WINDOW_MS = 6e4;
35
+ var DrainError = class extends Error {
36
+ constructor(status) {
37
+ super(`drain ${status}`);
38
+ this.status = status;
39
+ this.name = "DrainError";
40
+ }
41
+ };
42
+ var isAuthFailure = (err) => {
43
+ if (typeof err !== "object" || err === null) return false;
44
+ const candidate = err;
45
+ if (candidate.name !== "DrainError") return false;
46
+ return candidate.status === 401 || candidate.status === 403;
47
+ };
29
48
  var NATIVE_AGENT_BINARIES = { codex: "codex", gemini: "gemini" };
30
49
  var buildSpawnLaunch = (req, opts) => {
31
50
  const agent = req.agent === "codex" || req.agent === "gemini" ? req.agent : "claude";
@@ -64,6 +83,9 @@ var runSpawnDaemon = async () => {
64
83
  const spawnTimestamps = [];
65
84
  let timer;
66
85
  let inflight;
86
+ let fatalCode = null;
87
+ let finish = () => {
88
+ };
67
89
  const launch = (req) => {
68
90
  spawnTimestamps.push(Date.now());
69
91
  const cwd = req.cwd && existsSync(req.cwd) ? req.cwd : process.cwd();
@@ -94,7 +116,7 @@ var runSpawnDaemon = async () => {
94
116
  body: JSON.stringify({ machineId }),
95
117
  signal
96
118
  });
97
- if (!res.ok) throw new Error(`drain ${res.status}`);
119
+ if (!res.ok) throw new DrainError(res.status);
98
120
  const data = await res.json();
99
121
  const spawn2 = data.spawn;
100
122
  return spawn2 && typeof spawn2.prompt === "string" && typeof spawn2.id === "string" ? spawn2 : null;
@@ -122,7 +144,16 @@ var runSpawnDaemon = async () => {
122
144
  } else {
123
145
  idleTicks++;
124
146
  }
125
- } catch {
147
+ } catch (err) {
148
+ if (isAuthFailure(err)) {
149
+ const status = err.status;
150
+ fatalCode = EXIT.UNAUTHENTICATED;
151
+ stderr(
152
+ status === 401 ? "[pushary] daemon stopping: this machine's key was rejected. Run `pushary login` to sign in again.\n" : "[pushary] daemon stopping: this workspace has no active plan, so the queue is closed. Pick one at pushary.com/dashboard/settings?tab=billing\n"
153
+ );
154
+ finish();
155
+ return;
156
+ }
126
157
  errorStreak = Math.min(errorStreak + 1, MAX_ERROR_STREAK);
127
158
  } finally {
128
159
  clearTimeout(timeout);
@@ -148,11 +179,17 @@ var runSpawnDaemon = async () => {
148
179
  resolve();
149
180
  };
150
181
  for (const signal of signals) process.on(signal, onSignal);
182
+ finish = () => {
183
+ teardown();
184
+ for (const signal of signals) process.off(signal, onSignal);
185
+ resolve();
186
+ };
151
187
  });
152
- return 0;
188
+ return fatalCode ?? 0;
153
189
  };
154
190
 
155
191
  // bin/pushary-daemon.ts
192
+ exitOnHelpFlag("daemon");
156
193
  runSpawnDaemon().then((code) => process.exit(code)).catch((err) => {
157
194
  process.stderr.write(
158
195
  `[pushary] daemon error: ${err instanceof Error ? err.message : String(err)}
@@ -2,50 +2,162 @@
2
2
  import {
3
3
  describeWaitLadder
4
4
  } from "../chunk-IIENZE2J.js";
5
+ import {
6
+ detectAllAgents
7
+ } from "../chunk-UXXRRXUS.js";
8
+ import {
9
+ hasInstructionBlock
10
+ } from "../chunk-N7XJ4L2W.js";
5
11
  import {
6
12
  GEMINI_HOOK_BINARY,
7
- hasCodexHooks,
8
13
  hasGeminiHooks,
9
- hasInstructionBlock,
14
+ missingGeminiHookEvents
15
+ } from "../chunk-E2U35RLD.js";
16
+ import {
17
+ codexAgentsMd,
18
+ codexConfigToml,
19
+ codexHooksJson,
20
+ codexSkillDir,
21
+ hasCodexHooks,
10
22
  missingCodexHookEvents,
11
- missingGeminiHookEvents,
12
23
  readCodexMcpAuth,
13
24
  untrustedCodexHookEvents
14
- } from "../chunk-BQLCELYC.js";
25
+ } from "../chunk-ER657KRJ.js";
26
+ import {
27
+ describeReach,
28
+ fetchChannels,
29
+ reachVerdict
30
+ } from "../chunk-3O27ZSRE.js";
31
+ import "../chunk-3EGEA4KH.js";
32
+ import {
33
+ parseFlags
34
+ } from "../chunk-N4DA4CJB.js";
35
+ import {
36
+ keyPrefix,
37
+ readKeySource
38
+ } from "../chunk-247C4RE5.js";
39
+ import {
40
+ checkApiKey,
41
+ describeKeyCheck
42
+ } from "../chunk-YJ7YZRVV.js";
43
+ import {
44
+ createIo
45
+ } from "../chunk-TX7KBKT7.js";
46
+ import {
47
+ UsageError
48
+ } from "../chunk-7QLSKOSU.js";
49
+ import "../chunk-BC3VCZ3E.js";
15
50
  import {
16
51
  execNpm
17
- } from "../chunk-J7JWI3KU.js";
52
+ } from "../chunk-MUEW424A.js";
53
+ import {
54
+ exitOnHelpFlag
55
+ } from "../chunk-IIZZYUWK.js";
56
+ import {
57
+ EXIT,
58
+ getPackageVersion
59
+ } from "../chunk-SP7OZXCQ.js";
18
60
  import {
19
61
  readLedgerSummary
20
62
  } from "../chunk-WLGEY4UX.js";
21
63
  import {
22
64
  callMcpTool,
23
65
  sendMcpRequest
24
- } from "../chunk-DWED7BS3.js";
66
+ } from "../chunk-HNTKTK5B.js";
25
67
  import "../chunk-DQAN3JQP.js";
68
+ import {
69
+ getMachineId
70
+ } from "../chunk-RN3NOEJF.js";
26
71
  import {
27
72
  getBaseUrl
28
- } from "../chunk-NKXSILEW.js";
73
+ } from "../chunk-2UMNXADU.js";
29
74
 
30
75
  // bin/pushary-doctor.ts
31
- import { existsSync, readFileSync } from "fs";
76
+ import { existsSync, readFileSync, writeFileSync } from "fs";
32
77
  import { join } from "path";
33
- import { homedir } from "os";
78
+ import { homedir as homedir2 } from "os";
34
79
  import { execSync } from "child_process";
35
80
  import { confirm } from "@inquirer/prompts";
36
81
  import { parse as parseTOML } from "smol-toml";
37
- var dim = (s) => `\x1B[2m${s}\x1B[0m`;
38
- var bold = (s) => `\x1B[1m${s}\x1B[0m`;
39
- var green = (s) => `\x1B[32m${s}\x1B[0m`;
40
- var red = (s) => `\x1B[31m${s}\x1B[0m`;
41
- var yellow = (s) => `\x1B[33m${s}\x1B[0m`;
42
- var cyan = (s) => `\x1B[36m${s}\x1B[0m`;
82
+
83
+ // src/diagnostics/bundle.ts
84
+ import { homedir, hostname } from "os";
85
+ var globalVersion = () => {
86
+ try {
87
+ return execNpm("list -g --no-workspaces @pushary/agent-hooks --depth=0", { timeout: 1e4 }).toString().match(/@pushary\/agent-hooks@([\d.]+)/)?.[1] ?? null;
88
+ } catch {
89
+ return null;
90
+ }
91
+ };
92
+ var collectBundle = async (options2 = {}) => {
93
+ const now = options2.now ?? (() => /* @__PURE__ */ new Date());
94
+ const source = readKeySource();
95
+ const identity = options2.identity ?? null;
96
+ return {
97
+ generatedAt: now().toISOString(),
98
+ cliVersion: getPackageVersion(),
99
+ globalVersion: globalVersion(),
100
+ node: process.version,
101
+ platform: process.platform,
102
+ arch: process.arch,
103
+ shell: process.env.SHELL ?? null,
104
+ machineId: getMachineId(),
105
+ keySource: source.kind,
106
+ keyPrefix: source.key ? keyPrefix(source.key) : null,
107
+ // The value of the drift, not the keys: knowing two places disagree is the
108
+ // whole diagnostic, and the keys themselves are what must not travel.
109
+ keyDrift: source.drift !== null,
110
+ identity: identity ? stripWorkspace(identity) : null,
111
+ agents: detectAllAgents().map((entry) => ({ id: entry.agent, detected: entry.result })),
112
+ checks: options2.checks ?? []
113
+ };
114
+ };
115
+ var stripWorkspace = (identity) => {
116
+ const { workspaceId: _workspaceId, ...rest } = identity;
117
+ return rest;
118
+ };
119
+ var KEY_PATTERN = /pk_[A-Za-z0-9_]+\.[A-Za-z0-9_]+/g;
120
+ var homePattern = () => new RegExp(homedir().replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "g");
121
+ var redactText = (text) => text.replace(KEY_PATTERN, (match) => `${match.split(".")[0]}.REDACTED`).replace(homePattern(), "~").replace(new RegExp(hostname().replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "g"), "HOST");
122
+ var redact = (bundle) => JSON.parse(redactText(JSON.stringify(bundle)));
123
+ var bundleFileName = (bundle) => `pushary-diagnostics-${bundle.generatedAt.replace(/[:.]/g, "-")}.json`;
124
+
125
+ // bin/pushary-doctor.ts
126
+ exitOnHelpFlag("doctor");
127
+ var parseDoctorOptions = (argv) => {
128
+ const flags = parseFlags(argv, { bools: ["json", "roundtrip", "no-push", "bundle", "help", "h"], values: [] });
129
+ if (flags.command !== void 0 && flags.command !== "doctor") {
130
+ throw new UsageError(`doctor takes no positional arguments (got ${flags.command})`);
131
+ }
132
+ const roundtrip = flags.bools.has("roundtrip");
133
+ const noPush = flags.bools.has("no-push");
134
+ if (roundtrip && noPush) {
135
+ throw new UsageError("--roundtrip needs to send a push, so it cannot be combined with --no-push");
136
+ }
137
+ return { json: flags.bools.has("json"), roundtrip, noPush, bundle: flags.bools.has("bundle") };
138
+ };
139
+ var options;
140
+ try {
141
+ options = parseDoctorOptions(process.argv.slice(2));
142
+ } catch (err) {
143
+ process.stderr.write(` ! ${err instanceof Error ? err.message : String(err)}
144
+ `);
145
+ process.stderr.write(" Run `pushary doctor --help` for the full list.\n");
146
+ process.exit(EXIT.USAGE);
147
+ }
148
+ if (options.json) {
149
+ console.log = (...args) => {
150
+ process.stderr.write(`${args.map((a) => typeof a === "string" ? a : String(a)).join(" ")}
151
+ `);
152
+ };
153
+ }
154
+ var { bold, dim, green, yellow, red, cyan } = createIo();
43
155
  var pass = green("\u2713");
44
156
  var fail = red("\u2717");
45
157
  var warn = yellow("!");
46
- var CLAUDE_SETTINGS = join(homedir(), ".claude", "settings.json");
47
- var SKILL_PATH = join(homedir(), ".claude", "skills", "pushary", "SKILL.md");
48
- var SHELL_FILES = [".zshrc", ".zprofile", ".bashrc", ".bash_profile"].map((f) => join(homedir(), f));
158
+ var CLAUDE_SETTINGS = join(homedir2(), ".claude", "settings.json");
159
+ var SKILL_PATH = join(homedir2(), ".claude", "skills", "pushary", "SKILL.md");
160
+ var SHELL_FILES = [".zshrc", ".zprofile", ".bashrc", ".bash_profile"].map((f) => join(homedir2(), f));
49
161
  var readJson = (path) => {
50
162
  try {
51
163
  return JSON.parse(readFileSync(path, "utf-8"));
@@ -101,19 +213,9 @@ var main = async () => {
101
213
  console.log(` ${bold("Pushary Doctor")}`);
102
214
  console.log();
103
215
  console.log(` ${dim("Configuration")}`);
104
- let apiKey = process.env.PUSHARY_API_KEY;
105
- let keySource = apiKey ? "env" : null;
106
- const PUSHARY_CONFIG_FILE = join(homedir(), ".pushary", "config.json");
107
- if (!apiKey) {
108
- try {
109
- const parsed = JSON.parse(readFileSync(PUSHARY_CONFIG_FILE, "utf-8"));
110
- if (typeof parsed.apiKey === "string" && parsed.apiKey) {
111
- apiKey = parsed.apiKey;
112
- keySource = "config-file";
113
- }
114
- } catch {
115
- }
116
- }
216
+ const source = readKeySource();
217
+ let apiKey = source.key ?? void 0;
218
+ let keySource = source.kind === "env" ? "env" : source.kind === "config" ? "config-file" : null;
117
219
  if (!apiKey) {
118
220
  for (const f of SHELL_FILES) {
119
221
  try {
@@ -130,14 +232,48 @@ var main = async () => {
130
232
  }
131
233
  const maskKey = (k) => `pk_${k.split(".")[0]?.slice(3, 7)}...`;
132
234
  if (keySource === "config-file") {
133
- console.log(` ${pass} API key in ${dim("~/.pushary/config.json")} ${dim(`(${maskKey(apiKey)})`)}`);
235
+ console.log(` ${pass} API key in ${dim(source.origin ?? "the config file")} ${dim(`(${maskKey(apiKey)})`)}`);
134
236
  } else if (keySource === "profile") {
135
237
  console.log(` ${pass} API key in shell profile ${dim(`(${maskKey(apiKey)})`)}`);
136
238
  console.log(` ${warn} Re-run ${cyan("npx @pushary/agent-hooks setup")} so hooks read it from ${dim("~/.pushary/config.json")} without a shell reload`);
137
239
  } else {
138
240
  check(!!apiKey, "API key in environment", apiKey ? maskKey(apiKey) : "PUSHARY_API_KEY not set");
139
241
  }
140
- const CLAUDE_JSON = join(homedir(), ".claude.json");
242
+ if (source.drift) {
243
+ check(
244
+ false,
245
+ "One key, not two",
246
+ `shell has ${source.drift.envPrefix}, ${source.drift.configPath} has ${source.drift.configPrefix} \u2014 the shell one wins. Re-run setup, or open a new terminal.`
247
+ );
248
+ }
249
+ let keyVerdict = null;
250
+ if (apiKey) {
251
+ keyVerdict = await checkApiKey(apiKey);
252
+ switch (keyVerdict.kind) {
253
+ case "ok":
254
+ check(
255
+ true,
256
+ "Key verified with pushary.com",
257
+ `${keyVerdict.identity.siteSlug || keyVerdict.identity.siteName} \xB7 plan ${keyVerdict.identity.plan} \xB7 ${keyVerdict.identity.subscriptionStatus}`
258
+ );
259
+ break;
260
+ case "locked":
261
+ check(false, "Plan active", describeKeyCheck(keyVerdict).message);
262
+ for (const line of describeKeyCheck(keyVerdict).next) console.log(` ${dim(line)}`);
263
+ break;
264
+ case "rejected":
265
+ check(false, "Key accepted by pushary.com", describeKeyCheck(keyVerdict).message);
266
+ for (const line of describeKeyCheck(keyVerdict).next) console.log(` ${dim(line)}`);
267
+ break;
268
+ case "malformed":
269
+ check(false, "Key format", "expected pk_xxx.xxx");
270
+ break;
271
+ case "unreachable":
272
+ console.log(` ${warn} Could not verify the key with pushary.com ${dim(`(${keyVerdict.detail})`)}`);
273
+ break;
274
+ }
275
+ }
276
+ const CLAUDE_JSON = join(homedir2(), ".claude.json");
141
277
  const claudeJson = readJson(CLAUDE_JSON);
142
278
  const mcpServers = claudeJson?.mcpServers ?? {};
143
279
  const pusharyServer = mcpServers?.pushary;
@@ -179,10 +315,10 @@ var main = async () => {
179
315
  } else {
180
316
  check(false, "Claude Code: settings.json", "not found");
181
317
  }
182
- const codexConfigPath = join(homedir(), ".codex", "config.toml");
183
- const codexHooksPath = join(homedir(), ".codex", "hooks.json");
184
- const codexHooksJson = readJson(codexHooksPath);
185
- if (existsSync(codexConfigPath) || codexHooksJson) {
318
+ const codexConfigPath = codexConfigToml();
319
+ const codexHooksPath = codexHooksJson();
320
+ const codexHooksConfig = readJson(codexHooksPath);
321
+ if (existsSync(codexConfigPath) || codexHooksConfig) {
186
322
  const codexConfig = existsSync(codexConfigPath) ? readFileSync(codexConfigPath, "utf-8") : "";
187
323
  let codexParsed = {};
188
324
  try {
@@ -221,11 +357,11 @@ var main = async () => {
221
357
  }
222
358
  }
223
359
  const codexNotifyPath = codexConfig.match(/["']([^"']*pushary-codex[^"']*)["']/)?.[1] ?? null;
224
- const hooksInstalled = !!codexHooksJson && hasCodexHooks(codexHooksJson);
360
+ const hooksInstalled = !!codexHooksConfig && hasCodexHooks(codexHooksConfig);
225
361
  if (hooksInstalled) {
226
- const missingEvents = missingCodexHookEvents(codexHooksJson);
362
+ const missingEvents = missingCodexHookEvents(codexHooksConfig);
227
363
  check(missingEvents.length === 0, "Codex: native hooks installed", missingEvents.length === 0 ? "all 6 events" : `missing ${missingEvents.join(", ")}, re-run setup`);
228
- const hookCommand = extractHookCommand(codexHooksJson.hooks?.PreToolUse, "pushary-codex-hook") ?? extractHookCommand(codexHooksJson.hooks?.PermissionRequest, "pushary-codex-hook");
364
+ const hookCommand = extractHookCommand(codexHooksConfig.hooks?.PreToolUse, "pushary-codex-hook") ?? extractHookCommand(codexHooksConfig.hooks?.PermissionRequest, "pushary-codex-hook");
229
365
  if (hookCommand) {
230
366
  const resolves = commandResolves(hookCommand);
231
367
  check(resolves, "Codex: hook command resolves", resolves ? hookCommand : `not on PATH: ${hookCommand}`);
@@ -247,13 +383,13 @@ var main = async () => {
247
383
  check(resolves, "Codex: notify handler resolves", resolves ? codexNotifyPath : `not found: ${codexNotifyPath}`);
248
384
  }
249
385
  }
250
- const codexSkillPath = join(homedir(), ".codex", "skills", "pushary", "SKILL.md");
386
+ const codexSkillPath = join(codexSkillDir(), "SKILL.md");
251
387
  check(existsSync(codexSkillPath), "Codex: skill installed");
252
- const codexAgentsMd = join(homedir(), ".codex", "AGENTS.md");
253
- const hasCodexInstructions = hasInstructionBlock(codexAgentsMd);
254
- check(hasCodexInstructions, "Codex: proactive instructions (AGENTS.md)", hasCodexInstructions ? codexAgentsMd : "missing \u2014 re-run setup so Codex asks via push, not the terminal");
388
+ const codexAgentsMd2 = codexAgentsMd();
389
+ const hasCodexInstructions = hasInstructionBlock(codexAgentsMd2);
390
+ check(hasCodexInstructions, "Codex: proactive instructions (AGENTS.md)", hasCodexInstructions ? codexAgentsMd2 : "missing \u2014 re-run setup so Codex asks via push, not the terminal");
255
391
  }
256
- const geminiSettingsPath = join(homedir(), ".gemini", "settings.json");
392
+ const geminiSettingsPath = join(homedir2(), ".gemini", "settings.json");
257
393
  const geminiSettings = readJson(geminiSettingsPath);
258
394
  if (geminiSettings) {
259
395
  const geminiServers = geminiSettings.mcpServers ?? {};
@@ -291,13 +427,13 @@ var main = async () => {
291
427
  } else {
292
428
  check(false, "Gemini CLI: native hooks installed", "no Pushary hooks in ~/.gemini/settings.json \u2014 re-run setup");
293
429
  }
294
- const geminiMd = join(homedir(), ".gemini", "GEMINI.md");
430
+ const geminiMd = join(homedir2(), ".gemini", "GEMINI.md");
295
431
  const hasGeminiInstructions = hasInstructionBlock(geminiMd);
296
432
  check(hasGeminiInstructions, "Gemini CLI: proactive instructions (GEMINI.md)", hasGeminiInstructions ? geminiMd : "missing \u2014 re-run setup so Gemini asks via push, not the terminal");
297
433
  }
298
- const cursorPluginDir = join(homedir(), ".cursor", "plugins", "local", "pushary");
434
+ const cursorPluginDir = join(homedir2(), ".cursor", "plugins", "local", "pushary");
299
435
  if (existsSync(cursorPluginDir)) {
300
- const cursorUserHooks = join(homedir(), ".cursor", "hooks.json");
436
+ const cursorUserHooks = join(homedir2(), ".cursor", "hooks.json");
301
437
  const userHooks = readJson(cursorUserHooks);
302
438
  const gateEntry = userHooks.hooks?.beforeShellExecution?.find(
303
439
  (h) => String(h.command ?? "").includes("pushary-gate")
@@ -321,22 +457,22 @@ var main = async () => {
321
457
  }
322
458
  }
323
459
  check(existsSync(SKILL_PATH), "Skill installed", existsSync(SKILL_PATH) ? SKILL_PATH : "not found");
324
- let globalVersion = "";
460
+ let globalVersion2 = "";
325
461
  try {
326
- globalVersion = execNpm("list -g --no-workspaces @pushary/agent-hooks --depth=0", { timeout: 1e4 }).toString().match(/@pushary\/agent-hooks@([\d.]+)/)?.[1] ?? "";
462
+ globalVersion2 = execNpm("list -g --no-workspaces @pushary/agent-hooks --depth=0", { timeout: 1e4 }).toString().match(/@pushary\/agent-hooks@([\d.]+)/)?.[1] ?? "";
327
463
  } catch {
328
- globalVersion = "";
464
+ globalVersion2 = "";
329
465
  }
330
- check(!!globalVersion, "Global package installed", globalVersion || "not found");
331
- if (globalVersion) {
466
+ check(!!globalVersion2, "Global package installed", globalVersion2 || "not found");
467
+ if (globalVersion2) {
332
468
  try {
333
469
  const res = await fetch("https://registry.npmjs.org/@pushary/agent-hooks/latest", {
334
470
  signal: AbortSignal.timeout(5e3)
335
471
  });
336
472
  const latest = res.ok ? (await res.json()).version ?? "" : "";
337
473
  if (latest) {
338
- const upToDate = latest === globalVersion;
339
- check(upToDate, "Global package up to date", upToDate ? globalVersion : `${globalVersion} installed, ${latest} available \u2014 run npx @pushary/agent-hooks@latest upgrade`);
474
+ const upToDate = latest === globalVersion2;
475
+ check(upToDate, "Global package up to date", upToDate ? globalVersion2 : `${globalVersion2} installed, ${latest} available \u2014 run npx @pushary/agent-hooks@latest upgrade`);
340
476
  }
341
477
  } catch {
342
478
  }
@@ -351,7 +487,7 @@ var main = async () => {
351
487
  }
352
488
  });
353
489
  if (aliasRc) {
354
- if (globalVersion) {
490
+ if (globalVersion2) {
355
491
  check(true, "claude alias \u2192 pushary claude", `set in ${aliasRc.split("/").pop()}`);
356
492
  } else {
357
493
  check(false, "claude alias \u2192 pushary claude", "alias is set but pushary is not installed \u2014 run `unalias claude` (this shell) or reinstall");
@@ -428,25 +564,29 @@ var main = async () => {
428
564
  }
429
565
  console.log();
430
566
  console.log(` ${dim("Notification Delivery")}`);
431
- try {
432
- const notifRes = await fetch("https://pushary.com/api/v1/server/send", {
433
- method: "POST",
434
- headers: {
435
- "Content-Type": "application/json",
436
- "Authorization": `Bearer ${apiKey}`
437
- },
438
- body: JSON.stringify({
439
- title: "Pushary Doctor",
440
- body: "If you see this, push notifications are working."
441
- }),
442
- signal: AbortSignal.timeout(1e4)
443
- });
444
- check(notifRes.ok, "Push notification sent", notifRes.ok ? "check your phone" : `${notifRes.status}`);
567
+ const probe = await fetchChannels(apiKey);
568
+ if (probe.kind !== "ok") {
569
+ check(false, "Phone reachable", probe.kind === "unreachable" ? `could not read your devices (${probe.detail})` : probe.kind === "locked" ? "no plan active, so nothing can be delivered" : "the server rejected this key");
570
+ } else if (probe.status.precise) {
571
+ const channels = probe.status;
572
+ const verdict = reachVerdict(channels);
573
+ check(verdict.kind === "yours", "Phone reachable", verdict.kind === "yours" ? describeReach(channels) : verdict.kind === "nobody" ? "nothing connected, run pushary connect or get the app at https://pushary.com/download" : `${describeReach(channels)}; run pushary connect to add your own`);
574
+ }
575
+ if (options.noPush) {
576
+ console.log(` ${dim("\u2013")} Test push skipped ${dim("(--no-push)")}`);
577
+ } else try {
578
+ const result = await callMcpTool(apiKey, "send_notification", {
579
+ title: "Pushary Doctor",
580
+ body: "If you see this, push notifications are working.",
581
+ agentName: "Pushary Doctor"
582
+ }, { sessionId, timeoutMs: 2e4 });
583
+ const sent = typeof result.sent === "number" ? result.sent : (result.delivery?.mobile?.recipients ?? 0) + (result.delivery?.web?.recipients ?? 0);
584
+ check(sent > 0, "Push notification sent", sent > 0 ? `${sent} device${sent === 1 ? "" : "s"} \u2014 check your phone` : "reached 0 devices");
445
585
  } catch (err) {
446
586
  const msg = err instanceof Error ? err.message : "network error";
447
587
  check(false, "Push notification sent", msg);
448
588
  }
449
- const testQuestion = process.stdin.isTTY ? await confirm({ message: "Test question roundtrip? (sends a push notification)", default: false }) : false;
589
+ const testQuestion = options.roundtrip ? true : options.noPush || options.json || !process.stdin.isTTY ? false : await confirm({ message: "Test question roundtrip? (sends a push notification)", default: false });
450
590
  if (testQuestion) {
451
591
  console.log();
452
592
  console.log(` ${dim("Question Roundtrip")}`);
@@ -520,6 +660,41 @@ var main = async () => {
520
660
  console.log(` ${dim("Run")} ${cyan("npx @pushary/agent-hooks@latest clean")} ${dim("then")} ${cyan("setup")} ${dim("to fix.")}`);
521
661
  }
522
662
  console.log();
663
+ const keyExit = keyVerdict && (keyVerdict.kind === "rejected" || keyVerdict.kind === "locked" || keyVerdict.kind === "malformed") ? describeKeyCheck(keyVerdict).exitCode : null;
664
+ const exitCode = keyExit ?? (failed.length > 0 ? EXIT.PROBLEMS_FOUND : EXIT.OK);
665
+ if (options.bundle) {
666
+ try {
667
+ const bundle = redact(
668
+ await collectBundle({
669
+ identity: keyVerdict?.kind === "ok" ? keyVerdict.identity : null,
670
+ checks: results.map((r) => ({ id: r.label, ok: r.passed, detail: r.detail ?? "" }))
671
+ })
672
+ );
673
+ const target = join(process.cwd(), bundleFileName(bundle));
674
+ writeFileSync(target, `${JSON.stringify(bundle, null, 2)}
675
+ `, "utf-8");
676
+ const say = options.json ? process.stderr : process.stdout;
677
+ say.write(`
678
+ Diagnostics written to ${target}
679
+ `);
680
+ say.write(" Keys, your home directory and this machine name are redacted. Read it before sharing.\n\n");
681
+ } catch (err) {
682
+ process.stderr.write(` ! could not write the diagnostics file: ${err instanceof Error ? err.message : String(err)}
683
+ `);
684
+ }
685
+ }
686
+ if (options.json) {
687
+ process.stdout.write(`${JSON.stringify({
688
+ ok: exitCode === EXIT.OK,
689
+ exitCode,
690
+ keyCheck: keyVerdict?.kind ?? null,
691
+ passed: results.filter((r) => r.passed).length,
692
+ failed: failed.length,
693
+ checks: results.map((r) => ({ label: r.label, passed: r.passed, detail: r.detail ?? null }))
694
+ }, null, 2)}
695
+ `);
696
+ }
697
+ process.exit(exitCode);
523
698
  };
524
699
  main().catch((err) => {
525
700
  console.error(` doctor failed: ${err instanceof Error ? err.message : String(err)}`);
@@ -23,24 +23,24 @@ import {
23
23
  savePendingQuestion,
24
24
  sendNotification,
25
25
  waitForAnswer
26
- } from "../chunk-WNRYRN2R.js";
27
- import {
28
- getMachineId
29
- } from "../chunk-RN3NOEJF.js";
26
+ } from "../chunk-HIIBSYFJ.js";
30
27
  import {
31
28
  isGatingMoment,
32
29
  recordKeylessMoment
33
30
  } from "../chunk-WLGEY4UX.js";
34
- import "../chunk-DWED7BS3.js";
31
+ import "../chunk-HNTKTK5B.js";
35
32
  import {
36
33
  DECISION_LINE_MAX,
37
34
  effectiveWaitSeconds,
38
35
  hookWaitClamped,
39
36
  hookWaitDeadline
40
37
  } from "../chunk-DQAN3JQP.js";
38
+ import {
39
+ getMachineId
40
+ } from "../chunk-RN3NOEJF.js";
41
41
  import {
42
42
  getApiKey
43
- } from "../chunk-NKXSILEW.js";
43
+ } from "../chunk-2UMNXADU.js";
44
44
 
45
45
  // bin/pushary-gemini-hook.ts
46
46
  import { basename } from "path";
@@ -1,17 +1,22 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  handlePreToolUse
4
- } from "../chunk-BE5X3WXL.js";
4
+ } from "../chunk-5SO3CEGJ.js";
5
5
  import "../chunk-CAJZAFVS.js";
6
+ import {
7
+ exitOnHelpFlag
8
+ } from "../chunk-IIZZYUWK.js";
9
+ import "../chunk-SP7OZXCQ.js";
6
10
  import "../chunk-YHG74UFF.js";
7
- import "../chunk-WNRYRN2R.js";
8
- import "../chunk-RN3NOEJF.js";
11
+ import "../chunk-HIIBSYFJ.js";
9
12
  import "../chunk-WLGEY4UX.js";
10
- import "../chunk-DWED7BS3.js";
13
+ import "../chunk-HNTKTK5B.js";
11
14
  import "../chunk-DQAN3JQP.js";
12
- import "../chunk-NKXSILEW.js";
15
+ import "../chunk-RN3NOEJF.js";
16
+ import "../chunk-2UMNXADU.js";
13
17
 
14
18
  // bin/pushary-hook.ts
19
+ exitOnHelpFlag("hook");
15
20
  var main = async () => {
16
21
  let rawInput = "";
17
22
  for await (const chunk of process.stdin) {
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node