@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
package/CHANGELOG.md ADDED
@@ -0,0 +1,161 @@
1
+ # Changelog
2
+
3
+ ## 0.62.0
4
+
5
+ ### `pushary clean` could leave your API key on disk
6
+
7
+ If `CODEX_HOME` was set, `clean` looked for Codex's `config.toml` in `~/.codex`
8
+ and never found the real one. It printed `Codex config (not found)`, exited `0`,
9
+ and left the key in the file. A user who ran `clean` to remove their key was told
10
+ it had worked.
11
+
12
+ The same fault ran through the rest of the Codex path. With `CODEX_HOME` set:
13
+
14
+ - `setup` wrote the MCP server, the trust hashes and the notify entry to
15
+ `~/.codex/config.toml` while writing `hooks.json`, `AGENTS.md` and the skill to
16
+ the real `CODEX_HOME`, splitting the install across two directories
17
+ - `setup --dry-run` named the relocated path, so the preview and the real run
18
+ disagreed about where the key would land
19
+ - `doctor` read the relocated config and reported the key missing
20
+ - `logout` treated an exported-but-empty `CODEX_HOME` as a path relative to the
21
+ current directory
22
+
23
+ If you use `CODEX_HOME`, run `pushary clean --dry-run` to see what is still
24
+ there, and check `$CODEX_HOME/config.toml` for a key you thought was gone.
25
+
26
+ ### New
27
+
28
+ - `pushary clean --dry-run` lists everything it would remove and changes nothing
29
+ - `pushary doctor --bundle` writes a redacted diagnostics file for a support
30
+ thread. Keys, your home directory and the machine name are stripped.
31
+ - `pushary logout --revoke` kills the key server-side as well as locally, so it
32
+ stops working everywhere rather than only on this machine
33
+
34
+ ### Changed
35
+
36
+ - `pushary upgrade` now refreshes Codex, Gemini CLI and Cursor, not only Claude
37
+ Code, and names the agents it actually touched. It matters most for Codex,
38
+ whose hooks are trusted by hash: a refreshed hook command without a refreshed
39
+ trust entry is a hook Codex silently declines to run.
40
+ - `pushary upgrade` with no global install now names the install modes it can
41
+ see and the command that updates each, instead of one generic line and exit 0
42
+ - `pushary daemon` stops on a rejected key or a closed plan and exits `4`
43
+ (`UNAUTHENTICATED`). It used to treat that like a network blip, back off to a
44
+ two-minute poll and sit there under an `online` banner, so a revoked key left a
45
+ daemon that looked healthy and would never launch anything again.
46
+ - Ctrl-C during app pairing now cancels the pairing on the server, so a QR still
47
+ on your screen stops being claimable immediately instead of staying live for
48
+ the rest of its five minutes
49
+ - `setup` prints the command surface when it finishes
50
+
51
+ ## 0.61.0
52
+
53
+ The largest release since the CLI shipped. Setup no longer asks you to paste an
54
+ API key, the key is checked against the server before anything is written to your
55
+ machine, and commands exit with codes that mean something.
56
+
57
+ ### Read this first: exit codes changed
58
+
59
+ Commands that used to print a failure and exit `0` now exit non-zero. If you run
60
+ any of these in CI or chained behind `&&`, this is the part that affects you.
61
+
62
+ | Command | Was | Now |
63
+ | --- | --- | --- |
64
+ | any unknown command or `pushary` typo | `0`, printed help | `2` |
65
+ | `doctor` with a failing check | `0` | `5` |
66
+ | `setup` with a revoked key | `0` | `4` |
67
+ | `setup` with an expired or locked plan | `0` | `5` |
68
+ | `mode`, `wait` with a rejected key | `0` | non-zero |
69
+ | `status` with no key configured | did not exist | `3` |
70
+
71
+ The full ladder: `0` ok, `1` failed, `2` usage, `3` not configured,
72
+ `4` unauthenticated, `5` problems found, `6` no device, `7` input required,
73
+ `8` unreachable, `130` interrupted.
74
+
75
+ `pushary doctor && deploy` used to succeed against a machine doctor had just
76
+ declared broken. That is the change worth knowing about.
77
+
78
+ ### Sign in instead of pasting a key
79
+
80
+ `pushary setup` opens a browser, shows you a code, and receives the key sealed to
81
+ a keypair generated on your machine. The key is never pasted, never shown in a
82
+ terminal, and never transits in a form the server could read.
83
+
84
+ - `pushary login` and `pushary logout` as standalone commands
85
+ - Pasting a key still works, as a fallback and via `--key`
86
+ - `--key-stdin` reads the key from stdin, so it stays out of your shell history
87
+ and out of the process list
88
+
89
+ ### New commands
90
+
91
+ - `pushary status` reports what this machine is connected to, with `--json`
92
+ - `pushary login`, `pushary logout`, `pushary connect`
93
+
94
+ ### Setup
95
+
96
+ - The key is verified against the server before a single file is written. A
97
+ revoked key or a locked plan stops the run instead of producing a config that
98
+ looks finished and never delivers.
99
+ - `--yes` runs setup with no prompts, for CI and images
100
+ - `--agents claude_code,codex,...` picks agents without asking
101
+ - `--dry-run` prints what it would do and touches nothing
102
+ - Agent detection is real, so an agent you do not have is reported as skipped
103
+ rather than counted as configured
104
+ - Ctrl-C in a trailing prompt no longer reports the whole run as failed
105
+ - A stale `PUSHARY_API_KEY` export in your shell profile is now replaced rather
106
+ than skipped. It used to win forever over a newly rotated key.
107
+
108
+ ### Delivery is proved to your phone, not just to the workspace
109
+
110
+ Setup and doctor now check that an approval reaches **you**, not merely that
111
+ somebody in the workspace has a device. On a shared site where the only reachable
112
+ channel cannot be attributed to you, that is said plainly rather than reported as
113
+ success.
114
+
115
+ ### Diagnostics
116
+
117
+ - `pushary doctor --json` for one machine-readable envelope
118
+ - `pushary doctor --roundtrip` sends a real approval and waits for the tap. It
119
+ works without a TTY, where the old confirm prompt could not run at all.
120
+ - `pushary doctor --no-push` for a check that stays silent
121
+ - doctor reads the same key the hooks will actually use, honoring
122
+ `PUSHARY_CONFIG_FILE`, and reports a Claude or Cursor config holding a
123
+ different key
124
+
125
+ ### Correctness
126
+
127
+ - `--help` and `--version` print and exit instead of running the command.
128
+ `pushary daemon --help` used to start a daemon; `pushary hook --help` used to
129
+ block forever on stdin.
130
+ - Unknown flags are rejected on the commands that parse them, instead of being
131
+ coerced into a plausible wrong value. `--connect` with an unrecognized mode used
132
+ to silently mean `web`.
133
+ - Every file this CLI writes is written atomically, so an interrupted setup can
134
+ no longer truncate a large `~/.claude.json`
135
+ - An unparseable agent config is left alone instead of being overwritten
136
+ - Files holding your key are created `0600`
137
+ - `CODEX_HOME` is honored when locating `config.toml`
138
+ - Hermes setup works on Windows
139
+ - The Cursor plugin is staged and verified before the old one is replaced
140
+ - `installGlobally` pins the exact running version instead of drifting to latest
141
+ - `pushary clean` removes only the export line it wrote, not every line in your
142
+ shell profile that mentions the key variable
143
+ - Requests honor `HTTP_PROXY` and `HTTPS_PROXY`
144
+ - Color honors `NO_COLOR` and `FORCE_COLOR`, and turns itself off when output is
145
+ not a terminal
146
+ - `--json` output is machine-readable on stdout, with human text on stderr, so a
147
+ pipe never receives a spinner
148
+
149
+ ### Pairing
150
+
151
+ The pairing QR now points at a real web page, so scanning it on a phone without
152
+ the app installed lands on install instructions for that platform instead of a
153
+ dead custom-scheme link.
154
+
155
+ ### Known gaps
156
+
157
+ - `status`, `clean`, `mode`, `wait`, `stats`, `suggestions` and `upgrade` still
158
+ ignore an unknown flag rather than exiting `2`. Unchanged from 0.60.0.
159
+ - `pushary upgrade` reapplies Claude settings only. Other agents need `setup`.
160
+ - Codex hook trust still uses a fixed version ceiling, so a newer Codex is sent
161
+ to the manual `/hooks` step.
package/README.md CHANGED
@@ -58,17 +58,59 @@ Any other MCP client can use notify and ask by pointing at the Pushary MCP endpo
58
58
 
59
59
  ## Commands
60
60
 
61
+ <!-- Managed: every command below is declared in src/cli/commands.ts, and a test
62
+ asserts this list matches it. Do not edit one without the other. -->
63
+
61
64
  ```
62
- setup Configure Claude Code, Codex, Gemini CLI, Hermes, or Cursor with Pushary
63
- doctor Verify your Pushary installation is working
64
- clean Remove all Pushary configuration
65
- mode Switch approval mode (push_only, push_first, terminal_only, notify_only)
66
- wait Show or set the "wait for your phone" ladder (pushary wait 45)
67
- stats Show the approval moments your agents hit while not connected
68
- upgrade Update the globally installed hooks to the latest version
69
- hook Run as a PreToolUse hook (reads stdin, writes stdout)
65
+ setup Configure Claude Code, Codex, Gemini CLI, Hermes, or Cursor with Pushary
66
+ claude Run Claude Code through Pushary, reachable from your phone even when idle
67
+ daemon Keep this machine reachable so your phone can START a new session
68
+ status One screen: which key is in force, whether the server accepts it, what can receive an approval
69
+ login Sign in from this terminal, no key to copy (--with-token to pipe one in)
70
+ logout Remove the key this machine stores, and say what it does not remove
71
+ connect Connect a phone without re-running setup (--app for the Pushary app)
72
+ doctor Verify your Pushary installation is working
73
+ clean Remove all Pushary configuration (--yes for non-interactive)
74
+ mode Switch approval mode (push_only, push_first, terminal_only)
75
+ wait Show or set the "wait for your phone" ladder (pushary wait 45)
76
+ stats Show the approval moments your agents hit while not connected
77
+ suggestions List rules mined from your own approvals
78
+ upgrade Update the globally installed hooks to the latest version
79
+ hook Run as a PreToolUse hook (reads stdin, writes stdout)
70
80
  ```
71
81
 
82
+ Run `pushary <command> --help` for one command's options.
83
+
84
+ ### Non-interactive setup
85
+
86
+ `setup` asks which agents you use, so a run with no terminal on stdin needs to be
87
+ told instead. `--agents` or `--yes` does that; without either, setup takes the
88
+ defaults rather than waiting for a keypress that can never arrive.
89
+
90
+ ```bash
91
+ # Everything detected on this machine, no prompts
92
+ npx @pushary/agent-hooks@latest setup --key-stdin --yes --skip-phone < key.txt
93
+
94
+ # Named agents only
95
+ npx @pushary/agent-hooks@latest setup --agents claude_code,codex --yes --skip-phone
96
+
97
+ # Store the key and configure nothing
98
+ npx @pushary/agent-hooks@latest setup --agents none --yes
99
+
100
+ # Show what would be written, change nothing
101
+ npx @pushary/agent-hooks@latest setup --agents auto --dry-run
102
+
103
+ # One machine-readable object on stdout; every human line goes to stderr
104
+ npx @pushary/agent-hooks@latest setup --agents auto --yes --json
105
+ ```
106
+
107
+ Prefer `--key-stdin` over `--key` in CI: `--key` puts the secret in argv, where
108
+ any other user on the box can read it out of `ps`.
109
+
110
+ Exit codes: `0` done, `2` bad usage, `3` no key resolvable, `4` the server
111
+ rejected the key, `5` finished with problems (an inactive plan, or an installer
112
+ that failed), `130` cancelled with Ctrl-C.
113
+
72
114
  Examples:
73
115
 
74
116
  ```bash
@@ -106,7 +148,8 @@ If a policy cannot be fetched or a hook errors, the decision falls back to the a
106
148
  - Website: [pushary.com](https://pushary.com/?from=npm)
107
149
  - Docs: [pushary.com/docs](https://pushary.com/docs?from=npm)
108
150
  - Sign up: [pushary.com/sign-up](https://pushary.com/sign-up?from=npm)
109
- - Issues: [github.com/pushary/pushary/issues](https://github.com/pushary/pushary/issues)
151
+ - Troubleshooting: [pushary.com/docs/agents/reference/troubleshooting](https://pushary.com/docs/agents/reference/troubleshooting?from=npm)
152
+ - Support and bug reports: [Discord](https://discord.gg/9v3VvUByrr) or [business@pushary.com](mailto:business@pushary.com)
110
153
 
111
154
  ## License
112
155
 
@@ -17,18 +17,18 @@ import {
17
17
  reportEvent,
18
18
  resolvePolicy,
19
19
  waitForAnswer
20
- } from "../chunk-WNRYRN2R.js";
21
- import {
22
- getMachineId
23
- } from "../chunk-RN3NOEJF.js";
24
- import "../chunk-DWED7BS3.js";
20
+ } from "../chunk-HIIBSYFJ.js";
21
+ import "../chunk-HNTKTK5B.js";
25
22
  import {
26
23
  redactSecrets
27
24
  } from "../chunk-DQAN3JQP.js";
25
+ import {
26
+ getMachineId
27
+ } from "../chunk-RN3NOEJF.js";
28
28
  import {
29
29
  getApiKey,
30
30
  getBaseUrl
31
- } from "../chunk-NKXSILEW.js";
31
+ } from "../chunk-2UMNXADU.js";
32
32
 
33
33
  // src/wrapper/claudeBinary.ts
34
34
  import { statSync } from "fs";
@@ -1,28 +1,41 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
- removeClaudeAlias
4
- } from "../chunk-BC3VCZ3E.js";
3
+ removeClaudeMcpServers,
4
+ removePusharySettings
5
+ } from "../chunk-CAJZAFVS.js";
5
6
  import {
6
- removeCodexHooks,
7
- removeGeminiSettings,
8
7
  removeInstructionBlock
9
- } from "../chunk-BQLCELYC.js";
8
+ } from "../chunk-N7XJ4L2W.js";
9
+ import {
10
+ removeGeminiSettings
11
+ } from "../chunk-E2U35RLD.js";
12
+ import {
13
+ codexAgentsMd,
14
+ codexConfigToml,
15
+ codexHooksJson,
16
+ codexSkillDir,
17
+ removeCodexHooks
18
+ } from "../chunk-ER657KRJ.js";
19
+ import {
20
+ removeClaudeAlias
21
+ } from "../chunk-BC3VCZ3E.js";
10
22
  import {
11
23
  execNpm
12
- } from "../chunk-J7JWI3KU.js";
24
+ } from "../chunk-MUEW424A.js";
13
25
  import {
14
- removeClaudeMcpServers,
15
- removePusharySettings
16
- } from "../chunk-CAJZAFVS.js";
26
+ exitOnHelpFlag
27
+ } from "../chunk-IIZZYUWK.js";
28
+ import "../chunk-SP7OZXCQ.js";
17
29
  import "../chunk-DQAN3JQP.js";
18
30
 
19
31
  // bin/pushary-clean.ts
20
- import { existsSync, readFileSync, writeFileSync, rmSync, readdirSync } from "fs";
32
+ import { existsSync, readFileSync, writeFileSync, copyFileSync, rmSync, readdirSync } from "fs";
21
33
  import { join } from "path";
22
34
  import { homedir, tmpdir } from "os";
23
35
  import { execSync } from "child_process";
24
36
  import { confirm } from "@inquirer/prompts";
25
37
  import { parse as parseTOML, stringify as stringifyTOML } from "smol-toml";
38
+ exitOnHelpFlag("clean");
26
39
  var dim = (s) => `\x1B[2m${s}\x1B[0m`;
27
40
  var bold = (s) => `\x1B[1m${s}\x1B[0m`;
28
41
  var green = (s) => `\x1B[32m${s}\x1B[0m`;
@@ -61,8 +74,25 @@ var readJson = (path) => {
61
74
  return null;
62
75
  }
63
76
  };
77
+ var dryRun = false;
78
+ var did = (past) => dryRun ? `(would ${past === "cleaned" ? "clean" : past === "removed" ? "remove" : past === "uninstalled" ? "uninstall" : past})` : `(${past})`;
79
+ var wouldNote = (action, path) => {
80
+ console.log(` ${dim("would")} ${action} ${path}`);
81
+ };
82
+ var guardedWrite = (path, contents) => {
83
+ if (dryRun) return wouldNote("rewrite", path);
84
+ writeFileSync(path, contents, "utf-8");
85
+ };
86
+ var guardedRemove = (path, options = {}) => {
87
+ if (dryRun) return wouldNote("remove ", path);
88
+ rmSync(path, { recursive: true, force: options.force });
89
+ };
90
+ var guardedExec = (command, options, describe) => {
91
+ if (dryRun) return wouldNote("run ", describe);
92
+ execSync(command, options);
93
+ };
64
94
  var writeJson = (path, data) => {
65
- writeFileSync(path, JSON.stringify(data, null, 2) + "\n", "utf-8");
95
+ guardedWrite(path, JSON.stringify(data, null, 2) + "\n");
66
96
  };
67
97
  var cleanSettingsFile = (path, label) => {
68
98
  const data = readJson(path);
@@ -73,7 +103,7 @@ var cleanSettingsFile = (path, label) => {
73
103
  const changed = removePusharySettings(data);
74
104
  if (changed) {
75
105
  writeJson(path, data);
76
- console.log(` ${check} ${label} ${dim("(cleaned)")}`);
106
+ console.log(` ${check} ${label} ${dim(did("cleaned"))}`);
77
107
  } else {
78
108
  console.log(` ${skip} ${label} ${dim("(no pushary entries)")}`);
79
109
  }
@@ -83,7 +113,12 @@ var main = async () => {
83
113
  console.log(` ${bold("Pushary Clean")}`);
84
114
  console.log(` ${dim("Removes all Pushary configuration")}`);
85
115
  console.log();
86
- const assumeYes = process.argv.includes("--yes") || process.argv.includes("-y");
116
+ dryRun = process.argv.includes("--dry-run");
117
+ if (dryRun) {
118
+ console.log(` ${bold("Dry run")} ${dim("- nothing will be changed")}`);
119
+ console.log();
120
+ }
121
+ const assumeYes = dryRun || process.argv.includes("--yes") || process.argv.includes("-y");
87
122
  if (!assumeYes && !process.stdin.isTTY) {
88
123
  console.log(` ${yellow("!")} Non-interactive shell. Re-run with --yes to confirm removal.`);
89
124
  process.exit(1);
@@ -113,7 +148,7 @@ var main = async () => {
113
148
  if (mcpServers?.pushary) {
114
149
  delete mcpServers.pushary;
115
150
  writeJson(CURSOR_MCP, cursorData);
116
- console.log(` ${check} Cursor MCP config ${dim("(cleaned)")}`);
151
+ console.log(` ${check} Cursor MCP config ${dim(did("cleaned"))}`);
117
152
  } else {
118
153
  console.log(` ${skip} Cursor MCP config ${dim("(no pushary entries)")}`);
119
154
  }
@@ -121,7 +156,7 @@ var main = async () => {
121
156
  console.log(` ${skip} Cursor MCP config ${dim("(not found)")}`);
122
157
  }
123
158
  if (existsSync(CURSOR_PLUGIN_DIR)) {
124
- rmSync(CURSOR_PLUGIN_DIR, { recursive: true });
159
+ guardedRemove(CURSOR_PLUGIN_DIR);
125
160
  console.log(` ${check} Cursor plugin ${dim("(removed from ~/.cursor/plugins/local)")}`);
126
161
  } else {
127
162
  console.log(` ${skip} Cursor plugin ${dim("(not installed)")}`);
@@ -144,12 +179,12 @@ var main = async () => {
144
179
  console.log(` ${skip} Cursor gate ${dim("(no hooks.json)")}`);
145
180
  }
146
181
  if (existsSync(SKILL_DIR)) {
147
- rmSync(SKILL_DIR, { recursive: true });
148
- console.log(` ${check} Skill directory ${dim("(removed)")}`);
182
+ guardedRemove(SKILL_DIR);
183
+ console.log(` ${check} Skill directory ${dim(did("removed"))}`);
149
184
  } else {
150
185
  console.log(` ${skip} Skill directory ${dim("(not found)")}`);
151
186
  }
152
- const codexConfig = join(homedir(), ".codex", "config.toml");
187
+ const codexConfig = codexConfigToml();
153
188
  try {
154
189
  const config = readFileSync(codexConfig, "utf-8");
155
190
  const hadPushary = config.includes("pushary");
@@ -163,19 +198,19 @@ var main = async () => {
163
198
  parsed.notify = parsed.notify.filter((n) => typeof n !== "string" || !n.includes("pushary-codex"));
164
199
  if (parsed.notify.length === 0) delete parsed.notify;
165
200
  }
166
- writeFileSync(codexConfig, stringifyTOML(parsed), "utf-8");
167
- console.log(` ${check} Codex config ${dim("(cleaned)")}`);
201
+ guardedWrite(codexConfig, stringifyTOML(parsed));
202
+ console.log(` ${check} Codex config ${dim(did("cleaned"))}`);
168
203
  } else {
169
204
  console.log(` ${skip} Codex config ${dim("(no pushary entries)")}`);
170
205
  }
171
206
  } catch {
172
207
  console.log(` ${skip} Codex config ${dim("(not found)")}`);
173
208
  }
174
- const codexHooksJson = join(homedir(), ".codex", "hooks.json");
175
- const codexHooksData = readJson(codexHooksJson);
209
+ const codexHooksJson2 = codexHooksJson();
210
+ const codexHooksData = readJson(codexHooksJson2);
176
211
  if (codexHooksData) {
177
212
  if (removeCodexHooks(codexHooksData)) {
178
- writeJson(codexHooksJson, codexHooksData);
213
+ writeJson(codexHooksJson2, codexHooksData);
179
214
  console.log(` ${check} Codex hooks ${dim("(removed from ~/.codex/hooks.json)")}`);
180
215
  } else {
181
216
  console.log(` ${skip} Codex hooks ${dim("(no pushary entries)")}`);
@@ -183,16 +218,16 @@ var main = async () => {
183
218
  } else {
184
219
  console.log(` ${skip} Codex hooks ${dim("(not found)")}`);
185
220
  }
186
- const codexAgentsMd = join(homedir(), ".codex", "AGENTS.md");
187
- if (removeInstructionBlock(codexAgentsMd)) {
221
+ const codexAgentsMd2 = codexAgentsMd();
222
+ if (removeInstructionBlock(codexAgentsMd2)) {
188
223
  console.log(` ${check} Codex AGENTS.md ${dim("(removed Pushary block)")}`);
189
224
  } else {
190
225
  console.log(` ${skip} Codex AGENTS.md ${dim("(no pushary block)")}`);
191
226
  }
192
- const codexSkillDir = join(homedir(), ".codex", "skills", "pushary");
193
- if (existsSync(codexSkillDir)) {
194
- rmSync(codexSkillDir, { recursive: true });
195
- console.log(` ${check} Codex skill directory ${dim("(removed)")}`);
227
+ const codexSkillDir2 = codexSkillDir();
228
+ if (existsSync(codexSkillDir2)) {
229
+ guardedRemove(codexSkillDir2);
230
+ console.log(` ${check} Codex skill directory ${dim(did("removed"))}`);
196
231
  } else {
197
232
  console.log(` ${skip} Codex skill directory ${dim("(not found)")}`);
198
233
  }
@@ -201,7 +236,7 @@ var main = async () => {
201
236
  if (geminiSettings) {
202
237
  if (removeGeminiSettings(geminiSettings)) {
203
238
  writeJson(geminiSettingsPath, geminiSettings);
204
- console.log(` ${check} Gemini CLI settings ${dim("(cleaned)")}`);
239
+ console.log(` ${check} Gemini CLI settings ${dim(did("cleaned"))}`);
205
240
  } else {
206
241
  console.log(` ${skip} Gemini CLI settings ${dim("(no pushary entries)")}`);
207
242
  }
@@ -218,13 +253,13 @@ var main = async () => {
218
253
  if (hermesPython) {
219
254
  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"] = [x for x in e if x != "pushary"]; 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"] = [x for x in d if x != "clarify"]; c["agent"] = a; save_config(c)';
220
255
  try {
221
- execSync(`"${hermesPython}" -c '${snippet}'`, { stdio: "pipe", timeout: 15e3 });
256
+ guardedExec(`"${hermesPython}" -c '${snippet}'`, { stdio: "pipe", timeout: 15e3 }, "hermes plugin config update");
222
257
  console.log(` ${check} Hermes config ${dim("(plugin disabled, clarify toolset restored)")}`);
223
258
  } catch {
224
259
  console.log(` ${skip} Hermes config ${dim("(could not update config.yaml)")}`);
225
260
  }
226
261
  try {
227
- execSync(`"${hermesPython}" -m pip uninstall -y hermes-plugin-pushary`, { stdio: "pipe", timeout: 6e4 });
262
+ guardedExec(`"${hermesPython}" -m pip uninstall -y hermes-plugin-pushary`, { stdio: "pipe", timeout: 6e4 }, "pip uninstall hermes-plugin-pushary");
228
263
  console.log(` ${check} Hermes plugin ${dim("(pip uninstalled)")}`);
229
264
  } catch {
230
265
  console.log(` ${skip} Hermes plugin ${dim("(not installed)")}`);
@@ -236,9 +271,19 @@ var main = async () => {
236
271
  try {
237
272
  const content = readFileSync(shellFile, "utf-8");
238
273
  if (content.includes("PUSHARY_API_KEY")) {
239
- const cleaned = content.split("\n").filter((l) => !l.includes("PUSHARY_API_KEY")).join("\n");
240
- writeFileSync(shellFile, cleaned, "utf-8");
241
- console.log(` ${check} ${shellFile.split("/").pop()} ${dim("(removed API key)")}`);
274
+ const isOurExport = (line) => /^\s*export\s+PUSHARY_API_KEY=['"]?pk_[A-Za-z0-9]+\.[A-Za-z0-9]+['"]?\s*$/.test(line);
275
+ const lines = content.split("\n");
276
+ const declined = lines.filter((l) => l.includes("PUSHARY_API_KEY") && !isOurExport(l));
277
+ const cleaned = lines.filter((l) => !isOurExport(l)).join("\n");
278
+ if (cleaned !== content) {
279
+ const backup = `${shellFile}.pushary-backup-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}`;
280
+ if (!dryRun) copyFileSync(shellFile, backup);
281
+ guardedWrite(shellFile, cleaned);
282
+ console.log(` ${check} ${shellFile.split("/").pop()} ${dim(`(removed the export, backup at ${backup})`)}`);
283
+ }
284
+ for (const line of declined) {
285
+ console.log(` ${skip} ${shellFile.split("/").pop()} ${dim(`left a line referencing PUSHARY_API_KEY: ${line.trim()}`)}`);
286
+ }
242
287
  }
243
288
  } catch {
244
289
  }
@@ -250,7 +295,7 @@ var main = async () => {
250
295
  console.log(` ${skip} claude alias ${dim("(not set)")}`);
251
296
  }
252
297
  if (existsSync(PUSHARY_DIR)) {
253
- rmSync(PUSHARY_DIR, { recursive: true });
298
+ guardedRemove(PUSHARY_DIR);
254
299
  console.log(` ${check} Local state ${dim("(~/.pushary removed: stored API key, ledger)")}`);
255
300
  } else {
256
301
  console.log(` ${skip} Local state ${dim("(~/.pushary not found)")}`);
@@ -260,7 +305,7 @@ var main = async () => {
260
305
  for (const f of readdirSync(tmpdir())) {
261
306
  if (!f.startsWith("pushary-")) continue;
262
307
  try {
263
- rmSync(join(tmpdir(), f), { recursive: true, force: true });
308
+ guardedRemove(join(tmpdir(), f), { force: true });
264
309
  swept++;
265
310
  } catch {
266
311
  }
@@ -270,7 +315,7 @@ var main = async () => {
270
315
  }
271
316
  try {
272
317
  execNpm("uninstall -g --no-workspaces @pushary/agent-hooks", { stdio: "ignore", timeout: 3e4 });
273
- console.log(` ${check} Global package ${dim("(uninstalled)")}`);
318
+ console.log(` ${check} Global package ${dim(did("uninstalled"))}`);
274
319
  } catch {
275
320
  console.log(` ${skip} Global package ${dim("(not installed)")}`);
276
321
  }
@@ -31,24 +31,24 @@ import {
31
31
  toCodexWire,
32
32
  toPolicyLookup,
33
33
  waitForAnswer
34
- } from "../chunk-WNRYRN2R.js";
35
- import {
36
- getMachineId
37
- } from "../chunk-RN3NOEJF.js";
34
+ } from "../chunk-HIIBSYFJ.js";
38
35
  import {
39
36
  isGatingMoment,
40
37
  recordKeylessMoment
41
38
  } from "../chunk-WLGEY4UX.js";
42
- import "../chunk-DWED7BS3.js";
39
+ import "../chunk-HNTKTK5B.js";
43
40
  import {
44
41
  DECISION_LINE_MAX,
45
42
  effectiveWaitSeconds,
46
43
  hookWaitClamped,
47
44
  hookWaitDeadline
48
45
  } from "../chunk-DQAN3JQP.js";
46
+ import {
47
+ getMachineId
48
+ } from "../chunk-RN3NOEJF.js";
49
49
  import {
50
50
  getApiKey
51
- } from "../chunk-NKXSILEW.js";
51
+ } from "../chunk-2UMNXADU.js";
52
52
 
53
53
  // bin/pushary-codex-hook.ts
54
54
  import { basename, join } from "path";
@@ -3,15 +3,15 @@ import {
3
3
  askUser,
4
4
  reportEvent,
5
5
  waitForAnswer
6
- } from "../chunk-WNRYRN2R.js";
6
+ } from "../chunk-HIIBSYFJ.js";
7
+ import "../chunk-HNTKTK5B.js";
8
+ import "../chunk-DQAN3JQP.js";
7
9
  import {
8
10
  getMachineId
9
11
  } from "../chunk-RN3NOEJF.js";
10
- import "../chunk-DWED7BS3.js";
11
- import "../chunk-DQAN3JQP.js";
12
12
  import {
13
13
  getApiKey
14
- } from "../chunk-NKXSILEW.js";
14
+ } from "../chunk-2UMNXADU.js";
15
15
 
16
16
  // bin/pushary-codex.ts
17
17
  import { basename } from "path";
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node
@@ -0,0 +1,92 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ printKeyCheck
4
+ } from "../chunk-ONVEYEVR.js";
5
+ import {
6
+ confirmAppConnection,
7
+ connectDevice,
8
+ printConnectInstructions
9
+ } from "../chunk-3O27ZSRE.js";
10
+ import "../chunk-3EGEA4KH.js";
11
+ import {
12
+ parseFlags
13
+ } from "../chunk-N4DA4CJB.js";
14
+ import {
15
+ readKeySource
16
+ } from "../chunk-247C4RE5.js";
17
+ import {
18
+ checkApiKey
19
+ } from "../chunk-YJ7YZRVV.js";
20
+ import {
21
+ createIo
22
+ } from "../chunk-TX7KBKT7.js";
23
+ import {
24
+ UsageError
25
+ } from "../chunk-7QLSKOSU.js";
26
+ import "../chunk-BC3VCZ3E.js";
27
+ import {
28
+ exitOnHelpFlag
29
+ } from "../chunk-IIZZYUWK.js";
30
+ import {
31
+ EXIT
32
+ } from "../chunk-SP7OZXCQ.js";
33
+ import "../chunk-HNTKTK5B.js";
34
+ import "../chunk-DQAN3JQP.js";
35
+ import "../chunk-2UMNXADU.js";
36
+
37
+ // bin/pushary-connect.ts
38
+ exitOnHelpFlag("connect");
39
+ var parseConnectOptions = (argv) => {
40
+ const flags = parseFlags(argv, { bools: ["app", "browser", "help", "h"], values: [] });
41
+ if (flags.command !== void 0 && flags.command !== "connect") {
42
+ throw new UsageError(`connect takes no positional arguments (got ${flags.command})`);
43
+ }
44
+ if (flags.bools.has("app") && flags.bools.has("browser")) {
45
+ throw new UsageError("pass either --app or --browser, not both");
46
+ }
47
+ return { mode: flags.bools.has("app") ? "app" : "browser" };
48
+ };
49
+ var options;
50
+ try {
51
+ options = parseConnectOptions(process.argv.slice(2));
52
+ } catch (err) {
53
+ process.stderr.write(` ! ${err instanceof Error ? err.message : String(err)}
54
+ `);
55
+ process.stderr.write(" Run `pushary connect --help` for the full list.\n");
56
+ process.exit(EXIT.USAGE);
57
+ }
58
+ var { bold, dim, cyan, yellow } = createIo();
59
+ var main = async () => {
60
+ const source = readKeySource();
61
+ if (!source.key) {
62
+ console.log();
63
+ console.log(` ${yellow("!")} No API key on this machine, so there is no site to connect a phone to.`);
64
+ console.log(` ${dim("Run")} ${cyan("pushary setup")} ${dim("first.")}`);
65
+ console.log();
66
+ process.exit(EXIT.NOT_CONFIGURED);
67
+ }
68
+ console.log();
69
+ console.log(` ${bold("Pushary")} ${dim("connect a phone")}`);
70
+ console.log();
71
+ const keyCheck = await checkApiKey(source.key);
72
+ if (keyCheck.kind !== "ok") {
73
+ const report = printKeyCheck(keyCheck);
74
+ if (keyCheck.kind !== "unreachable") process.exit(report.exitCode);
75
+ console.log();
76
+ }
77
+ const result = options.mode === "app" ? await confirmAppConnection(source.key) : process.stdout.isTTY ? await connectDevice(source.key, { showCapabilities: true }) : await printConnectInstructions(source.key);
78
+ console.log();
79
+ if (result.reachesYou) {
80
+ console.log(` ${dim("Your phone can receive approvals. Try")} ${cyan("pushary doctor --roundtrip")} ${dim("to answer one.")}`);
81
+ console.log();
82
+ process.exit(EXIT.OK);
83
+ }
84
+ console.log(` ${dim("No device of yours can answer yet. Run")} ${cyan("pushary connect")} ${dim("again when you are ready.")}`);
85
+ console.log();
86
+ process.exit(EXIT.NO_DEVICE);
87
+ };
88
+ main().catch((err) => {
89
+ process.stderr.write(` ! connect failed: ${err instanceof Error ? err.message : String(err)}
90
+ `);
91
+ process.exit(EXIT.FAILED);
92
+ });