@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
@@ -1,65 +1,47 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ COMMANDS,
4
+ EXIT,
5
+ getPackageVersion,
6
+ isCommandName,
7
+ renderHelp
8
+ } from "../chunk-SP7OZXCQ.js";
2
9
 
3
10
  // bin/pushary.ts
4
- var command = process.argv[2];
5
- if (command === "setup") {
6
- await import("./pushary-setup.js");
7
- } else if (command === "hook") {
8
- await import("./pushary-hook.js");
9
- } else if (command === "clean") {
10
- await import("./pushary-clean.js");
11
- } else if (command === "doctor") {
12
- await import("./pushary-doctor.js");
13
- } else if (command === "mode") {
14
- await import("./pushary-mode.js");
15
- } else if (command === "wait") {
16
- await import("./pushary-wait.js");
17
- } else if (command === "stats") {
18
- await import("./pushary-stats.js");
19
- } else if (command === "suggestions") {
20
- await import("./pushary-suggestions.js");
21
- } else if (command === "upgrade") {
22
- await import("./pushary-upgrade.js");
23
- } else if (command === "claude") {
24
- await import("./pushary-claude.js");
25
- } else if (command === "daemon") {
26
- await import("./pushary-daemon.js");
27
- } else {
28
- console.log(`
29
- Pushary Agent Hooks
30
-
31
- Commands:
32
- setup Configure Claude Code, Codex, Gemini CLI, Hermes, or Cursor with Pushary
33
- claude Run Claude Code through Pushary \u2014 the native terminal, and reachable from
34
- your phone: send an instruction and it drives even a fully idle agent
35
- (press Ctrl-] to take the terminal back). Add --remote to start headless.
36
- daemon Keep this machine reachable to START a new session from your phone, even
37
- with nothing running. Leave it in a project dir; it launches a headless
38
- 'pushary claude --remote' when you send a prompt from the app.
39
- doctor Verify your Pushary installation is working
40
- clean Remove all Pushary configuration (--yes for non-interactive)
41
- mode Switch approval mode (push_only, push_first, terminal_only)
42
- wait Show or set the "wait for your phone" ladder (pushary wait 45)
43
- stats Show the approval moments your agents hit while not connected
44
- suggestions List rules mined from your own approvals; accept one with
45
- 'pushary suggestions accept <id>' to always-allow that pattern
46
- upgrade Update the globally installed hooks to the latest version
47
- hook Run as a PreToolUse hook (reads stdin, writes stdout)
48
-
49
- Setup options:
50
- --key <pk_xxx.xxx> Use this API key instead of prompting for one
51
- --connect web Connect your phone via the PWA QR code (default)
52
- --connect app Pair the native Pushary app, no key to paste
53
- --skip-phone Skip the phone connect step (CI or non-interactive shells)
54
-
55
- Usage:
56
- npx @pushary/agent-hooks@latest setup
57
- npx @pushary/agent-hooks@latest setup --key pk_xxx.xxx --skip-phone
58
- npx @pushary/agent-hooks@latest doctor
59
- npx @pushary/agent-hooks@latest mode push_only --for 30m
60
- npx @pushary/agent-hooks@latest wait 45
61
- pushary claude # native terminal, reachable from your phone when idle
62
- pushary claude --remote -p "start the refactor" # start headless, drive entirely from your phone
63
- pushary daemon # keep this machine ready to spawn a session from your phone
11
+ var LOADERS = {
12
+ setup: () => import("./pushary-setup.js"),
13
+ claude: () => import("./pushary-claude.js"),
14
+ daemon: () => import("./pushary-daemon.js"),
15
+ status: () => import("./pushary-status.js"),
16
+ login: () => import("./pushary-login.js"),
17
+ logout: () => import("./pushary-logout.js"),
18
+ connect: () => import("./pushary-connect.js"),
19
+ doctor: () => import("./pushary-doctor.js"),
20
+ clean: () => import("./pushary-clean.js"),
21
+ mode: () => import("./pushary-mode.js"),
22
+ wait: () => import("./pushary-wait.js"),
23
+ stats: () => import("./pushary-stats.js"),
24
+ suggestions: () => import("./pushary-suggestions.js"),
25
+ upgrade: () => import("./pushary-upgrade.js"),
26
+ hook: () => import("./pushary-hook.js")
27
+ };
28
+ var argv = process.argv.slice(2);
29
+ var command = argv[0] ?? "";
30
+ var wants = (...names) => argv.some((arg) => names.includes(arg));
31
+ if (!command || wants("--help", "-h") && !isCommandName(command)) {
32
+ process.stdout.write(renderHelp(getPackageVersion()));
33
+ process.exit(EXIT.OK);
34
+ }
35
+ if (wants("--version", "-v") && !isCommandName(command)) {
36
+ process.stdout.write(`${getPackageVersion()}
37
+ `);
38
+ process.exit(EXIT.OK);
39
+ }
40
+ if (!isCommandName(command)) {
41
+ process.stderr.write(`Unknown command: ${command}
42
+ `);
43
+ process.stderr.write(`Run \`pushary --help\`. Available: ${COMMANDS.map((c) => c.name).join(", ")}
64
44
  `);
45
+ process.exit(EXIT.USAGE);
65
46
  }
47
+ await LOADERS[command]();
@@ -0,0 +1,236 @@
1
+ import {
2
+ resolveShellRc
3
+ } from "./chunk-BC3VCZ3E.js";
4
+ import {
5
+ configFilePath
6
+ } from "./chunk-2UMNXADU.js";
7
+
8
+ // src/cli/fsx.ts
9
+ import { copyFileSync, existsSync, fsyncSync, mkdirSync, openSync, closeSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "fs";
10
+ import { dirname, join } from "path";
11
+ var readJsonSafe = (filePath) => {
12
+ let raw;
13
+ try {
14
+ raw = readFileSync(filePath, "utf-8");
15
+ } catch (err) {
16
+ const error = err;
17
+ if (error.code === "ENOENT") return { kind: "missing" };
18
+ return { kind: "unreadable", error };
19
+ }
20
+ if (raw.trim() === "") return { kind: "ok", value: {} };
21
+ try {
22
+ return { kind: "ok", value: JSON.parse(raw) };
23
+ } catch (err) {
24
+ return { kind: "unparseable", error: err, raw };
25
+ }
26
+ };
27
+ var stamp = () => (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
28
+ var backupFile = (filePath) => {
29
+ const target = `${filePath}.pushary-backup-${stamp()}`;
30
+ copyFileSync(filePath, target);
31
+ return target;
32
+ };
33
+ var writeFileAtomic = (filePath, contents, mode) => {
34
+ const dir = dirname(filePath);
35
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
36
+ const temp = join(dir, `.${process.pid}-${stamp()}.pushary-tmp`);
37
+ try {
38
+ writeFileSync(temp, contents, mode === void 0 ? "utf-8" : { encoding: "utf-8", mode });
39
+ const handle = openSync(temp, "r+");
40
+ try {
41
+ fsyncSync(handle);
42
+ } finally {
43
+ closeSync(handle);
44
+ }
45
+ renameSync(temp, filePath);
46
+ } catch (err) {
47
+ try {
48
+ if (existsSync(temp)) unlinkSync(temp);
49
+ } catch {
50
+ }
51
+ throw err;
52
+ }
53
+ };
54
+ var writeJsonAtomic = (filePath, data, mode) => writeFileAtomic(filePath, `${JSON.stringify(data, null, 2)}
55
+ `, mode);
56
+
57
+ // src/keystore.ts
58
+ import { chmodSync, existsSync as existsSync2, readFileSync as readFileSync2, realpathSync, writeFileSync as writeFileSync2 } from "fs";
59
+ import { homedir } from "os";
60
+ import { join as join2 } from "path";
61
+ var KEY_FILE_MODE = 384;
62
+ var FALLBACK_RCS = [".zshrc", ".zprofile", ".bashrc", ".bash_profile"];
63
+ var OUR_EXPORT = /^[ \t]*export[ \t]+PUSHARY_API_KEY=(['"]?)(pk_[a-f0-9]+\.[a-f0-9]+)\1[ \t]*$/;
64
+ var MENTIONS_KEY = /PUSHARY_API_KEY/;
65
+ var isComment = (line) => line.trimStart().startsWith("#");
66
+ var exportLine = (apiKey) => `export PUSHARY_API_KEY='${apiKey}'`;
67
+ var keyPrefix = (key) => key.split(".")[0] ?? key;
68
+ var rcResult = (kind, over = {}) => ({
69
+ kind,
70
+ path: null,
71
+ shell: null,
72
+ previousKeyPrefix: null,
73
+ declinedLine: null,
74
+ backupPath: null,
75
+ detail: null,
76
+ ...over
77
+ });
78
+ var realPathOf = (filePath) => {
79
+ try {
80
+ return realpathSync(filePath);
81
+ } catch {
82
+ return filePath;
83
+ }
84
+ };
85
+ var rewriteRcExport = (rcPath, apiKey, shell = null) => {
86
+ const target = realPathOf(rcPath);
87
+ let content = "";
88
+ try {
89
+ content = readFileSync2(target, "utf-8");
90
+ } catch (err) {
91
+ const error = err;
92
+ if (error.code !== "ENOENT") {
93
+ return rcResult("unavailable", { path: target, shell, detail: error.message });
94
+ }
95
+ }
96
+ const lines = content.split("\n");
97
+ const foreign = lines.findIndex((line) => !isComment(line) && MENTIONS_KEY.test(line) && !OUR_EXPORT.test(line));
98
+ if (foreign >= 0) {
99
+ const match = /pk_[a-f0-9]+/.exec(lines[foreign] ?? "");
100
+ return rcResult("declined", {
101
+ path: target,
102
+ shell,
103
+ declinedLine: foreign + 1,
104
+ previousKeyPrefix: match?.[0] ?? null
105
+ });
106
+ }
107
+ const ours = lines.map((line, index) => ({ line, index })).filter((entry) => OUR_EXPORT.test(entry.line));
108
+ if (ours.length > 0) {
109
+ const previous = OUR_EXPORT.exec(ours[ours.length - 1].line)?.[2] ?? null;
110
+ if (ours.every((entry) => OUR_EXPORT.exec(entry.line)?.[2] === apiKey)) {
111
+ return rcResult("unchanged", {
112
+ path: target,
113
+ shell,
114
+ previousKeyPrefix: previous ? keyPrefix(previous) : null
115
+ });
116
+ }
117
+ const next = [...lines];
118
+ for (const entry of ours) next[entry.index] = exportLine(apiKey);
119
+ try {
120
+ const backupPath = backupFile(target);
121
+ writeFileAtomic(target, next.join("\n"), KEY_FILE_MODE);
122
+ return rcResult("updated", {
123
+ path: target,
124
+ shell,
125
+ previousKeyPrefix: previous ? keyPrefix(previous) : null,
126
+ backupPath
127
+ });
128
+ } catch (err) {
129
+ return rcResult("unavailable", { path: target, shell, detail: err.message });
130
+ }
131
+ }
132
+ const gap = content.length === 0 ? "" : content.endsWith("\n") ? "" : "\n";
133
+ try {
134
+ writeFileAtomic(target, `${content}${gap}${exportLine(apiKey)}
135
+ `, KEY_FILE_MODE);
136
+ } catch (err) {
137
+ return rcResult("unavailable", { path: target, shell, detail: err.message });
138
+ }
139
+ return rcResult("written", { path: target, shell });
140
+ };
141
+ var resolveRcTarget = () => {
142
+ const resolved = resolveShellRc();
143
+ if (resolved) return { path: resolved.path, shell: resolved.shell };
144
+ const fallback = FALLBACK_RCS.map((name) => join2(homedir(), name)).find(existsSync2);
145
+ return fallback ? { path: fallback, shell: null } : null;
146
+ };
147
+ var writeKey = (apiKey, options = {}) => {
148
+ const configPath = configFilePath();
149
+ const existing = readJsonSafe(configPath);
150
+ let current = {};
151
+ let configBackupPath = null;
152
+ if (existing.kind === "unreadable") {
153
+ throw new Error(
154
+ `${configPath} exists but could not be read (${existing.error.message}). Fix its permissions or remove it, then re-run setup. Nothing was changed.`
155
+ );
156
+ }
157
+ if (existing.kind === "ok") {
158
+ current = existing.value;
159
+ } else if (existing.kind === "unparseable") {
160
+ configBackupPath = backupFile(configPath);
161
+ }
162
+ writeJsonAtomic(configPath, { ...current, apiKey }, KEY_FILE_MODE);
163
+ restrictFile(configPath);
164
+ const target = (options.resolveRc ?? resolveRcTarget)();
165
+ const rc = target ? rewriteRcExport(target.path, apiKey, target.shell) : rcResult("no-rc");
166
+ process.env.PUSHARY_API_KEY = apiKey;
167
+ return { configPath, rc, configBackupPath };
168
+ };
169
+ var clearKey = (options = {}) => {
170
+ const configPath = configFilePath();
171
+ const existing = readJsonSafe(configPath);
172
+ let removedFromConfig = false;
173
+ if (existing.kind === "ok" && typeof existing.value.apiKey === "string") {
174
+ const { apiKey: _dropped, ...rest } = existing.value;
175
+ writeJsonAtomic(configPath, rest, KEY_FILE_MODE);
176
+ restrictFile(configPath);
177
+ removedFromConfig = true;
178
+ }
179
+ const target = (options.resolveRc ?? resolveRcTarget)();
180
+ let rcLinesDisabled = 0;
181
+ if (target) {
182
+ try {
183
+ const real = realpathSync(target.path);
184
+ const lines = readFileSync2(real, "utf8").split("\n");
185
+ const next = lines.map((line) => {
186
+ if (!OUR_EXPORT.test(line)) return line;
187
+ rcLinesDisabled += 1;
188
+ return `# ${line.trim()} # removed by pushary logout`;
189
+ });
190
+ if (rcLinesDisabled > 0) writeFileSync2(real, next.join("\n"));
191
+ } catch {
192
+ rcLinesDisabled = 0;
193
+ }
194
+ }
195
+ delete process.env.PUSHARY_API_KEY;
196
+ return {
197
+ removedFromConfig,
198
+ configPath,
199
+ rcPath: target?.path ?? null,
200
+ rcLinesDisabled
201
+ };
202
+ };
203
+ var restrictFile = (filePath) => {
204
+ try {
205
+ chmodSync(filePath, KEY_FILE_MODE);
206
+ } catch {
207
+ }
208
+ };
209
+ var readConfigKey = (configPath) => {
210
+ const result = readJsonSafe(configPath);
211
+ if (result.kind !== "ok") return null;
212
+ const key = typeof result.value.apiKey === "string" ? result.value.apiKey.trim() : "";
213
+ return key || null;
214
+ };
215
+ var readKeySource = () => {
216
+ const configPath = configFilePath();
217
+ const envKey = process.env.PUSHARY_API_KEY?.trim() || null;
218
+ const fileKey = readConfigKey(configPath);
219
+ const drift = envKey && fileKey && envKey !== fileKey ? { envPrefix: keyPrefix(envKey), configPrefix: keyPrefix(fileKey), configPath } : null;
220
+ if (envKey) return { kind: "env", key: envKey, origin: "PUSHARY_API_KEY", drift };
221
+ if (fileKey) return { kind: "config", key: fileKey, origin: configPath, drift: null };
222
+ return { kind: "none", key: null, origin: null, drift: null };
223
+ };
224
+
225
+ export {
226
+ readJsonSafe,
227
+ backupFile,
228
+ writeFileAtomic,
229
+ writeJsonAtomic,
230
+ KEY_FILE_MODE,
231
+ exportLine,
232
+ keyPrefix,
233
+ writeKey,
234
+ clearKey,
235
+ readKeySource
236
+ };
@@ -12,18 +12,34 @@ var readKeyFromConfigFile = () => {
12
12
  return void 0;
13
13
  }
14
14
  };
15
+ var driftChecked = false;
16
+ var warnOnKeyDrift = (envKey) => {
17
+ if (driftChecked || process.env.PUSHARY_SUPPRESS_KEY_DRIFT_WARNING === "1") return;
18
+ driftChecked = true;
19
+ const fileKey = readKeyFromConfigFile();
20
+ if (!fileKey || fileKey === envKey) return;
21
+ const short = (key) => key.split(".")[0] ?? key;
22
+ process.stderr.write(
23
+ `[pushary] PUSHARY_API_KEY in your environment (${short(envKey)}) differs from ${configFilePath()} (${short(fileKey)}). The environment one is being used. Run 'pushary doctor' for details, or set PUSHARY_SUPPRESS_KEY_DRIFT_WARNING=1 if this is deliberate.
24
+ `
25
+ );
26
+ };
15
27
  var getApiKey = () => {
16
28
  const envKey = process.env.PUSHARY_API_KEY?.trim();
17
- if (envKey) return envKey;
29
+ if (envKey) {
30
+ warnOnKeyDrift(envKey);
31
+ return envKey;
32
+ }
18
33
  const fileKey = readKeyFromConfigFile();
19
34
  if (fileKey) return fileKey;
20
35
  throw new Error(
21
36
  "PUSHARY_API_KEY is not set and no key was found in ~/.pushary/config.json. Run `npx @pushary/agent-hooks setup`, or get your API key at https://pushary.com/sign-up?from=ai-coding"
22
37
  );
23
38
  };
24
- var getBaseUrl = () => process.env.PUSHARY_BASE_URL ?? "https://pushary.com";
39
+ var getBaseUrl = () => process.env.PUSHARY_BASE_URL?.trim() || process.env.PUSHARY_API_URL?.trim() || "https://pushary.com";
25
40
 
26
41
  export {
42
+ configFilePath,
27
43
  getApiKey,
28
44
  getBaseUrl
29
45
  };
@@ -0,0 +1,44 @@
1
+ // src/pairing.ts
2
+ import crypto from "crypto";
3
+ var X25519_SPKI_PREFIX = Buffer.from("302a300506032b656e032100", "hex");
4
+ var HKDF_INFO = Buffer.from("pushary-pair-v1");
5
+ var HEADER_BYTES = 32 + 12 + 16;
6
+ var rawToPublicKey = (raw) => crypto.createPublicKey({
7
+ key: Buffer.concat([X25519_SPKI_PREFIX, raw]),
8
+ format: "der",
9
+ type: "spki"
10
+ });
11
+ var publicKeyToRaw = (key) => key.export({ format: "der", type: "spki" }).subarray(-32);
12
+ var generateKeypair = () => {
13
+ const { publicKey, privateKey } = crypto.generateKeyPairSync("x25519");
14
+ const publicKeyRaw = publicKeyToRaw(publicKey);
15
+ return { publicKeyB64: publicKeyRaw.toString("base64"), publicKeyRaw, privateKey };
16
+ };
17
+ var publicKeyFingerprint = (b64) => {
18
+ const raw = Buffer.from(b64, "base64");
19
+ const digest = crypto.createHash("sha256").update(raw).digest("hex");
20
+ return (digest.slice(0, 8).match(/.{2}/g) ?? []).join(" ").toUpperCase();
21
+ };
22
+ var openSealedKey = (blobB64, keypair) => {
23
+ const blob = Buffer.from(blobB64, "base64");
24
+ if (blob.length <= HEADER_BYTES) throw new Error("sealed payload too short");
25
+ const ephemeralRaw = blob.subarray(0, 32);
26
+ const nonce = blob.subarray(32, 44);
27
+ const tag = blob.subarray(44, 60);
28
+ const ciphertext = blob.subarray(60);
29
+ const shared = crypto.diffieHellman({
30
+ privateKey: keypair.privateKey,
31
+ publicKey: rawToPublicKey(ephemeralRaw)
32
+ });
33
+ const salt = Buffer.concat([ephemeralRaw, keypair.publicKeyRaw]);
34
+ const key = Buffer.from(crypto.hkdfSync("sha256", shared, salt, HKDF_INFO, 32));
35
+ const decipher = crypto.createDecipheriv("aes-256-gcm", key, nonce);
36
+ decipher.setAuthTag(tag);
37
+ return Buffer.concat([decipher.update(ciphertext), decipher.final()]).toString("utf8");
38
+ };
39
+
40
+ export {
41
+ generateKeypair,
42
+ publicKeyFingerprint,
43
+ openSealedKey
44
+ };