@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.
- package/CHANGELOG.md +161 -0
- package/README.md +52 -9
- package/dist/bin/pushary-claude.js +6 -6
- package/dist/bin/pushary-clean.js +83 -38
- package/dist/bin/pushary-codex-hook.js +6 -6
- package/dist/bin/pushary-codex.js +4 -4
- package/dist/bin/pushary-connect.d.ts +1 -0
- package/dist/bin/pushary-connect.js +92 -0
- package/dist/bin/pushary-daemon.js +42 -5
- package/dist/bin/pushary-doctor.js +246 -71
- package/dist/bin/pushary-gemini-hook.js +6 -6
- package/dist/bin/pushary-hook.js +10 -5
- package/dist/bin/pushary-login.d.ts +1 -0
- package/dist/bin/pushary-login.js +156 -0
- package/dist/bin/pushary-logout.d.ts +1 -0
- package/dist/bin/pushary-logout.js +144 -0
- package/dist/bin/pushary-mode.js +24 -13
- package/dist/bin/pushary-notification-hook.js +4 -4
- package/dist/bin/pushary-permission-denied-hook.js +5 -5
- package/dist/bin/pushary-permission-hook.js +5 -5
- package/dist/bin/pushary-post-hook.js +4 -4
- package/dist/bin/pushary-prompt-hook.js +4 -4
- package/dist/bin/pushary-session-end-hook.js +4 -4
- package/dist/bin/pushary-session-start-hook.js +4 -4
- package/dist/bin/pushary-setup.js +769 -518
- package/dist/bin/pushary-stats.js +6 -1
- package/dist/bin/pushary-status.d.ts +1 -0
- package/dist/bin/pushary-status.js +206 -0
- package/dist/bin/pushary-stop-hook.js +4 -4
- package/dist/bin/pushary-stopfailure-hook.js +4 -4
- package/dist/bin/pushary-suggestions.js +10 -5
- package/dist/bin/pushary-upgrade.js +184 -14
- package/dist/bin/pushary-wait.js +21 -12
- package/dist/bin/pushary.js +42 -60
- package/dist/chunk-247C4RE5.js +236 -0
- package/dist/{chunk-NKXSILEW.js → chunk-2UMNXADU.js} +18 -2
- package/dist/chunk-3EGEA4KH.js +44 -0
- package/dist/chunk-3O27ZSRE.js +569 -0
- package/dist/{chunk-BE5X3WXL.js → chunk-5SO3CEGJ.js} +5 -5
- package/dist/chunk-7QLSKOSU.js +19 -0
- package/dist/chunk-E2U35RLD.js +108 -0
- package/dist/chunk-ER657KRJ.js +168 -0
- package/dist/{chunk-WNRYRN2R.js → chunk-HIIBSYFJ.js} +5 -5
- package/dist/{chunk-DWED7BS3.js → chunk-HNTKTK5B.js} +1 -1
- package/dist/chunk-IIZZYUWK.js +18 -0
- package/dist/{chunk-J7JWI3KU.js → chunk-MUEW424A.js} +19 -1
- package/dist/chunk-N4DA4CJB.js +57 -0
- package/dist/chunk-N7XJ4L2W.js +79 -0
- package/dist/chunk-ONVEYEVR.js +44 -0
- package/dist/chunk-QHOVJXOT.js +255 -0
- package/dist/chunk-SP7OZXCQ.js +224 -0
- package/dist/chunk-TX7KBKT7.js +79 -0
- package/dist/chunk-UXXRRXUS.js +93 -0
- package/dist/chunk-YJ7YZRVV.js +217 -0
- package/dist/src/index.js +5 -5
- package/package.json +11 -4
- package/dist/chunk-BQLCELYC.js +0 -332
package/dist/bin/pushary.js
CHANGED
|
@@ -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
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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)
|
|
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
|
|
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
|
+
};
|