@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
@@ -4,6 +4,8 @@ import {
4
4
  } from "../chunk-IIENZE2J.js";
5
5
  import {
6
6
  GEMINI_HOOK_BINARY,
7
+ codexConfigToml,
8
+ codexHooksJson,
7
9
  hasCodexHooks,
8
10
  hasGeminiHooks,
9
11
  hasInstructionBlock,
@@ -11,21 +13,50 @@ import {
11
13
  missingGeminiHookEvents,
12
14
  readCodexMcpAuth,
13
15
  untrustedCodexHookEvents
14
- } from "../chunk-BQLCELYC.js";
16
+ } from "../chunk-S4TFBJ5O.js";
17
+ import {
18
+ describeReach,
19
+ fetchChannels,
20
+ reachVerdict
21
+ } from "../chunk-GX64YGU3.js";
22
+ import "../chunk-3EGEA4KH.js";
23
+ import {
24
+ parseFlags
25
+ } from "../chunk-N4DA4CJB.js";
26
+ import {
27
+ readKeySource
28
+ } from "../chunk-B26DNXCA.js";
29
+ import {
30
+ checkApiKey,
31
+ describeKeyCheck
32
+ } from "../chunk-UKNAAVEE.js";
33
+ import {
34
+ createIo
35
+ } from "../chunk-TX7KBKT7.js";
36
+ import {
37
+ UsageError
38
+ } from "../chunk-7QLSKOSU.js";
39
+ import "../chunk-BC3VCZ3E.js";
15
40
  import {
16
41
  execNpm
17
- } from "../chunk-J7JWI3KU.js";
42
+ } from "../chunk-MUEW424A.js";
43
+ import {
44
+ exitOnHelpFlag
45
+ } from "../chunk-HUJQSP4F.js";
46
+ import {
47
+ EXIT
48
+ } from "../chunk-SML23YOT.js";
18
49
  import {
19
50
  readLedgerSummary
20
51
  } from "../chunk-WLGEY4UX.js";
21
52
  import {
22
53
  callMcpTool,
23
54
  sendMcpRequest
24
- } from "../chunk-DWED7BS3.js";
55
+ } from "../chunk-HNTKTK5B.js";
25
56
  import "../chunk-DQAN3JQP.js";
26
57
  import {
27
58
  getBaseUrl
28
- } from "../chunk-NKXSILEW.js";
59
+ } from "../chunk-2UMNXADU.js";
29
60
 
30
61
  // bin/pushary-doctor.ts
31
62
  import { existsSync, readFileSync } from "fs";
@@ -34,12 +65,35 @@ import { homedir } from "os";
34
65
  import { execSync } from "child_process";
35
66
  import { confirm } from "@inquirer/prompts";
36
67
  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`;
68
+ exitOnHelpFlag("doctor");
69
+ var parseDoctorOptions = (argv) => {
70
+ const flags = parseFlags(argv, { bools: ["json", "roundtrip", "no-push", "help", "h"], values: [] });
71
+ if (flags.command !== void 0 && flags.command !== "doctor") {
72
+ throw new UsageError(`doctor takes no positional arguments (got ${flags.command})`);
73
+ }
74
+ const roundtrip = flags.bools.has("roundtrip");
75
+ const noPush = flags.bools.has("no-push");
76
+ if (roundtrip && noPush) {
77
+ throw new UsageError("--roundtrip needs to send a push, so it cannot be combined with --no-push");
78
+ }
79
+ return { json: flags.bools.has("json"), roundtrip, noPush };
80
+ };
81
+ var options;
82
+ try {
83
+ options = parseDoctorOptions(process.argv.slice(2));
84
+ } catch (err) {
85
+ process.stderr.write(` ! ${err instanceof Error ? err.message : String(err)}
86
+ `);
87
+ process.stderr.write(" Run `pushary doctor --help` for the full list.\n");
88
+ process.exit(EXIT.USAGE);
89
+ }
90
+ if (options.json) {
91
+ console.log = (...args) => {
92
+ process.stderr.write(`${args.map((a) => typeof a === "string" ? a : String(a)).join(" ")}
93
+ `);
94
+ };
95
+ }
96
+ var { bold, dim, green, yellow, red, cyan } = createIo();
43
97
  var pass = green("\u2713");
44
98
  var fail = red("\u2717");
45
99
  var warn = yellow("!");
@@ -101,19 +155,9 @@ var main = async () => {
101
155
  console.log(` ${bold("Pushary Doctor")}`);
102
156
  console.log();
103
157
  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
- }
158
+ const source = readKeySource();
159
+ let apiKey = source.key ?? void 0;
160
+ let keySource = source.kind === "env" ? "env" : source.kind === "config" ? "config-file" : null;
117
161
  if (!apiKey) {
118
162
  for (const f of SHELL_FILES) {
119
163
  try {
@@ -130,13 +174,47 @@ var main = async () => {
130
174
  }
131
175
  const maskKey = (k) => `pk_${k.split(".")[0]?.slice(3, 7)}...`;
132
176
  if (keySource === "config-file") {
133
- console.log(` ${pass} API key in ${dim("~/.pushary/config.json")} ${dim(`(${maskKey(apiKey)})`)}`);
177
+ console.log(` ${pass} API key in ${dim(source.origin ?? "the config file")} ${dim(`(${maskKey(apiKey)})`)}`);
134
178
  } else if (keySource === "profile") {
135
179
  console.log(` ${pass} API key in shell profile ${dim(`(${maskKey(apiKey)})`)}`);
136
180
  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
181
  } else {
138
182
  check(!!apiKey, "API key in environment", apiKey ? maskKey(apiKey) : "PUSHARY_API_KEY not set");
139
183
  }
184
+ if (source.drift) {
185
+ check(
186
+ false,
187
+ "One key, not two",
188
+ `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.`
189
+ );
190
+ }
191
+ let keyVerdict = null;
192
+ if (apiKey) {
193
+ keyVerdict = await checkApiKey(apiKey);
194
+ switch (keyVerdict.kind) {
195
+ case "ok":
196
+ check(
197
+ true,
198
+ "Key verified with pushary.com",
199
+ `${keyVerdict.identity.siteSlug || keyVerdict.identity.siteName} \xB7 plan ${keyVerdict.identity.plan} \xB7 ${keyVerdict.identity.subscriptionStatus}`
200
+ );
201
+ break;
202
+ case "locked":
203
+ check(false, "Plan active", describeKeyCheck(keyVerdict).message);
204
+ for (const line of describeKeyCheck(keyVerdict).next) console.log(` ${dim(line)}`);
205
+ break;
206
+ case "rejected":
207
+ check(false, "Key accepted by pushary.com", describeKeyCheck(keyVerdict).message);
208
+ for (const line of describeKeyCheck(keyVerdict).next) console.log(` ${dim(line)}`);
209
+ break;
210
+ case "malformed":
211
+ check(false, "Key format", "expected pk_xxx.xxx");
212
+ break;
213
+ case "unreachable":
214
+ console.log(` ${warn} Could not verify the key with pushary.com ${dim(`(${keyVerdict.detail})`)}`);
215
+ break;
216
+ }
217
+ }
140
218
  const CLAUDE_JSON = join(homedir(), ".claude.json");
141
219
  const claudeJson = readJson(CLAUDE_JSON);
142
220
  const mcpServers = claudeJson?.mcpServers ?? {};
@@ -179,10 +257,10 @@ var main = async () => {
179
257
  } else {
180
258
  check(false, "Claude Code: settings.json", "not found");
181
259
  }
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) {
260
+ const codexConfigPath = codexConfigToml();
261
+ const codexHooksPath = codexHooksJson();
262
+ const codexHooksConfig = readJson(codexHooksPath);
263
+ if (existsSync(codexConfigPath) || codexHooksConfig) {
186
264
  const codexConfig = existsSync(codexConfigPath) ? readFileSync(codexConfigPath, "utf-8") : "";
187
265
  let codexParsed = {};
188
266
  try {
@@ -221,11 +299,11 @@ var main = async () => {
221
299
  }
222
300
  }
223
301
  const codexNotifyPath = codexConfig.match(/["']([^"']*pushary-codex[^"']*)["']/)?.[1] ?? null;
224
- const hooksInstalled = !!codexHooksJson && hasCodexHooks(codexHooksJson);
302
+ const hooksInstalled = !!codexHooksConfig && hasCodexHooks(codexHooksConfig);
225
303
  if (hooksInstalled) {
226
- const missingEvents = missingCodexHookEvents(codexHooksJson);
304
+ const missingEvents = missingCodexHookEvents(codexHooksConfig);
227
305
  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");
306
+ const hookCommand = extractHookCommand(codexHooksConfig.hooks?.PreToolUse, "pushary-codex-hook") ?? extractHookCommand(codexHooksConfig.hooks?.PermissionRequest, "pushary-codex-hook");
229
307
  if (hookCommand) {
230
308
  const resolves = commandResolves(hookCommand);
231
309
  check(resolves, "Codex: hook command resolves", resolves ? hookCommand : `not on PATH: ${hookCommand}`);
@@ -428,25 +506,29 @@ var main = async () => {
428
506
  }
429
507
  console.log();
430
508
  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}`);
509
+ const probe = await fetchChannels(apiKey);
510
+ if (probe.kind !== "ok") {
511
+ 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");
512
+ } else if (probe.status.precise) {
513
+ const channels = probe.status;
514
+ const verdict = reachVerdict(channels);
515
+ 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`);
516
+ }
517
+ if (options.noPush) {
518
+ console.log(` ${dim("\u2013")} Test push skipped ${dim("(--no-push)")}`);
519
+ } else try {
520
+ const result = await callMcpTool(apiKey, "send_notification", {
521
+ title: "Pushary Doctor",
522
+ body: "If you see this, push notifications are working.",
523
+ agentName: "Pushary Doctor"
524
+ }, { sessionId, timeoutMs: 2e4 });
525
+ const sent = typeof result.sent === "number" ? result.sent : (result.delivery?.mobile?.recipients ?? 0) + (result.delivery?.web?.recipients ?? 0);
526
+ check(sent > 0, "Push notification sent", sent > 0 ? `${sent} device${sent === 1 ? "" : "s"} \u2014 check your phone` : "reached 0 devices");
445
527
  } catch (err) {
446
528
  const msg = err instanceof Error ? err.message : "network error";
447
529
  check(false, "Push notification sent", msg);
448
530
  }
449
- const testQuestion = process.stdin.isTTY ? await confirm({ message: "Test question roundtrip? (sends a push notification)", default: false }) : false;
531
+ 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
532
  if (testQuestion) {
451
533
  console.log();
452
534
  console.log(` ${dim("Question Roundtrip")}`);
@@ -520,6 +602,20 @@ var main = async () => {
520
602
  console.log(` ${dim("Run")} ${cyan("npx @pushary/agent-hooks@latest clean")} ${dim("then")} ${cyan("setup")} ${dim("to fix.")}`);
521
603
  }
522
604
  console.log();
605
+ const keyExit = keyVerdict && (keyVerdict.kind === "rejected" || keyVerdict.kind === "locked" || keyVerdict.kind === "malformed") ? describeKeyCheck(keyVerdict).exitCode : null;
606
+ const exitCode = keyExit ?? (failed.length > 0 ? EXIT.PROBLEMS_FOUND : EXIT.OK);
607
+ if (options.json) {
608
+ process.stdout.write(`${JSON.stringify({
609
+ ok: exitCode === EXIT.OK,
610
+ exitCode,
611
+ keyCheck: keyVerdict?.kind ?? null,
612
+ passed: results.filter((r) => r.passed).length,
613
+ failed: failed.length,
614
+ checks: results.map((r) => ({ label: r.label, passed: r.passed, detail: r.detail ?? null }))
615
+ }, null, 2)}
616
+ `);
617
+ }
618
+ process.exit(exitCode);
523
619
  };
524
620
  main().catch((err) => {
525
621
  console.error(` doctor failed: ${err instanceof Error ? err.message : String(err)}`);
@@ -23,7 +23,7 @@ import {
23
23
  savePendingQuestion,
24
24
  sendNotification,
25
25
  waitForAnswer
26
- } from "../chunk-WNRYRN2R.js";
26
+ } from "../chunk-VFBYRR2N.js";
27
27
  import {
28
28
  getMachineId
29
29
  } from "../chunk-RN3NOEJF.js";
@@ -31,7 +31,7 @@ import {
31
31
  isGatingMoment,
32
32
  recordKeylessMoment
33
33
  } from "../chunk-WLGEY4UX.js";
34
- import "../chunk-DWED7BS3.js";
34
+ import "../chunk-HNTKTK5B.js";
35
35
  import {
36
36
  DECISION_LINE_MAX,
37
37
  effectiveWaitSeconds,
@@ -40,7 +40,7 @@ import {
40
40
  } from "../chunk-DQAN3JQP.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-HI4AGGE6.js";
5
5
  import "../chunk-CAJZAFVS.js";
6
+ import {
7
+ exitOnHelpFlag
8
+ } from "../chunk-HUJQSP4F.js";
9
+ import "../chunk-SML23YOT.js";
6
10
  import "../chunk-YHG74UFF.js";
7
- import "../chunk-WNRYRN2R.js";
11
+ import "../chunk-VFBYRR2N.js";
8
12
  import "../chunk-RN3NOEJF.js";
9
13
  import "../chunk-WLGEY4UX.js";
10
- import "../chunk-DWED7BS3.js";
14
+ import "../chunk-HNTKTK5B.js";
11
15
  import "../chunk-DQAN3JQP.js";
12
- import "../chunk-NKXSILEW.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
@@ -0,0 +1,156 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ runBrowserLogin
4
+ } from "../chunk-QHOVJXOT.js";
5
+ import "../chunk-3EGEA4KH.js";
6
+ import {
7
+ parseFlags
8
+ } from "../chunk-N4DA4CJB.js";
9
+ import {
10
+ readKeySource,
11
+ writeKey
12
+ } from "../chunk-B26DNXCA.js";
13
+ import {
14
+ checkApiKey,
15
+ describeKeyCheck
16
+ } from "../chunk-UKNAAVEE.js";
17
+ import {
18
+ createIo
19
+ } from "../chunk-TX7KBKT7.js";
20
+ import {
21
+ UsageError
22
+ } from "../chunk-7QLSKOSU.js";
23
+ import "../chunk-BC3VCZ3E.js";
24
+ import {
25
+ exitOnHelpFlag
26
+ } from "../chunk-HUJQSP4F.js";
27
+ import {
28
+ EXIT,
29
+ getPackageVersion
30
+ } from "../chunk-SML23YOT.js";
31
+ import {
32
+ isValidApiKey
33
+ } from "../chunk-DQAN3JQP.js";
34
+ import "../chunk-2UMNXADU.js";
35
+
36
+ // bin/pushary-login.ts
37
+ exitOnHelpFlag("login");
38
+ var parseLoginOptions = (argv) => {
39
+ const flags = parseFlags(argv, {
40
+ bools: ["with-token", "no-browser", "json", "force", "help", "h"],
41
+ values: []
42
+ });
43
+ if (flags.command !== void 0 && flags.command !== "login") {
44
+ throw new UsageError(`login takes no positional arguments (got ${flags.command})`);
45
+ }
46
+ return {
47
+ withToken: flags.bools.has("with-token"),
48
+ noBrowser: flags.bools.has("no-browser"),
49
+ json: flags.bools.has("json"),
50
+ force: flags.bools.has("force")
51
+ };
52
+ };
53
+ var options;
54
+ try {
55
+ options = parseLoginOptions(process.argv.slice(2));
56
+ } catch (err) {
57
+ process.stderr.write(` ! ${err instanceof Error ? err.message : String(err)}
58
+ `);
59
+ process.stderr.write(" Run `pushary login --help` for the full list.\n");
60
+ process.exit(EXIT.USAGE);
61
+ }
62
+ var { bold, dim, green, cyan, yellow } = createIo();
63
+ var check = green("\u2713");
64
+ var say = options.json ? (line) => process.stderr.write(`${line}
65
+ `) : (line) => process.stdout.write(`${line}
66
+ `);
67
+ var emit = (payload) => {
68
+ if (options.json) process.stdout.write(`${JSON.stringify(payload, null, 2)}
69
+ `);
70
+ };
71
+ var readStdin = async () => {
72
+ const chunks = [];
73
+ for await (const chunk of process.stdin) chunks.push(Buffer.from(chunk));
74
+ return Buffer.concat(chunks).toString("utf8").trim();
75
+ };
76
+ var store = (apiKey, siteSlug, keyName) => {
77
+ const result = writeKey(apiKey);
78
+ say("");
79
+ say(` ${check} ${bold("Signed in.")}${siteSlug ? ` ${dim(`Site ${siteSlug}`)}` : ""}`);
80
+ say(` ${dim(`Key saved to ${result.configPath}`)}`);
81
+ if (result.rc.kind === "written" || result.rc.kind === "updated") {
82
+ say(` ${dim(`Shell export ${result.rc.kind} in ${result.rc.path}`)}`);
83
+ } else if (result.rc.kind === "declined") {
84
+ say(` ${yellow("!")} ${dim(`Left ${result.rc.path} alone: line ${result.rc.declinedLine} already sets PUSHARY_API_KEY.`)}`);
85
+ }
86
+ emit({ ok: true, siteSlug, keyName, configPath: result.configPath, rc: result.rc.kind });
87
+ };
88
+ var loginWithToken = async () => {
89
+ const token = await readStdin();
90
+ if (!token) {
91
+ say(` ${yellow("!")} Nothing arrived on stdin. Pipe the key in: pushary login --with-token < key.txt`);
92
+ emit({ ok: false, reason: "no_input" });
93
+ process.exit(EXIT.INPUT_REQUIRED);
94
+ }
95
+ if (!isValidApiKey(token)) {
96
+ say(` ${yellow("!")} That does not look like a Pushary key. Expected pk_xxx.xxx`);
97
+ emit({ ok: false, reason: "malformed" });
98
+ process.exit(EXIT.USAGE);
99
+ }
100
+ const verdict = await checkApiKey(token);
101
+ if (verdict.kind !== "ok") {
102
+ const report = describeKeyCheck(verdict);
103
+ say(` ${yellow("!")} ${report.message}`);
104
+ for (const line of report.next) say(` ${dim(line)}`);
105
+ emit({ ok: false, reason: verdict.kind });
106
+ if (verdict.kind !== "unreachable") process.exit(report.exitCode);
107
+ }
108
+ store(token, verdict.kind === "ok" ? verdict.identity.siteSlug : "", "pasted key");
109
+ process.exit(EXIT.OK);
110
+ };
111
+ var main = async () => {
112
+ if (options.withToken) await loginWithToken();
113
+ if (!options.force) {
114
+ const existing = readKeySource();
115
+ if (existing.key) {
116
+ const verdict = await checkApiKey(existing.key);
117
+ if (verdict.kind === "ok") {
118
+ say("");
119
+ say(` ${check} Already signed in ${dim(`(${verdict.identity.siteSlug})`)}`);
120
+ say(` ${dim("Run")} ${cyan("pushary login --force")} ${dim("to replace this key.")}`);
121
+ emit({ ok: true, reused: true, siteSlug: verdict.identity.siteSlug });
122
+ process.exit(EXIT.OK);
123
+ }
124
+ }
125
+ }
126
+ const outcome = await runBrowserLogin(
127
+ { say, bold, dim, cyan, yellow },
128
+ { cliVersion: getPackageVersion(), noBrowser: options.noBrowser }
129
+ );
130
+ if (outcome.kind === "ok") {
131
+ store(outcome.apiKey, outcome.siteSlug, outcome.keyName);
132
+ process.exit(EXIT.OK);
133
+ }
134
+ if (outcome.kind === "unavailable") {
135
+ say(` ${yellow("!")} Couldn't start a login ${dim(`(${outcome.detail})`)}`);
136
+ say(` ${dim("Check your connection, or paste a key with")} ${cyan("pushary login --with-token < key.txt")}`);
137
+ emit({ ok: false, reason: "unreachable", detail: outcome.detail });
138
+ process.exit(EXIT.UNREACHABLE);
139
+ }
140
+ if (outcome.kind === "rejected") {
141
+ say("");
142
+ say(` ${yellow("!")} That request was rejected in the browser. Nothing was written.`);
143
+ emit({ ok: false, reason: "rejected" });
144
+ process.exit(EXIT.NOT_CONFIGURED);
145
+ }
146
+ say("");
147
+ say(` ${yellow("!")} The code expired before it was approved.`);
148
+ say(` ${dim("Run")} ${cyan("pushary login")} ${dim("for a fresh one.")}`);
149
+ emit({ ok: false, reason: "expired" });
150
+ process.exit(EXIT.NOT_CONFIGURED);
151
+ };
152
+ main().catch((err) => {
153
+ process.stderr.write(` ! login failed: ${err instanceof Error ? err.message : String(err)}
154
+ `);
155
+ process.exit(EXIT.FAILED);
156
+ });
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node
@@ -0,0 +1,102 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ parseFlags
4
+ } from "../chunk-N4DA4CJB.js";
5
+ import {
6
+ clearKey,
7
+ readKeySource
8
+ } from "../chunk-B26DNXCA.js";
9
+ import {
10
+ createIo
11
+ } from "../chunk-TX7KBKT7.js";
12
+ import {
13
+ UsageError
14
+ } from "../chunk-7QLSKOSU.js";
15
+ import "../chunk-BC3VCZ3E.js";
16
+ import {
17
+ exitOnHelpFlag
18
+ } from "../chunk-HUJQSP4F.js";
19
+ import {
20
+ EXIT
21
+ } from "../chunk-SML23YOT.js";
22
+ import "../chunk-2UMNXADU.js";
23
+
24
+ // bin/pushary-logout.ts
25
+ import { existsSync } from "fs";
26
+ import { homedir } from "os";
27
+ import { join } from "path";
28
+ exitOnHelpFlag("logout");
29
+ var parseLogoutOptions = (argv) => {
30
+ const flags = parseFlags(argv, { bools: ["json", "help", "h"], values: [] });
31
+ if (flags.command !== void 0 && flags.command !== "logout") {
32
+ throw new UsageError(`logout takes no positional arguments (got ${flags.command})`);
33
+ }
34
+ return { json: flags.bools.has("json") };
35
+ };
36
+ var options;
37
+ try {
38
+ options = parseLogoutOptions(process.argv.slice(2));
39
+ } catch (err) {
40
+ process.stderr.write(` ! ${err instanceof Error ? err.message : String(err)}
41
+ `);
42
+ process.exit(EXIT.USAGE);
43
+ }
44
+ var { bold, dim, green, cyan, yellow } = createIo();
45
+ var say = options.json ? (line) => process.stderr.write(`${line}
46
+ `) : (line) => process.stdout.write(`${line}
47
+ `);
48
+ var AGENT_CONFIGS = [
49
+ { label: "Claude Code", path: join(homedir(), ".claude.json") },
50
+ { label: "Codex", path: join(process.env.CODEX_HOME ?? join(homedir(), ".codex"), "config.toml") },
51
+ { label: "Gemini CLI", path: join(homedir(), ".gemini", "settings.json") },
52
+ { label: "Cursor", path: join(homedir(), ".cursor", "mcp.json") }
53
+ ];
54
+ var main = () => {
55
+ const before = readKeySource();
56
+ const result = clearKey();
57
+ const remaining = AGENT_CONFIGS.filter((entry) => existsSync(entry.path));
58
+ say("");
59
+ if (!result.removedFromConfig && result.rcLinesDisabled === 0 && before.kind !== "env") {
60
+ say(` ${dim("No key was stored on this machine. Nothing to remove.")}`);
61
+ } else {
62
+ say(` ${green("\u2713")} ${bold("Signed out.")}`);
63
+ if (result.removedFromConfig) say(` ${dim(`Key removed from ${result.configPath}`)}`);
64
+ if (result.rcLinesDisabled > 0) {
65
+ say(` ${dim(`Commented out ${result.rcLinesDisabled} export line in ${result.rcPath}`)}`);
66
+ }
67
+ }
68
+ if (before.kind === "env") {
69
+ say("");
70
+ say(` ${yellow("!")} PUSHARY_API_KEY is set in this shell, and a child process cannot unset it.`);
71
+ say(` ${dim("Run")} ${cyan("unset PUSHARY_API_KEY")} ${dim("here, or open a new terminal.")}`);
72
+ }
73
+ if (remaining.length > 0) {
74
+ say("");
75
+ say(` ${dim("Still holding a copy of the key, untouched by logout:")}`);
76
+ for (const entry of remaining) say(` ${dim(`\u2022 ${entry.label} ${entry.path}`)}`);
77
+ say(` ${dim("Run")} ${cyan("pushary clean")} ${dim("to remove Pushary from those too.")}`);
78
+ }
79
+ say("");
80
+ say(` ${dim("The key itself is still valid server-side. Revoke it at")} ${cyan("pushary.com/dashboard/agent/settings")}`);
81
+ say("");
82
+ if (options.json) {
83
+ process.stdout.write(
84
+ `${JSON.stringify(
85
+ {
86
+ ok: true,
87
+ removedFromConfig: result.removedFromConfig,
88
+ configPath: result.configPath,
89
+ rcPath: result.rcPath,
90
+ rcLinesDisabled: result.rcLinesDisabled,
91
+ envStillSet: before.kind === "env",
92
+ agentConfigsRetainingKey: remaining.map((entry) => entry.path)
93
+ },
94
+ null,
95
+ 2
96
+ )}
97
+ `
98
+ );
99
+ }
100
+ process.exit(EXIT.OK);
101
+ };
102
+ main();
@@ -1,15 +1,28 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ exitOnFailedResponse
4
+ } from "../chunk-R6C4USIV.js";
5
+ import "../chunk-UKNAAVEE.js";
6
+ import {
7
+ createIo
8
+ } from "../chunk-TX7KBKT7.js";
9
+ import "../chunk-7QLSKOSU.js";
10
+ import {
11
+ exitOnHelpFlag
12
+ } from "../chunk-HUJQSP4F.js";
13
+ import {
14
+ EXIT
15
+ } from "../chunk-SML23YOT.js";
16
+ import "../chunk-DQAN3JQP.js";
2
17
  import {
3
18
  getApiKey,
4
19
  getBaseUrl
5
- } from "../chunk-NKXSILEW.js";
20
+ } from "../chunk-2UMNXADU.js";
6
21
 
7
22
  // bin/pushary-mode.ts
23
+ exitOnHelpFlag("mode");
8
24
  var VALID_MODES = ["push_only", "push_first", "terminal_only", "notify_only"];
9
- var dim = (s) => `\x1B[2m${s}\x1B[0m`;
10
- var green = (s) => `\x1B[32m${s}\x1B[0m`;
11
- var cyan = (s) => `\x1B[36m${s}\x1B[0m`;
12
- var yellow = (s) => `\x1B[33m${s}\x1B[0m`;
25
+ var { dim, green, cyan, yellow } = createIo();
13
26
  var parseDuration = (value) => {
14
27
  const match = value.match(/^(\d+)(m|h)$/);
15
28
  if (!match) return null;
@@ -30,6 +43,7 @@ var main = async () => {
30
43
  };
31
44
  if (!mode || mode === "status") {
32
45
  const res2 = await fetch(`${baseUrl}/api/mcp/mode`, { headers });
46
+ if (!res2.ok) await exitOnFailedResponse(res2, baseUrl);
33
47
  const data2 = await res2.json();
34
48
  if (!data2.override) {
35
49
  console.log(` Mode: ${cyan("default")} ${dim("(using per-tool policies)")}`);
@@ -44,7 +58,8 @@ var main = async () => {
44
58
  return;
45
59
  }
46
60
  if (mode === "clear" || mode === "reset") {
47
- await fetch(`${baseUrl}/api/mcp/mode`, { method: "DELETE", headers });
61
+ const res2 = await fetch(`${baseUrl}/api/mcp/mode`, { method: "DELETE", headers });
62
+ if (!res2.ok) await exitOnFailedResponse(res2, baseUrl);
48
63
  console.log(` ${green("\u2713")} Mode override cleared \u2014 using per-tool policies`);
49
64
  return;
50
65
  }
@@ -53,14 +68,14 @@ var main = async () => {
53
68
  console.log(` Valid modes: ${VALID_MODES.join(", ")}`);
54
69
  console.log(` ${dim("Usage: pushary mode <mode> [--for <duration>]")}`);
55
70
  console.log(` ${dim("Example: pushary mode push_only --for 30m")}`);
56
- return;
71
+ process.exit(EXIT.USAGE);
57
72
  }
58
73
  let ttlSeconds;
59
74
  if (forFlag === "--for" && forValue) {
60
75
  const parsed = parseDuration(forValue);
61
76
  if (!parsed) {
62
77
  console.log(` ${yellow("!")} Invalid duration: ${forValue} ${dim("(use e.g. 30m, 1h)")}`);
63
- return;
78
+ process.exit(EXIT.USAGE);
64
79
  }
65
80
  ttlSeconds = parsed;
66
81
  }
@@ -69,11 +84,7 @@ var main = async () => {
69
84
  headers,
70
85
  body: JSON.stringify({ mode, ttlSeconds })
71
86
  });
72
- if (!res.ok) {
73
- const err = await res.json();
74
- console.log(` ${yellow("!")} Failed: ${err.error ?? res.statusText}`);
75
- return;
76
- }
87
+ if (!res.ok) await exitOnFailedResponse(res, baseUrl);
77
88
  const data = await res.json();
78
89
  console.log(` ${green("\u2713")} Mode set to ${cyan(data.override.mode)}`);
79
90
  if (data.override.expiresAt) {