@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.
- package/CHANGELOG.md +113 -0
- package/README.md +52 -9
- package/dist/bin/pushary-claude.js +3 -3
- package/dist/bin/pushary-clean.js +26 -11
- package/dist/bin/pushary-codex-hook.js +3 -3
- package/dist/bin/pushary-codex.js +3 -3
- package/dist/bin/pushary-connect.d.ts +1 -0
- package/dist/bin/pushary-connect.js +92 -0
- package/dist/bin/pushary-daemon.js +7 -2
- package/dist/bin/pushary-doctor.js +142 -46
- package/dist/bin/pushary-gemini-hook.js +3 -3
- package/dist/bin/pushary-hook.js +9 -4
- 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 +102 -0
- package/dist/bin/pushary-mode.js +24 -13
- package/dist/bin/pushary-notification-hook.js +3 -3
- package/dist/bin/pushary-permission-denied-hook.js +4 -4
- package/dist/bin/pushary-permission-hook.js +4 -4
- package/dist/bin/pushary-post-hook.js +3 -3
- package/dist/bin/pushary-prompt-hook.js +3 -3
- package/dist/bin/pushary-session-end-hook.js +3 -3
- package/dist/bin/pushary-session-start-hook.js +3 -3
- package/dist/bin/pushary-setup.js +858 -547
- 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 +3 -3
- package/dist/bin/pushary-stopfailure-hook.js +3 -3
- package/dist/bin/pushary-suggestions.js +10 -5
- package/dist/bin/pushary-upgrade.js +11 -6
- package/dist/bin/pushary-wait.js +21 -12
- package/dist/bin/pushary.js +42 -60
- package/dist/{chunk-NKXSILEW.js → chunk-2UMNXADU.js} +18 -2
- package/dist/chunk-3EGEA4KH.js +44 -0
- package/dist/chunk-7QLSKOSU.js +19 -0
- package/dist/chunk-B26DNXCA.js +235 -0
- package/dist/chunk-GX64YGU3.js +544 -0
- package/dist/{chunk-BE5X3WXL.js → chunk-HI4AGGE6.js} +2 -2
- package/dist/{chunk-DWED7BS3.js → chunk-HNTKTK5B.js} +1 -1
- package/dist/chunk-HUJQSP4F.js +18 -0
- package/dist/{chunk-J7JWI3KU.js → chunk-MUEW424A.js} +19 -1
- package/dist/chunk-N4DA4CJB.js +57 -0
- package/dist/chunk-QHOVJXOT.js +255 -0
- package/dist/chunk-R6C4USIV.js +44 -0
- package/dist/{chunk-BQLCELYC.js → chunk-S4TFBJ5O.js} +7 -0
- package/dist/chunk-SML23YOT.js +216 -0
- package/dist/chunk-TX7KBKT7.js +79 -0
- package/dist/chunk-UKNAAVEE.js +217 -0
- package/dist/{chunk-WNRYRN2R.js → chunk-VFBYRR2N.js} +2 -2
- package/dist/src/index.js +4 -4
- package/package.json +11 -4
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
runBrowserLogin
|
|
4
|
+
} from "../chunk-QHOVJXOT.js";
|
|
5
|
+
import {
|
|
6
|
+
addClaudeMcpServer,
|
|
7
|
+
addPusharyHooks,
|
|
8
|
+
addPusharyToolPermissions
|
|
9
|
+
} from "../chunk-CAJZAFVS.js";
|
|
6
10
|
import {
|
|
7
11
|
GEMINI_HOOK_BINARY,
|
|
8
12
|
addCodexHookTrust,
|
|
@@ -13,365 +17,351 @@ import {
|
|
|
13
17
|
renderAgentInstructions,
|
|
14
18
|
renderProjectAgentInstructions,
|
|
15
19
|
writeInstructionBlock
|
|
16
|
-
} from "../chunk-
|
|
20
|
+
} from "../chunk-S4TFBJ5O.js";
|
|
21
|
+
import {
|
|
22
|
+
confirmAppConnection,
|
|
23
|
+
connectDevice,
|
|
24
|
+
connectViaAppPairing,
|
|
25
|
+
printConnectInstructions,
|
|
26
|
+
setHumanStream
|
|
27
|
+
} from "../chunk-GX64YGU3.js";
|
|
28
|
+
import "../chunk-3EGEA4KH.js";
|
|
29
|
+
import {
|
|
30
|
+
parseFlags
|
|
31
|
+
} from "../chunk-N4DA4CJB.js";
|
|
32
|
+
import {
|
|
33
|
+
KEY_FILE_MODE,
|
|
34
|
+
backupFile,
|
|
35
|
+
exportLine,
|
|
36
|
+
readJsonSafe,
|
|
37
|
+
writeFileAtomic,
|
|
38
|
+
writeJsonAtomic,
|
|
39
|
+
writeKey
|
|
40
|
+
} from "../chunk-B26DNXCA.js";
|
|
41
|
+
import {
|
|
42
|
+
checkApiKey,
|
|
43
|
+
describeKeyCheck
|
|
44
|
+
} from "../chunk-UKNAAVEE.js";
|
|
45
|
+
import {
|
|
46
|
+
UsageError
|
|
47
|
+
} from "../chunk-7QLSKOSU.js";
|
|
48
|
+
import {
|
|
49
|
+
installClaudeAlias,
|
|
50
|
+
resolveShellRc
|
|
51
|
+
} from "../chunk-BC3VCZ3E.js";
|
|
17
52
|
import {
|
|
18
|
-
|
|
53
|
+
installGlobally,
|
|
19
54
|
npmErrorMessage,
|
|
20
55
|
quoteIfNeeded,
|
|
21
56
|
resolveGlobalBinDir,
|
|
22
57
|
resolveGlobalBinary
|
|
23
|
-
} from "../chunk-
|
|
58
|
+
} from "../chunk-MUEW424A.js";
|
|
24
59
|
import {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
} from "../chunk-
|
|
60
|
+
EXIT,
|
|
61
|
+
getPackageVersion,
|
|
62
|
+
renderCommandHelp
|
|
63
|
+
} from "../chunk-SML23YOT.js";
|
|
29
64
|
import {
|
|
30
65
|
reportEvent
|
|
31
|
-
} from "../chunk-
|
|
66
|
+
} from "../chunk-VFBYRR2N.js";
|
|
32
67
|
import "../chunk-RN3NOEJF.js";
|
|
33
|
-
import "../chunk-
|
|
68
|
+
import "../chunk-HNTKTK5B.js";
|
|
34
69
|
import {
|
|
35
70
|
isValidApiKey
|
|
36
71
|
} from "../chunk-DQAN3JQP.js";
|
|
37
|
-
import "../chunk-
|
|
72
|
+
import "../chunk-2UMNXADU.js";
|
|
38
73
|
|
|
39
74
|
// bin/pushary-setup.ts
|
|
40
|
-
import { existsSync, readFileSync, writeFileSync,
|
|
41
|
-
import { join, dirname, basename } from "path";
|
|
42
|
-
import { homedir } from "os";
|
|
43
|
-
import { execSync as
|
|
75
|
+
import { existsSync as existsSync2, readFileSync, writeFileSync, mkdirSync, cpSync, rmSync, renameSync } from "fs";
|
|
76
|
+
import { join as join2, dirname, basename } from "path";
|
|
77
|
+
import { homedir as homedir2, tmpdir } from "os";
|
|
78
|
+
import { execSync as execSync3 } from "child_process";
|
|
44
79
|
import { checkbox, input, confirm } from "@inquirer/prompts";
|
|
45
80
|
import { fileURLToPath } from "url";
|
|
46
81
|
import { parse as parseTOML, stringify as stringifyTOML } from "smol-toml";
|
|
47
82
|
|
|
48
|
-
// src/
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
var HKDF_INFO = Buffer.from("pushary-pair-v1");
|
|
55
|
-
var HEADER_BYTES = 32 + 12 + 16;
|
|
56
|
-
var rawToPublicKey = (raw) => crypto.createPublicKey({
|
|
57
|
-
key: Buffer.concat([X25519_SPKI_PREFIX, raw]),
|
|
58
|
-
format: "der",
|
|
59
|
-
type: "spki"
|
|
60
|
-
});
|
|
61
|
-
var publicKeyToRaw = (key) => key.export({ format: "der", type: "spki" }).subarray(-32);
|
|
62
|
-
var generateKeypair = () => {
|
|
63
|
-
const { publicKey, privateKey } = crypto.generateKeyPairSync("x25519");
|
|
64
|
-
const publicKeyRaw = publicKeyToRaw(publicKey);
|
|
65
|
-
return { publicKeyB64: publicKeyRaw.toString("base64"), publicKeyRaw, privateKey };
|
|
66
|
-
};
|
|
67
|
-
var publicKeyFingerprint = (b64) => {
|
|
68
|
-
const raw = Buffer.from(b64, "base64");
|
|
69
|
-
const digest = crypto.createHash("sha256").update(raw).digest("hex");
|
|
70
|
-
return (digest.slice(0, 8).match(/.{2}/g) ?? []).join(" ").toUpperCase();
|
|
71
|
-
};
|
|
72
|
-
var openSealedKey = (blobB64, keypair) => {
|
|
73
|
-
const blob = Buffer.from(blobB64, "base64");
|
|
74
|
-
if (blob.length <= HEADER_BYTES) throw new Error("sealed payload too short");
|
|
75
|
-
const ephemeralRaw = blob.subarray(0, 32);
|
|
76
|
-
const nonce = blob.subarray(32, 44);
|
|
77
|
-
const tag = blob.subarray(44, 60);
|
|
78
|
-
const ciphertext = blob.subarray(60);
|
|
79
|
-
const shared = crypto.diffieHellman({
|
|
80
|
-
privateKey: keypair.privateKey,
|
|
81
|
-
publicKey: rawToPublicKey(ephemeralRaw)
|
|
82
|
-
});
|
|
83
|
-
const salt = Buffer.concat([ephemeralRaw, keypair.publicKeyRaw]);
|
|
84
|
-
const key = Buffer.from(crypto.hkdfSync("sha256", shared, salt, HKDF_INFO, 32));
|
|
85
|
-
const decipher = crypto.createDecipheriv("aes-256-gcm", key, nonce);
|
|
86
|
-
decipher.setAuthTag(tag);
|
|
87
|
-
return Buffer.concat([decipher.update(ciphertext), decipher.final()]).toString("utf8");
|
|
88
|
-
};
|
|
89
|
-
|
|
90
|
-
// src/onboarding.ts
|
|
91
|
-
var dim = (s) => `\x1B[2m${s}\x1B[0m`;
|
|
92
|
-
var bold = (s) => `\x1B[1m${s}\x1B[0m`;
|
|
93
|
-
var green = (s) => `\x1B[32m${s}\x1B[0m`;
|
|
94
|
-
var cyan = (s) => `\x1B[36m${s}\x1B[0m`;
|
|
95
|
-
var yellow = (s) => `\x1B[33m${s}\x1B[0m`;
|
|
96
|
-
var check = green("\u2713");
|
|
97
|
-
var API_BASE = process.env.PUSHARY_API_URL?.trim() || "https://pushary.com";
|
|
98
|
-
var CONNECT_POLL_INTERVAL_MS = 2500;
|
|
99
|
-
var CONNECT_TIMEOUT_MS = 18e4;
|
|
100
|
-
var DELIVERY_POLL_INTERVAL_MS = 1500;
|
|
101
|
-
var DELIVERY_TIMEOUT_MS = 2e4;
|
|
102
|
-
var authHeaders = (apiKey) => ({
|
|
103
|
-
"Content-Type": "application/json",
|
|
104
|
-
Authorization: `Bearer ${apiKey}`
|
|
105
|
-
});
|
|
106
|
-
var getJson = async (url, apiKey) => {
|
|
107
|
-
try {
|
|
108
|
-
const res = await fetch(url, { headers: authHeaders(apiKey), signal: AbortSignal.timeout(1e4) });
|
|
109
|
-
if (!res.ok) return null;
|
|
110
|
-
return await res.json().catch(() => null);
|
|
111
|
-
} catch {
|
|
112
|
-
return null;
|
|
113
|
-
}
|
|
114
|
-
};
|
|
115
|
-
var fetchSite = async (apiKey) => {
|
|
116
|
-
const data = await getJson(`${API_BASE}/api/v1/server/site`, apiKey);
|
|
117
|
-
if (!data || typeof data.slug !== "string" || typeof data.subscribeUrl !== "string") return null;
|
|
118
|
-
return { slug: data.slug, name: typeof data.name === "string" ? data.name : data.slug, subscribeUrl: data.subscribeUrl };
|
|
119
|
-
};
|
|
120
|
-
var activeSubscriberCount = async (apiKey) => {
|
|
121
|
-
const data = await getJson(`${API_BASE}/api/v1/server/subscribers/count`, apiKey);
|
|
122
|
-
return data && typeof data.active === "number" ? data.active : 0;
|
|
123
|
-
};
|
|
124
|
-
var sendTestNotification = async (apiKey) => {
|
|
125
|
-
const res = await fetch(`${API_BASE}/api/v1/server/send`, {
|
|
126
|
-
method: "POST",
|
|
127
|
-
headers: authHeaders(apiKey),
|
|
128
|
-
body: JSON.stringify({
|
|
129
|
-
title: "Pushary is connected",
|
|
130
|
-
body: "Your AI agent can now reach you right here.",
|
|
131
|
-
metadata: { source: "cli-onboarding" }
|
|
132
|
-
}),
|
|
133
|
-
signal: AbortSignal.timeout(15e3)
|
|
134
|
-
});
|
|
135
|
-
const data = await res.json().catch(() => ({}));
|
|
136
|
-
if (!res.ok) throw new Error(typeof data.error === "string" ? data.error : res.statusText);
|
|
137
|
-
return Array.isArray(data.notificationIds) ? data.notificationIds : [];
|
|
138
|
-
};
|
|
139
|
-
var isDelivered = async (apiKey, id) => {
|
|
140
|
-
const data = await getJson(`${API_BASE}/api/v1/server/notifications/${id}`, apiKey);
|
|
141
|
-
return !!data && (data.status === "delivered" || data.deliveredAt != null);
|
|
142
|
-
};
|
|
143
|
-
var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
144
|
-
var startSpinner = (getLabel) => {
|
|
145
|
-
const frames = [" ", ". ", ".. ", "..."];
|
|
146
|
-
let i = 0;
|
|
147
|
-
const interval = setInterval(() => {
|
|
148
|
-
process.stdout.write(`\r ${dim(frames[i++ % frames.length])} ${getLabel()}`);
|
|
149
|
-
}, 200);
|
|
150
|
-
return (finalGlyph, finalLabel) => {
|
|
151
|
-
clearInterval(interval);
|
|
152
|
-
process.stdout.write(`\r ${finalGlyph} ${finalLabel}\x1B[K
|
|
153
|
-
`);
|
|
154
|
-
};
|
|
155
|
-
};
|
|
156
|
-
var printQr = (url) => new Promise((resolve, reject) => {
|
|
157
|
-
try {
|
|
158
|
-
qrcodeTerminal.generate(url, { small: true }, (qr) => {
|
|
159
|
-
process.stdout.write("\n" + qr.split("\n").map((line) => " " + line).join("\n") + "\n");
|
|
160
|
-
resolve();
|
|
161
|
-
});
|
|
162
|
-
} catch (err) {
|
|
163
|
-
reject(err);
|
|
83
|
+
// src/setup/closing.ts
|
|
84
|
+
var ACTIVITY_URL = "pushary.com/dashboard/agent/activity";
|
|
85
|
+
var nextSteps = (facts, reachable) => {
|
|
86
|
+
const lines = [];
|
|
87
|
+
if (facts.completed.includes("claude_code")) {
|
|
88
|
+
lines.push("pushary claude run Claude Code so your phone can start and steer it");
|
|
164
89
|
}
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
const left = Math.max(0, Math.ceil((deadline - Date.now()) / 1e3));
|
|
170
|
-
return `Waiting for your phone ${dim(`(${left}s)`)}`;
|
|
171
|
-
});
|
|
172
|
-
try {
|
|
173
|
-
while (Date.now() < deadline) {
|
|
174
|
-
if (await activeSubscriberCount(apiKey) > 0) {
|
|
175
|
-
stop(check, "Phone connected");
|
|
176
|
-
return true;
|
|
177
|
-
}
|
|
178
|
-
await sleep(CONNECT_POLL_INTERVAL_MS);
|
|
179
|
-
}
|
|
180
|
-
stop(yellow("!"), "Didn't detect a connection yet");
|
|
181
|
-
return false;
|
|
182
|
-
} catch (err) {
|
|
183
|
-
stop(yellow("!"), `Couldn't check connection ${dim(`(${err instanceof Error ? err.message : "network error"})`)}`);
|
|
184
|
-
return false;
|
|
90
|
+
if (reachable) {
|
|
91
|
+
lines.push("pushary daemon keep this machine reachable with nothing running");
|
|
92
|
+
} else {
|
|
93
|
+
lines.push("pushary connect connect a phone without re-running setup");
|
|
185
94
|
}
|
|
95
|
+
lines.push("pushary doctor re-check everything");
|
|
96
|
+
return lines;
|
|
186
97
|
};
|
|
187
|
-
var
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
98
|
+
var describeClosing = (facts) => {
|
|
99
|
+
const reachable = facts.reachChecked && facts.reachesYou;
|
|
100
|
+
if (facts.configuredNothing) {
|
|
101
|
+
return {
|
|
102
|
+
ok: false,
|
|
103
|
+
headline: "Setup failed.",
|
|
104
|
+
next: ["Nothing was configured. Run pushary doctor to see why."],
|
|
105
|
+
exitCode: EXIT.FAILED
|
|
106
|
+
};
|
|
196
107
|
}
|
|
197
|
-
if (
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
108
|
+
if (facts.failed.length > 0) {
|
|
109
|
+
const count = facts.failed.length;
|
|
110
|
+
return {
|
|
111
|
+
ok: false,
|
|
112
|
+
headline: `Setup finished with ${count} problem${count === 1 ? "" : "s"}.`,
|
|
113
|
+
next: [
|
|
114
|
+
...nextSteps(facts, reachable),
|
|
115
|
+
"",
|
|
116
|
+
`Approvals: ${ACTIVITY_URL}`
|
|
117
|
+
],
|
|
118
|
+
exitCode: EXIT.PROBLEMS_FOUND
|
|
119
|
+
};
|
|
207
120
|
}
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
121
|
+
if (facts.reachChecked && !facts.reachesYou) {
|
|
122
|
+
return {
|
|
123
|
+
ok: false,
|
|
124
|
+
headline: "Agents configured. Your phone is not connected yet.",
|
|
125
|
+
next: [
|
|
126
|
+
"Approvals fall back to the terminal until a device of yours can answer.",
|
|
127
|
+
"",
|
|
128
|
+
...nextSteps(facts, false)
|
|
129
|
+
],
|
|
130
|
+
exitCode: EXIT.NO_DEVICE
|
|
131
|
+
};
|
|
215
132
|
}
|
|
216
|
-
if (
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
133
|
+
if (!facts.reachChecked) {
|
|
134
|
+
return {
|
|
135
|
+
ok: true,
|
|
136
|
+
headline: "Setup complete.",
|
|
137
|
+
next: [
|
|
138
|
+
"The phone check was skipped, so nothing here proves an approval reaches you.",
|
|
139
|
+
"",
|
|
140
|
+
...nextSteps(facts, false)
|
|
141
|
+
],
|
|
142
|
+
exitCode: EXIT.OK
|
|
143
|
+
};
|
|
220
144
|
}
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
const connected = await waitForDevice(apiKey);
|
|
228
|
-
if (!connected) {
|
|
229
|
-
console.log(` ${dim("No rush \u2014 open")} ${cyan(site.subscribeUrl)} ${dim("on your phone whenever you're ready.")}`);
|
|
230
|
-
return false;
|
|
231
|
-
}
|
|
232
|
-
await sendAndConfirm(apiKey);
|
|
233
|
-
return true;
|
|
234
|
-
};
|
|
235
|
-
var printConnectInstructions = async (apiKey) => {
|
|
236
|
-
const site = await fetchSite(apiKey);
|
|
237
|
-
const target = site?.subscribeUrl ?? "https://pushary.com";
|
|
238
|
-
console.log(` ${dim("Connect your phone for push notifications at")} ${cyan(target)}`);
|
|
145
|
+
return {
|
|
146
|
+
ok: true,
|
|
147
|
+
headline: "Setup complete.",
|
|
148
|
+
next: [...nextSteps(facts, true), "", `Approvals: ${ACTIVITY_URL}`],
|
|
149
|
+
exitCode: EXIT.OK
|
|
150
|
+
};
|
|
239
151
|
};
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
152
|
+
|
|
153
|
+
// src/skills-cli.ts
|
|
154
|
+
import { execSync } from "child_process";
|
|
155
|
+
var installSkillViaSkillsCli = (agent) => {
|
|
243
156
|
try {
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
body: JSON.stringify({ cliPublicKey }),
|
|
248
|
-
signal: AbortSignal.timeout(1e4)
|
|
157
|
+
execSync(`npx -y skills@latest add Pushary/pushary-skill@pushary -y -g --agent ${agent}`, {
|
|
158
|
+
stdio: "pipe",
|
|
159
|
+
timeout: 9e4
|
|
249
160
|
});
|
|
250
|
-
|
|
251
|
-
const data = await res.json().catch(() => null);
|
|
252
|
-
return data && typeof data.pairId === "string" ? data.pairId : null;
|
|
161
|
+
return true;
|
|
253
162
|
} catch {
|
|
254
|
-
return
|
|
163
|
+
return false;
|
|
255
164
|
}
|
|
256
165
|
};
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
166
|
+
|
|
167
|
+
// src/setup/options.ts
|
|
168
|
+
var AGENT_CHOICES = ["claude_code", "codex", "gemini_cli", "hermes", "cursor", "custom"];
|
|
169
|
+
var SETUP_FLAGS = {
|
|
170
|
+
bools: ["key-stdin", "skip-phone", "yes", "y", "json", "dry-run", "help", "h"],
|
|
171
|
+
values: ["key", "connect", "agents"]
|
|
172
|
+
};
|
|
173
|
+
var isAgentChoice = (value) => AGENT_CHOICES.includes(value);
|
|
174
|
+
var parseAgentSelection = (raw, yes) => {
|
|
175
|
+
if (raw === void 0) {
|
|
176
|
+
return yes ? { kind: "auto" } : { kind: "ask" };
|
|
266
177
|
}
|
|
178
|
+
const value = raw.trim().toLowerCase();
|
|
179
|
+
if (value === "auto") return { kind: "auto" };
|
|
180
|
+
if (value === "none") return { kind: "none" };
|
|
181
|
+
const parts = value.split(",").map((part) => part.trim()).filter((part) => part !== "");
|
|
182
|
+
if (parts.length === 0) throw new UsageError("--agents needs a value");
|
|
183
|
+
const unknown = parts.filter((part) => !isAgentChoice(part));
|
|
184
|
+
if (unknown.length > 0) {
|
|
185
|
+
throw new UsageError(
|
|
186
|
+
`--agents does not know: ${unknown.join(", ")}. Use auto, none, or any of: ${AGENT_CHOICES.join(", ")}`
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
return { kind: "explicit", agents: [...new Set(parts)] };
|
|
267
190
|
};
|
|
268
|
-
var
|
|
269
|
-
const
|
|
270
|
-
const
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
191
|
+
var parseSetupOptions = (input2) => {
|
|
192
|
+
const flags = parseFlags(input2.argv, SETUP_FLAGS);
|
|
193
|
+
const stray = [
|
|
194
|
+
...flags.command !== void 0 && flags.command !== "setup" ? [flags.command] : [],
|
|
195
|
+
...flags.rest
|
|
196
|
+
];
|
|
197
|
+
if (stray.length > 0) {
|
|
198
|
+
throw new UsageError(
|
|
199
|
+
`setup takes no positional arguments (got ${stray.join(", ")}). Did you mean --agents ${stray[0]}?`
|
|
200
|
+
);
|
|
274
201
|
}
|
|
275
|
-
const
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
const
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
return `Waiting for the app to authorize ${dim(`(${left}s)`)}`;
|
|
288
|
-
});
|
|
289
|
-
try {
|
|
290
|
-
while (Date.now() < deadline) {
|
|
291
|
-
const claim = await claimPairing(pairId);
|
|
292
|
-
if (claim?.status === "authorized") {
|
|
293
|
-
let apiKey;
|
|
294
|
-
try {
|
|
295
|
-
apiKey = openSealedKey(claim.sealed, keypair);
|
|
296
|
-
} catch {
|
|
297
|
-
stop(yellow("!"), "Pairing failed to decrypt \u2014 re-run setup to try again");
|
|
298
|
-
return null;
|
|
299
|
-
}
|
|
300
|
-
stop(check, "App connected");
|
|
301
|
-
return { apiKey, handle: claim.siteSlug };
|
|
302
|
-
}
|
|
303
|
-
if (claim?.status === "expired") {
|
|
304
|
-
stop(yellow("!"), "Pairing expired \u2014 re-run setup to try again");
|
|
305
|
-
return null;
|
|
306
|
-
}
|
|
307
|
-
await sleep(PAIR_POLL_INTERVAL_MS);
|
|
308
|
-
}
|
|
309
|
-
stop(yellow("!"), "Timed out waiting for the app");
|
|
310
|
-
return null;
|
|
311
|
-
} catch (err) {
|
|
312
|
-
stop(yellow("!"), `Pairing error ${dim(`(${err instanceof Error ? err.message : "network"})`)}`);
|
|
313
|
-
return null;
|
|
202
|
+
const yes = flags.bools.has("yes") || flags.bools.has("y");
|
|
203
|
+
const help = flags.bools.has("help") || flags.bools.has("h");
|
|
204
|
+
const agentsFlag = flags.values.get("agents");
|
|
205
|
+
const agents = parseAgentSelection(agentsFlag, yes);
|
|
206
|
+
const connectRaw = flags.values.get("connect");
|
|
207
|
+
if (connectRaw !== void 0 && connectRaw !== "web" && connectRaw !== "app") {
|
|
208
|
+
throw new UsageError(`--connect must be one of: web, app (got ${connectRaw})`);
|
|
209
|
+
}
|
|
210
|
+
const key = flags.values.get("key")?.trim() || null;
|
|
211
|
+
const keyStdin = flags.bools.has("key-stdin");
|
|
212
|
+
if (key && keyStdin) {
|
|
213
|
+
throw new UsageError("Pass either --key or --key-stdin, not both");
|
|
314
214
|
}
|
|
215
|
+
return {
|
|
216
|
+
connectMode: connectRaw === "app" ? "app" : "web",
|
|
217
|
+
connectExplicit: connectRaw !== void 0,
|
|
218
|
+
skipPhone: flags.bools.has("skip-phone"),
|
|
219
|
+
// Anything that means "there is nobody to answer a prompt". A non-TTY stdin
|
|
220
|
+
// is included because that is the case that used to hang: it is better to
|
|
221
|
+
// take the defaults and say so than to wait forever for a keypress.
|
|
222
|
+
nonInteractive: yes || agentsFlag !== void 0 || !input2.stdinIsTty,
|
|
223
|
+
json: flags.bools.has("json"),
|
|
224
|
+
dryRun: flags.bools.has("dry-run"),
|
|
225
|
+
help,
|
|
226
|
+
agents,
|
|
227
|
+
key,
|
|
228
|
+
keyStdin
|
|
229
|
+
};
|
|
315
230
|
};
|
|
316
231
|
|
|
317
|
-
// src/
|
|
318
|
-
import { execSync } from "child_process";
|
|
319
|
-
|
|
232
|
+
// src/setup/detect.ts
|
|
233
|
+
import { execSync as execSync2 } from "child_process";
|
|
234
|
+
import { existsSync } from "fs";
|
|
235
|
+
import { homedir } from "os";
|
|
236
|
+
import { join } from "path";
|
|
237
|
+
var whichCommand = () => process.platform === "win32" ? "where" : "which";
|
|
238
|
+
var binaryOnPath = (binary) => {
|
|
320
239
|
try {
|
|
321
|
-
|
|
322
|
-
stdio: "pipe",
|
|
323
|
-
timeout: 9e4
|
|
324
|
-
});
|
|
240
|
+
execSync2(`${whichCommand()} ${binary}`, { stdio: "ignore", timeout: 5e3 });
|
|
325
241
|
return true;
|
|
326
242
|
} catch {
|
|
327
243
|
return false;
|
|
328
244
|
}
|
|
329
245
|
};
|
|
246
|
+
var defaultDeps = { onPath: binaryOnPath, exists: existsSync };
|
|
247
|
+
var detectAgent = (probe, deps = defaultDeps) => {
|
|
248
|
+
const checked = [];
|
|
249
|
+
if (probe.binary) {
|
|
250
|
+
checked.push(`${whichCommand()} ${probe.binary}`);
|
|
251
|
+
if (deps.onPath(probe.binary)) {
|
|
252
|
+
return { kind: "installed", evidence: `${probe.binary} on PATH`, strong: true };
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
for (const path of probe.paths) {
|
|
256
|
+
checked.push(path);
|
|
257
|
+
if (deps.exists(path)) return { kind: "installed", evidence: path, strong: true };
|
|
258
|
+
}
|
|
259
|
+
for (const marker of probe.configMarkers) {
|
|
260
|
+
checked.push(marker);
|
|
261
|
+
if (deps.exists(marker)) return { kind: "installed", evidence: marker, strong: false };
|
|
262
|
+
}
|
|
263
|
+
if (probe.binary === null && probe.paths.length === 0 && probe.configMarkers.length === 0) {
|
|
264
|
+
return { kind: "unknown" };
|
|
265
|
+
}
|
|
266
|
+
return { kind: "not-found", checked };
|
|
267
|
+
};
|
|
268
|
+
var agentProbes = (home = homedir()) => ({
|
|
269
|
+
claude_code: {
|
|
270
|
+
binary: "claude",
|
|
271
|
+
// The native installer puts it here, outside a default non-login PATH.
|
|
272
|
+
paths: [join(home, ".local", "bin", "claude")],
|
|
273
|
+
configMarkers: [join(home, ".claude")]
|
|
274
|
+
},
|
|
275
|
+
codex: {
|
|
276
|
+
binary: "codex",
|
|
277
|
+
paths: [],
|
|
278
|
+
configMarkers: [process.env.CODEX_HOME?.trim() || join(home, ".codex")]
|
|
279
|
+
},
|
|
280
|
+
gemini_cli: {
|
|
281
|
+
binary: "gemini",
|
|
282
|
+
paths: [],
|
|
283
|
+
configMarkers: [join(home, ".gemini")]
|
|
284
|
+
},
|
|
285
|
+
hermes: {
|
|
286
|
+
binary: "hermes",
|
|
287
|
+
paths: [],
|
|
288
|
+
configMarkers: [join(home, ".hermes")]
|
|
289
|
+
},
|
|
290
|
+
cursor: {
|
|
291
|
+
// GUI editor. `cursor` on PATH only exists if the user ran "Install 'cursor'
|
|
292
|
+
// command in PATH" from the command palette, which most never do.
|
|
293
|
+
binary: "cursor",
|
|
294
|
+
paths: process.platform === "darwin" ? ["/Applications/Cursor.app"] : process.platform === "win32" ? [join(process.env.LOCALAPPDATA ?? join(home, "AppData", "Local"), "Programs", "cursor")] : ["/usr/share/cursor", join(home, ".local", "share", "cursor")],
|
|
295
|
+
configMarkers: [join(home, ".cursor")]
|
|
296
|
+
},
|
|
297
|
+
// "Other" is a set of printed instructions for any MCP or HTTP client. There is
|
|
298
|
+
// nothing to detect, and it must never be auto-selected.
|
|
299
|
+
custom: { binary: null, paths: [], configMarkers: [] }
|
|
300
|
+
});
|
|
301
|
+
var detectAllAgents = (deps = defaultDeps, home) => {
|
|
302
|
+
const probes = agentProbes(home);
|
|
303
|
+
return Object.keys(probes).map((agent) => ({
|
|
304
|
+
agent,
|
|
305
|
+
result: detectAgent(probes[agent], deps)
|
|
306
|
+
}));
|
|
307
|
+
};
|
|
308
|
+
var isDetected = (result) => result.kind === "installed";
|
|
309
|
+
var shortenHome = (path, home = homedir()) => home && path.startsWith(home) ? `~${path.slice(home.length)}` : path;
|
|
310
|
+
var describeDetection = (result, home) => {
|
|
311
|
+
if (result.kind !== "installed") return null;
|
|
312
|
+
const evidence = shortenHome(result.evidence, home);
|
|
313
|
+
return result.strong ? evidence : `${evidence}, config only`;
|
|
314
|
+
};
|
|
330
315
|
|
|
331
316
|
// bin/pushary-setup.ts
|
|
332
|
-
var CLAUDE_SETTINGS =
|
|
333
|
-
var CLAUDE_JSON =
|
|
334
|
-
var CURSOR_PLUGIN_DIR =
|
|
335
|
-
var CURSOR_USER_HOOKS =
|
|
336
|
-
var CLAUDE_SKILL_DIR =
|
|
337
|
-
var CODEX_HOME = process.env.CODEX_HOME?.trim() ||
|
|
338
|
-
var CODEX_SKILL_DIR =
|
|
339
|
-
var CODEX_AGENTS_MD =
|
|
340
|
-
var GEMINI_SETTINGS =
|
|
341
|
-
var GEMINI_MD =
|
|
342
|
-
var
|
|
343
|
-
var
|
|
344
|
-
var
|
|
345
|
-
|
|
346
|
-
var bold2 = (s) => `\x1B[1m${s}\x1B[0m`;
|
|
347
|
-
var green2 = (s) => `\x1B[32m${s}\x1B[0m`;
|
|
348
|
-
var cyan2 = (s) => `\x1B[36m${s}\x1B[0m`;
|
|
349
|
-
var yellow2 = (s) => `\x1B[33m${s}\x1B[0m`;
|
|
350
|
-
var check2 = green2("\u2713");
|
|
351
|
-
var parseKeyFlag = () => {
|
|
352
|
-
const args = process.argv.slice(2);
|
|
353
|
-
for (let i = 0; i < args.length; i++) {
|
|
354
|
-
if (args[i] === "--key" && args[i + 1]) return args[i + 1].trim();
|
|
355
|
-
if (args[i].startsWith("--key=")) return args[i].slice(6).trim();
|
|
356
|
-
}
|
|
357
|
-
return void 0;
|
|
317
|
+
var CLAUDE_SETTINGS = join2(homedir2(), ".claude", "settings.json");
|
|
318
|
+
var CLAUDE_JSON = join2(homedir2(), ".claude.json");
|
|
319
|
+
var CURSOR_PLUGIN_DIR = join2(homedir2(), ".cursor", "plugins", "local", "pushary");
|
|
320
|
+
var CURSOR_USER_HOOKS = join2(homedir2(), ".cursor", "hooks.json");
|
|
321
|
+
var CLAUDE_SKILL_DIR = join2(homedir2(), ".claude", "skills", "pushary");
|
|
322
|
+
var CODEX_HOME = process.env.CODEX_HOME?.trim() || join2(homedir2(), ".codex");
|
|
323
|
+
var CODEX_SKILL_DIR = join2(CODEX_HOME, "skills", "pushary");
|
|
324
|
+
var CODEX_AGENTS_MD = join2(CODEX_HOME, "AGENTS.md");
|
|
325
|
+
var GEMINI_SETTINGS = join2(homedir2(), ".gemini", "settings.json");
|
|
326
|
+
var GEMINI_MD = join2(homedir2(), ".gemini", "GEMINI.md");
|
|
327
|
+
var PUSHARY_CONFIG_FILE = join2(homedir2(), ".pushary", "config.json");
|
|
328
|
+
var manualSteps = [];
|
|
329
|
+
var noteManual = (step) => {
|
|
330
|
+
manualSteps.push(step);
|
|
358
331
|
};
|
|
359
|
-
var
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
332
|
+
var humanStream = process.stdout;
|
|
333
|
+
var writeHuman = (text) => {
|
|
334
|
+
humanStream.write(text);
|
|
335
|
+
};
|
|
336
|
+
var dim = (s) => `\x1B[2m${s}\x1B[0m`;
|
|
337
|
+
var bold = (s) => `\x1B[1m${s}\x1B[0m`;
|
|
338
|
+
var green = (s) => `\x1B[32m${s}\x1B[0m`;
|
|
339
|
+
var cyan = (s) => `\x1B[36m${s}\x1B[0m`;
|
|
340
|
+
var yellow = (s) => `\x1B[33m${s}\x1B[0m`;
|
|
341
|
+
var check = green("\u2713");
|
|
342
|
+
var readKeyFromStdin = async (options) => {
|
|
343
|
+
if (!options.keyStdin) return void 0;
|
|
344
|
+
const chunks = [];
|
|
345
|
+
for await (const chunk of process.stdin) chunks.push(chunk);
|
|
346
|
+
const first = Buffer.concat(chunks).toString("utf-8").split("\n", 1)[0]?.trim();
|
|
347
|
+
if (!first) {
|
|
348
|
+
console.log(`
|
|
349
|
+
${yellow("!")} --key-stdin was passed but stdin was empty.`);
|
|
350
|
+
process.exit(2);
|
|
368
351
|
}
|
|
369
|
-
|
|
352
|
+
if (!isValidApiKey(first)) {
|
|
353
|
+
console.log(`
|
|
354
|
+
${yellow("!")} The key on stdin is not a valid Pushary key. Expected: ${dim("pk_xxx.xxx")}`);
|
|
355
|
+
console.log(` ${dim("Copy your key from")} ${cyan("https://pushary.com/dashboard/agent/settings")}
|
|
356
|
+
`);
|
|
357
|
+
process.exit(2);
|
|
358
|
+
}
|
|
359
|
+
return first;
|
|
370
360
|
};
|
|
371
361
|
var isInstalled = (command) => {
|
|
372
362
|
const whichCmd = process.platform === "win32" ? "where" : "which";
|
|
373
363
|
try {
|
|
374
|
-
|
|
364
|
+
execSync3(`${whichCmd} ${command}`, { stdio: "ignore", timeout: 5e3 });
|
|
375
365
|
return true;
|
|
376
366
|
} catch {
|
|
377
367
|
return false;
|
|
@@ -384,11 +374,40 @@ var readJson = (filePath) => {
|
|
|
384
374
|
return {};
|
|
385
375
|
}
|
|
386
376
|
};
|
|
377
|
+
var readAgentJson = (filePath) => {
|
|
378
|
+
const result = readJsonSafe(filePath);
|
|
379
|
+
if (result.kind === "ok") return result.value;
|
|
380
|
+
if (result.kind === "missing") return {};
|
|
381
|
+
const backup = backupFile(filePath);
|
|
382
|
+
const reason = result.kind === "unparseable" ? "is not valid JSON" : "could not be read";
|
|
383
|
+
throw new Error(
|
|
384
|
+
`${filePath} ${reason}, so it was left untouched. A copy is at ${backup}. Fix or remove the original, then re-run setup.`
|
|
385
|
+
);
|
|
386
|
+
};
|
|
387
387
|
var writeJson = (filePath, data) => {
|
|
388
388
|
const dir = dirname(filePath);
|
|
389
|
-
if (!
|
|
389
|
+
if (!existsSync2(dir)) mkdirSync(dir, { recursive: true });
|
|
390
390
|
writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n", "utf-8");
|
|
391
391
|
};
|
|
392
|
+
var readAgentToml = (filePath) => {
|
|
393
|
+
let raw;
|
|
394
|
+
try {
|
|
395
|
+
raw = readFileSync(filePath, "utf-8");
|
|
396
|
+
} catch (err) {
|
|
397
|
+
if (err.code === "ENOENT") return {};
|
|
398
|
+
throw new Error(`${filePath} could not be read, so it was left untouched.`);
|
|
399
|
+
}
|
|
400
|
+
if (raw.trim() === "") return {};
|
|
401
|
+
try {
|
|
402
|
+
return parseTOML(raw);
|
|
403
|
+
} catch {
|
|
404
|
+
const backup = backupFile(filePath);
|
|
405
|
+
throw new Error(
|
|
406
|
+
`${filePath} is not valid TOML, so it was left untouched. A copy is at ${backup}. Fix or remove the original, then re-run setup.`
|
|
407
|
+
);
|
|
408
|
+
}
|
|
409
|
+
};
|
|
410
|
+
var isCancellation = (err) => err instanceof Error && (err.name === "ExitPromptError" || /force closed the prompt/i.test(err.message));
|
|
392
411
|
var formatError = (err) => {
|
|
393
412
|
if (err instanceof Error) {
|
|
394
413
|
const errWithSignal = err;
|
|
@@ -403,29 +422,20 @@ var spinner = async (label, fn, options = {}) => {
|
|
|
403
422
|
const frames = [" ", ". ", ".. ", "..."];
|
|
404
423
|
let i = 0;
|
|
405
424
|
const interval = setInterval(() => {
|
|
406
|
-
|
|
425
|
+
writeHuman(`\r ${dim(frames[i++ % frames.length])} ${label}`);
|
|
407
426
|
}, 200);
|
|
408
427
|
try {
|
|
409
428
|
await fn();
|
|
410
429
|
clearInterval(interval);
|
|
411
|
-
|
|
430
|
+
writeHuman(`\r ${check} ${label}
|
|
412
431
|
`);
|
|
413
432
|
} catch (err) {
|
|
414
433
|
clearInterval(interval);
|
|
415
|
-
|
|
434
|
+
writeHuman(`\r ${yellow("!")} ${label} ${dim(`(${formatError(err)})`)}
|
|
416
435
|
`);
|
|
417
436
|
if (!options.optional) throw err;
|
|
418
437
|
}
|
|
419
438
|
};
|
|
420
|
-
var getPackageVersion = () => {
|
|
421
|
-
try {
|
|
422
|
-
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
423
|
-
const pkg = JSON.parse(readFileSync(join(__dirname, "..", "..", "package.json"), "utf-8"));
|
|
424
|
-
return pkg.version ?? "0.0.0";
|
|
425
|
-
} catch {
|
|
426
|
-
return "0.0.0";
|
|
427
|
-
}
|
|
428
|
-
};
|
|
429
439
|
var checkForUpdates = async (current) => {
|
|
430
440
|
try {
|
|
431
441
|
const res = await fetch("https://registry.npmjs.org/@pushary/agent-hooks/latest", {
|
|
@@ -434,17 +444,18 @@ var checkForUpdates = async (current) => {
|
|
|
434
444
|
const data = await res.json();
|
|
435
445
|
const latest = data.version;
|
|
436
446
|
if (latest && latest !== current) {
|
|
437
|
-
console.log(` ${
|
|
438
|
-
console.log(` ${
|
|
447
|
+
console.log(` ${yellow("!")} Update available: ${dim(current)} \u2192 ${green(latest)}`);
|
|
448
|
+
console.log(` ${dim("Run:")} npx @pushary/agent-hooks@latest upgrade`);
|
|
439
449
|
console.log();
|
|
440
450
|
}
|
|
441
451
|
} catch {
|
|
442
452
|
}
|
|
443
453
|
};
|
|
444
|
-
var
|
|
445
|
-
|
|
454
|
+
var installGlobally2 = async () => {
|
|
455
|
+
const version = getPackageVersion();
|
|
456
|
+
await spinner(`Installing pushary-hook globally (${version})`, async () => {
|
|
446
457
|
try {
|
|
447
|
-
|
|
458
|
+
await installGlobally({ version });
|
|
448
459
|
} catch (err) {
|
|
449
460
|
throw new Error(npmErrorMessage(err));
|
|
450
461
|
}
|
|
@@ -455,118 +466,129 @@ var fetchSkillContent = async () => {
|
|
|
455
466
|
if (_cachedSkillContent) return _cachedSkillContent;
|
|
456
467
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
457
468
|
const candidates = [
|
|
458
|
-
|
|
459
|
-
|
|
469
|
+
join2(__dirname, "..", "..", "data", "SKILL.md"),
|
|
470
|
+
join2(__dirname, "..", "data", "SKILL.md")
|
|
460
471
|
];
|
|
461
|
-
const source = candidates.find((path) =>
|
|
472
|
+
const source = candidates.find((path) => existsSync2(path));
|
|
462
473
|
if (!source) throw new Error("packaged skill not found");
|
|
463
474
|
_cachedSkillContent = readFileSync(source, "utf-8");
|
|
464
475
|
return _cachedSkillContent;
|
|
465
476
|
};
|
|
466
|
-
var skillsCliInUse = () => isInstalled("skills") ||
|
|
477
|
+
var skillsCliInUse = () => isInstalled("skills") || existsSync2(join2(homedir2(), ".agents", "skills"));
|
|
467
478
|
var installSkill = async (agent, fallbackDir) => {
|
|
468
479
|
await spinner("Installing Pushary skill", async () => {
|
|
469
480
|
if (skillsCliInUse() && installSkillViaSkillsCli(agent)) return;
|
|
470
481
|
const content = await fetchSkillContent();
|
|
471
|
-
if (!
|
|
472
|
-
writeFileSync(
|
|
482
|
+
if (!existsSync2(fallbackDir)) mkdirSync(fallbackDir, { recursive: true });
|
|
483
|
+
writeFileSync(join2(fallbackDir, "SKILL.md"), content, "utf-8");
|
|
473
484
|
});
|
|
474
485
|
};
|
|
475
486
|
var setupClaudeCode = async (apiKey) => {
|
|
476
487
|
console.log(`
|
|
477
|
-
${
|
|
488
|
+
${bold("Setting up Claude Code")}
|
|
478
489
|
`);
|
|
479
|
-
|
|
490
|
+
await installGlobally2();
|
|
491
|
+
const claudeJson = readAgentJson(CLAUDE_JSON);
|
|
492
|
+
const settings = readAgentJson(CLAUDE_SETTINGS);
|
|
480
493
|
await spinner("Adding MCP server (type: http)", async () => {
|
|
481
|
-
|
|
482
|
-
addClaudeMcpServer(data, apiKey);
|
|
483
|
-
writeJson(CLAUDE_JSON, data);
|
|
494
|
+
addClaudeMcpServer(claudeJson, apiKey);
|
|
484
495
|
});
|
|
485
496
|
await spinner("Auto-allowing Pushary tools", async () => {
|
|
486
497
|
addPusharyToolPermissions(settings);
|
|
487
498
|
});
|
|
488
|
-
await installGlobally();
|
|
489
499
|
await spinner("Adding hooks (approvals, activity, presence, and lifecycle events)", async () => {
|
|
490
500
|
addPusharyHooks(settings, resolveGlobalBinDir("pushary-hook"));
|
|
491
501
|
});
|
|
502
|
+
await spinner(`Writing ${CLAUDE_JSON}`, async () => {
|
|
503
|
+
writeJsonAtomic(CLAUDE_JSON, claudeJson, 384);
|
|
504
|
+
});
|
|
492
505
|
await spinner(`Writing ${CLAUDE_SETTINGS}`, async () => {
|
|
493
|
-
|
|
506
|
+
writeJsonAtomic(CLAUDE_SETTINGS, settings);
|
|
494
507
|
});
|
|
495
508
|
await installSkill("claude-code", CLAUDE_SKILL_DIR);
|
|
496
509
|
console.log();
|
|
497
|
-
console.log(` ${
|
|
498
|
-
console.log(` ${
|
|
499
|
-
console.log(` ${
|
|
500
|
-
console.log(` ${
|
|
501
|
-
console.log(` ${
|
|
510
|
+
console.log(` ${dim("What this configured:")}`);
|
|
511
|
+
console.log(` ${dim("\u2022")} MCP server: your agent can send notifications and ask questions`);
|
|
512
|
+
console.log(` ${dim("\u2022")} Skill: teaches your agent when and how to use Pushary`);
|
|
513
|
+
console.log(` ${dim("\u2022")} Hooks: route permission approvals through push notifications`);
|
|
514
|
+
console.log(` ${dim("\u2022")} Auto-allowed tools: no permission prompts for Pushary MCP calls`);
|
|
502
515
|
};
|
|
516
|
+
var IS_WINDOWS = process.platform === "win32";
|
|
503
517
|
var resolveHermesPython = () => {
|
|
504
518
|
try {
|
|
505
|
-
const
|
|
506
|
-
|
|
519
|
+
const probe = IS_WINDOWS ? "where hermes" : "command -v hermes";
|
|
520
|
+
const launcher = execSync3(probe, { encoding: "utf-8", stdio: "pipe", timeout: 5e3 }).split("\n")[0].trim();
|
|
521
|
+
if (launcher && !IS_WINDOWS) {
|
|
507
522
|
const shebang = readFileSync(launcher, "utf-8").split("\n", 1)[0];
|
|
508
523
|
if (shebang.startsWith("#!")) {
|
|
509
524
|
const interpreter = shebang.slice(2).trim().split(/\s+/)[0];
|
|
510
|
-
if (interpreter && /python/i.test(interpreter) &&
|
|
525
|
+
if (interpreter && /python/i.test(interpreter) && existsSync2(interpreter)) return interpreter;
|
|
511
526
|
}
|
|
512
527
|
}
|
|
513
528
|
} catch {
|
|
514
529
|
}
|
|
515
|
-
const
|
|
516
|
-
|
|
517
|
-
return null;
|
|
530
|
+
const venvRoot = join2(homedir2(), ".hermes", "hermes-agent", "venv");
|
|
531
|
+
const candidates = IS_WINDOWS ? [join2(venvRoot, "Scripts", "python.exe"), join2(venvRoot, "Scripts", "python3.exe")] : [join2(venvRoot, "bin", "python3"), join2(venvRoot, "bin", "python")];
|
|
532
|
+
return candidates.find(existsSync2) ?? null;
|
|
518
533
|
};
|
|
519
534
|
var ensurePip = (python) => {
|
|
520
535
|
try {
|
|
521
|
-
|
|
536
|
+
execSync3(`"${python}" -m pip --version`, { stdio: "ignore", timeout: 15e3 });
|
|
522
537
|
return;
|
|
523
538
|
} catch {
|
|
524
539
|
}
|
|
525
540
|
try {
|
|
526
|
-
|
|
541
|
+
execSync3(`"${python}" -m ensurepip --upgrade`, { stdio: "pipe", timeout: 6e4 });
|
|
527
542
|
} catch {
|
|
528
543
|
}
|
|
529
544
|
};
|
|
530
545
|
var enablePusharyPlugin = (python) => {
|
|
531
546
|
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"] = (e + ["pushary"]) if "pushary" not in e else e; 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"] = (d + ["clarify"]) if "clarify" not in d else d; c["agent"] = a; save_config(c)';
|
|
547
|
+
const scriptPath = join2(tmpdir(), `pushary-hermes-${process.pid}.py`);
|
|
532
548
|
try {
|
|
533
|
-
|
|
549
|
+
writeFileSync(scriptPath, snippet.split("; ").join("\n"), "utf-8");
|
|
550
|
+
execSync3(`"${python}" "${scriptPath}"`, { stdio: "pipe", timeout: 15e3 });
|
|
534
551
|
return;
|
|
535
552
|
} catch {
|
|
553
|
+
execSync3("hermes plugins enable pushary", { stdio: "ignore", timeout: 1e4 });
|
|
554
|
+
} finally {
|
|
555
|
+
try {
|
|
556
|
+
rmSync(scriptPath, { force: true });
|
|
557
|
+
} catch {
|
|
558
|
+
}
|
|
536
559
|
}
|
|
537
|
-
execSync2("hermes plugins enable pushary", { stdio: "ignore", timeout: 1e4 });
|
|
538
560
|
};
|
|
539
561
|
var setupHermes = async (_apiKey) => {
|
|
540
562
|
console.log(`
|
|
541
|
-
${
|
|
563
|
+
${bold("Setting up Hermes Agent")}
|
|
542
564
|
`);
|
|
543
565
|
if (!isInstalled("hermes")) {
|
|
544
|
-
console.log(` ${
|
|
545
|
-
console.log(` ${
|
|
546
|
-
return;
|
|
566
|
+
console.log(` ${yellow("!")} Hermes CLI not found. Skipping.`);
|
|
567
|
+
console.log(` ${dim("Install Hermes and re-run setup to configure.")}`);
|
|
568
|
+
return "skipped";
|
|
547
569
|
}
|
|
548
570
|
const python = resolveHermesPython();
|
|
549
571
|
if (!python) {
|
|
550
|
-
console.log(` ${
|
|
551
|
-
|
|
552
|
-
return;
|
|
572
|
+
console.log(` ${yellow("!")} Could not locate the Python that Hermes runs in. Skipping.`);
|
|
573
|
+
noteManual("Install the Hermes plugin by hand: <hermes-python> -m pip install hermes-plugin-pushary");
|
|
574
|
+
return "skipped";
|
|
553
575
|
}
|
|
554
576
|
await spinner("Installing hermes-plugin-pushary into Hermes\u2019 environment", async () => {
|
|
555
577
|
ensurePip(python);
|
|
556
|
-
|
|
578
|
+
execSync3(`"${python}" -m pip install --upgrade hermes-plugin-pushary`, { stdio: "pipe", timeout: 18e4 });
|
|
557
579
|
});
|
|
558
580
|
await spinner("Enabling plugin + routing questions to push", async () => {
|
|
559
581
|
enablePusharyPlugin(python);
|
|
560
582
|
});
|
|
561
583
|
console.log();
|
|
562
|
-
console.log(` ${
|
|
563
|
-
console.log(` ${
|
|
564
|
-
console.log(` ${
|
|
565
|
-
console.log(` ${
|
|
566
|
-
console.log(` ${
|
|
567
|
-
console.log(` ${
|
|
584
|
+
console.log(` ${dim("What this configured:")}`);
|
|
585
|
+
console.log(` ${dim("\u2022")} Native tools: pushary_notify, pushary_ask, pushary_wait, pushary_cancel`);
|
|
586
|
+
console.log(` ${dim("\u2022")} Questions go to your phone: Hermes' terminal ${bold("clarify")} tool is disabled, so pushary_ask handles every question`);
|
|
587
|
+
console.log(` ${dim("\u2022")} Auto-notifications: push alert when tools return errors`);
|
|
588
|
+
console.log(` ${dim("\u2022")} Permission gating: set ${bold("PUSHARY_GATE_TOOLS")} to require lock-screen approval for risky tools`);
|
|
589
|
+
console.log(` ${dim("To re-enable terminal prompts:")} remove ${bold("clarify")} from ${dim("agent.disabled_toolsets")} in ~/.hermes/config.yaml`);
|
|
568
590
|
};
|
|
569
|
-
var CODEX_HOOKS_JSON =
|
|
591
|
+
var CODEX_HOOKS_JSON = join2(CODEX_HOME, "hooks.json");
|
|
570
592
|
var CODEX_HOOKS_MIN_VERSION = [0, 122, 0];
|
|
571
593
|
var CODEX_TRUST_VERIFIED_MAX = [0, 142, 2];
|
|
572
594
|
var parseCodexVersion = (raw) => {
|
|
@@ -582,7 +604,7 @@ var compareCodexVersion = (a, b) => {
|
|
|
582
604
|
};
|
|
583
605
|
var readCodexVersion = () => {
|
|
584
606
|
try {
|
|
585
|
-
return parseCodexVersion(
|
|
607
|
+
return parseCodexVersion(execSync3("codex --version", { encoding: "utf-8", stdio: "pipe", timeout: 1e4 }));
|
|
586
608
|
} catch {
|
|
587
609
|
return null;
|
|
588
610
|
}
|
|
@@ -625,24 +647,19 @@ var addCodexNotifyEntry = (codexConfig) => {
|
|
|
625
647
|
};
|
|
626
648
|
var setupCodex = async (apiKey) => {
|
|
627
649
|
console.log(`
|
|
628
|
-
${
|
|
650
|
+
${bold("Setting up Codex")}
|
|
629
651
|
`);
|
|
630
652
|
if (!isInstalled("codex")) {
|
|
631
|
-
console.log(` ${
|
|
632
|
-
console.log(` ${
|
|
633
|
-
return;
|
|
653
|
+
console.log(` ${yellow("!")} Codex CLI not found. Skipping.`);
|
|
654
|
+
console.log(` ${dim("Install Codex and re-run setup to configure.")}`);
|
|
655
|
+
return "skipped";
|
|
634
656
|
}
|
|
635
|
-
await
|
|
636
|
-
const codexConfig =
|
|
657
|
+
await installGlobally2();
|
|
658
|
+
const codexConfig = join2(homedir2(), ".codex", "config.toml");
|
|
637
659
|
await spinner("Adding Pushary MCP server (key embedded, auto-allowed)", async () => {
|
|
638
|
-
|
|
639
|
-
try {
|
|
640
|
-
raw = readFileSync(codexConfig, "utf-8");
|
|
641
|
-
} catch {
|
|
642
|
-
}
|
|
643
|
-
const config = raw ? parseTOML(raw) : {};
|
|
660
|
+
const config = readAgentToml(codexConfig);
|
|
644
661
|
addCodexMcpServer(config, apiKey);
|
|
645
|
-
|
|
662
|
+
writeFileAtomic(codexConfig, stringifyTOML(config), KEY_FILE_MODE);
|
|
646
663
|
});
|
|
647
664
|
const codexVersion = readCodexVersion();
|
|
648
665
|
const hooksSupported = codexSupportsHooks(codexVersion);
|
|
@@ -651,7 +668,7 @@ var setupCodex = async (apiKey) => {
|
|
|
651
668
|
if (hooksSupported) {
|
|
652
669
|
const hookCommand = quoteIfNeeded(resolveGlobalBinary("pushary-codex-hook") ?? "pushary-codex-hook");
|
|
653
670
|
await spinner("Adding native hooks (~/.codex/hooks.json)", async () => {
|
|
654
|
-
const hooksConfig =
|
|
671
|
+
const hooksConfig = readAgentJson(CODEX_HOOKS_JSON);
|
|
655
672
|
addCodexHooks(hooksConfig, hookCommand);
|
|
656
673
|
writeJson(CODEX_HOOKS_JSON, hooksConfig);
|
|
657
674
|
});
|
|
@@ -672,9 +689,9 @@ var setupCodex = async (apiKey) => {
|
|
|
672
689
|
removeCodexNotifyEntry(codexConfig);
|
|
673
690
|
});
|
|
674
691
|
} else {
|
|
675
|
-
console.log(` ${
|
|
676
|
-
console.log(` ${
|
|
677
|
-
console.log(` ${
|
|
692
|
+
console.log(` ${yellow("!")} This Codex version predates native hooks (needs ${CODEX_HOOKS_MIN_VERSION.join(".")}+).`);
|
|
693
|
+
console.log(` ${dim("Installing the deprecated notify handler instead. Upgrade Codex and re-run setup")}`);
|
|
694
|
+
console.log(` ${dim("to get policy enforcement, phone approvals, and session tracking.")}`);
|
|
678
695
|
await spinner("Adding notify handler for Codex events (deprecated)", async () => {
|
|
679
696
|
addCodexNotifyEntry(codexConfig);
|
|
680
697
|
});
|
|
@@ -684,48 +701,49 @@ var setupCodex = async (apiKey) => {
|
|
|
684
701
|
writeInstructionBlock(CODEX_AGENTS_MD, renderAgentInstructions("Codex"));
|
|
685
702
|
});
|
|
686
703
|
console.log();
|
|
687
|
-
console.log(` ${
|
|
688
|
-
console.log(` ${
|
|
689
|
-
console.log(` ${
|
|
690
|
-
console.log(` ${
|
|
691
|
-
console.log(` ${
|
|
704
|
+
console.log(` ${dim("What this configured:")}`);
|
|
705
|
+
console.log(` ${dim("\u2022")} MCP server: Codex can send notifications and ask questions`);
|
|
706
|
+
console.log(` ${dim("\u2022")} API key embedded in config.toml: works without a shell reload or export`);
|
|
707
|
+
console.log(` ${dim("\u2022")} Proactive instructions: ~/.codex/AGENTS.md tells Codex to ask via push, not the terminal`);
|
|
708
|
+
console.log(` ${dim("\u2022")} Auto-allowed tools: no permission prompts for Pushary MCP calls`);
|
|
692
709
|
if (hooksSupported) {
|
|
693
|
-
console.log(` ${
|
|
710
|
+
console.log(` ${dim("\u2022")} Native hooks: phone approvals, policy enforcement, kill switch, session tracking`);
|
|
694
711
|
if (trusted) {
|
|
695
|
-
console.log(` ${
|
|
712
|
+
console.log(` ${dim("\u2022")} Hook pre-trusted: no ${cyan("/hooks")} step, phone approvals work right away`);
|
|
696
713
|
} else {
|
|
697
714
|
console.log();
|
|
698
|
-
console.log(` ${
|
|
699
|
-
console.log(` ${
|
|
700
|
-
console.log(` ${
|
|
715
|
+
console.log(` ${bold("One step left, and it happens inside Codex")}`);
|
|
716
|
+
console.log(` ${dim("Codex makes you trust any hook before it runs. This is Codex's own")}`);
|
|
717
|
+
console.log(` ${dim("security check, not a Pushary thing, and you only do it once.")}`);
|
|
701
718
|
console.log();
|
|
702
|
-
console.log(` ${
|
|
703
|
-
console.log(` ${
|
|
719
|
+
console.log(` ${cyan("1.")} Open Codex and type ${cyan("/hooks")}`);
|
|
720
|
+
console.log(` ${cyan("2.")} You will see the Pushary hook commands. Trust them.`);
|
|
704
721
|
console.log();
|
|
705
|
-
console.log(` ${
|
|
706
|
-
console.log(` ${
|
|
707
|
-
console.log(` ${
|
|
722
|
+
console.log(` ${dim("Until you do, Codex just runs as usual without the phone approvals.")}`);
|
|
723
|
+
console.log(` ${dim("To confirm:")} run a command that needs approval and watch your phone.`);
|
|
724
|
+
console.log(` ${dim("Pushary updates keep the same trust, so Codex will not ask again.")}`);
|
|
725
|
+
noteManual("Open Codex, type /hooks, and trust the Pushary hook. Until then Codex runs without phone approvals.");
|
|
708
726
|
}
|
|
709
727
|
} else {
|
|
710
|
-
console.log(` ${
|
|
728
|
+
console.log(` ${dim("\u2022")} Notify handler (deprecated): captures turn completions and approval requests`);
|
|
711
729
|
}
|
|
712
730
|
};
|
|
713
731
|
var resolveBundledPlugin = () => {
|
|
714
732
|
const dir = dirname(fileURLToPath(import.meta.url));
|
|
715
733
|
const candidates = [
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
734
|
+
join2(dir, "..", "..", "data", "cursor-plugin"),
|
|
735
|
+
join2(dir, "..", "data", "cursor-plugin"),
|
|
736
|
+
join2(dir, "..", "..", "..", "cursor-plugin"),
|
|
737
|
+
join2(dir, "..", "..", "cursor-plugin")
|
|
720
738
|
];
|
|
721
|
-
return candidates.find((p) =>
|
|
739
|
+
return candidates.find((p) => existsSync2(join2(p, ".cursor-plugin", "plugin.json"))) ?? null;
|
|
722
740
|
};
|
|
723
741
|
var installCursorUserHooks = (gateScript) => {
|
|
724
|
-
const template = readJson(
|
|
742
|
+
const template = readJson(join2(CURSOR_PLUGIN_DIR, "hooks", "hooks.json")).hooks?.beforeShellExecution?.[0];
|
|
725
743
|
if (!template) throw new Error("bundled Cursor hooks.json missing a beforeShellExecution entry");
|
|
726
744
|
const entry = { ...template, command: `node "${gateScript}"` };
|
|
727
745
|
let userHooks = {};
|
|
728
|
-
if (
|
|
746
|
+
if (existsSync2(CURSOR_USER_HOOKS)) {
|
|
729
747
|
try {
|
|
730
748
|
userHooks = JSON.parse(readFileSync(CURSOR_USER_HOOKS, "utf-8"));
|
|
731
749
|
} catch {
|
|
@@ -743,8 +761,8 @@ var installCursorUserHooks = (gateScript) => {
|
|
|
743
761
|
writeJson(CURSOR_USER_HOOKS, { ...userHooks, version: userHooks.version ?? 1, hooks });
|
|
744
762
|
};
|
|
745
763
|
var neutralizePluginGate = () => {
|
|
746
|
-
const path =
|
|
747
|
-
if (!
|
|
764
|
+
const path = join2(CURSOR_PLUGIN_DIR, "hooks", "hooks.json");
|
|
765
|
+
if (!existsSync2(path)) return;
|
|
748
766
|
const data = readJson(path);
|
|
749
767
|
if (data.hooks && "beforeShellExecution" in data.hooks) {
|
|
750
768
|
delete data.hooks.beforeShellExecution;
|
|
@@ -753,85 +771,122 @@ var neutralizePluginGate = () => {
|
|
|
753
771
|
};
|
|
754
772
|
var setupCursor = async (apiKey) => {
|
|
755
773
|
console.log(`
|
|
756
|
-
${
|
|
774
|
+
${bold("Setting up Cursor")}
|
|
757
775
|
`);
|
|
758
776
|
const source = resolveBundledPlugin();
|
|
759
777
|
if (!source) throw new Error("bundled Cursor plugin not found in this package");
|
|
760
778
|
await spinner("Installing Pushary plugin", async () => {
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
779
|
+
const staging = join2(dirname(CURSOR_PLUGIN_DIR), `.pushary-staging-${process.pid}`);
|
|
780
|
+
const backup = `${CURSOR_PLUGIN_DIR}.pushary-backup-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}`;
|
|
781
|
+
rmSync(staging, { recursive: true, force: true });
|
|
782
|
+
try {
|
|
783
|
+
cpSync(source, staging, {
|
|
784
|
+
recursive: true,
|
|
785
|
+
filter: (p) => !["tools", "node_modules", ".git", ".DS_Store"].includes(basename(p))
|
|
786
|
+
});
|
|
787
|
+
const staged = readJsonSafe(join2(staging, ".cursor-plugin", "plugin.json"));
|
|
788
|
+
if (staged.kind !== "ok") {
|
|
789
|
+
throw new Error("staged Cursor plugin is missing or has an unreadable plugin.json");
|
|
790
|
+
}
|
|
791
|
+
const hadExisting = existsSync2(CURSOR_PLUGIN_DIR);
|
|
792
|
+
if (hadExisting) renameSync(CURSOR_PLUGIN_DIR, backup);
|
|
793
|
+
try {
|
|
794
|
+
renameSync(staging, CURSOR_PLUGIN_DIR);
|
|
795
|
+
} catch (err) {
|
|
796
|
+
if (hadExisting) renameSync(backup, CURSOR_PLUGIN_DIR);
|
|
797
|
+
throw err;
|
|
798
|
+
}
|
|
799
|
+
if (hadExisting) rmSync(backup, { recursive: true, force: true });
|
|
800
|
+
} finally {
|
|
801
|
+
rmSync(staging, { recursive: true, force: true });
|
|
802
|
+
}
|
|
766
803
|
});
|
|
767
804
|
await spinner("Linking your API key", async () => {
|
|
768
|
-
const mcpPath =
|
|
769
|
-
const mcp =
|
|
805
|
+
const mcpPath = join2(CURSOR_PLUGIN_DIR, "mcp.json");
|
|
806
|
+
const mcp = readAgentJson(mcpPath);
|
|
770
807
|
const servers = mcp.mcpServers ?? {};
|
|
771
808
|
if (servers.pushary) {
|
|
772
809
|
servers.pushary.headers = { ...servers.pushary.headers, Authorization: `Bearer ${apiKey}` };
|
|
773
810
|
mcp.mcpServers = servers;
|
|
774
|
-
|
|
811
|
+
writeJsonAtomic(mcpPath, mcp, KEY_FILE_MODE);
|
|
775
812
|
}
|
|
776
813
|
});
|
|
777
814
|
await spinner("Registering permission gate (~/.cursor/hooks.json)", async () => {
|
|
778
|
-
installCursorUserHooks(
|
|
815
|
+
installCursorUserHooks(join2(CURSOR_PLUGIN_DIR, "scripts", "pushary-gate.mjs"));
|
|
779
816
|
neutralizePluginGate();
|
|
780
817
|
});
|
|
781
818
|
console.log();
|
|
782
|
-
console.log(` ${
|
|
783
|
-
console.log(` ${
|
|
784
|
-
console.log(` ${
|
|
785
|
-
console.log(` ${
|
|
786
|
-
console.log(` ${
|
|
819
|
+
console.log(` ${dim("What this configured:")}`);
|
|
820
|
+
console.log(` ${dim("\u2022")} Plugin installed to ~/.cursor/plugins/local/pushary (MCP tools, rule, skill)`);
|
|
821
|
+
console.log(` ${dim("\u2022")} Permission gate registered once in ~/.cursor/hooks.json`);
|
|
822
|
+
console.log(` ${dim("\u2022")} Risky shell commands route to push approval before they run`);
|
|
823
|
+
console.log(` ${dim("\u2022")} Fully quit and reopen Cursor to load it (a Reload Window may not be enough)`);
|
|
824
|
+
noteManual("Fully quit and reopen Cursor. A Reload Window may not be enough.");
|
|
787
825
|
};
|
|
788
826
|
var saveApiKey = async (apiKey) => {
|
|
827
|
+
let result;
|
|
789
828
|
await spinner("Saving your API key", async () => {
|
|
790
|
-
|
|
791
|
-
const config = readJson(PUSHARY_CONFIG_FILE);
|
|
792
|
-
writeFileSync(PUSHARY_CONFIG_FILE, JSON.stringify({ ...config, apiKey }, null, 2) + "\n", "utf-8");
|
|
793
|
-
try {
|
|
794
|
-
chmodSync(PUSHARY_CONFIG_FILE, 384);
|
|
795
|
-
} catch {
|
|
796
|
-
}
|
|
797
|
-
const exportLine = `
|
|
798
|
-
export PUSHARY_API_KEY='${apiKey}'
|
|
799
|
-
`;
|
|
800
|
-
const shellFile = SHELL_FILES.find((f) => existsSync(f));
|
|
801
|
-
if (shellFile) {
|
|
802
|
-
const content = readFileSync(shellFile, "utf-8");
|
|
803
|
-
if (!content.includes("PUSHARY_API_KEY")) {
|
|
804
|
-
appendFileSync(shellFile, exportLine, "utf-8");
|
|
805
|
-
}
|
|
806
|
-
}
|
|
807
|
-
process.env.PUSHARY_API_KEY = apiKey;
|
|
829
|
+
result = writeKey(apiKey);
|
|
808
830
|
});
|
|
831
|
+
if (!result) throw new Error("Could not save the API key");
|
|
832
|
+
if (result.configBackupPath) {
|
|
833
|
+
console.log(` ${yellow("!")} ${PUSHARY_CONFIG_FILE} did not parse. Saved a copy at ${dim(result.configBackupPath)}`);
|
|
834
|
+
}
|
|
835
|
+
const rc = result.rc;
|
|
836
|
+
switch (rc.kind) {
|
|
837
|
+
case "updated":
|
|
838
|
+
console.log(` ${check} Replaced the old key in ${dim(rc.path ?? "")}${rc.previousKeyPrefix ? dim(` (was ${rc.previousKeyPrefix})`) : ""}`);
|
|
839
|
+
break;
|
|
840
|
+
case "declined":
|
|
841
|
+
console.log(` ${yellow("!")} ${rc.path}:${rc.declinedLine} already sets ${cyan("PUSHARY_API_KEY")} in a form Pushary did not write.`);
|
|
842
|
+
console.log(` ${dim("It was left alone, and it will shadow the new key in new shells. Update it to:")}`);
|
|
843
|
+
console.log(` ${cyan(exportLine(apiKey))}`);
|
|
844
|
+
break;
|
|
845
|
+
case "unavailable":
|
|
846
|
+
console.log(` ${yellow("!")} Could not update ${rc.path}${rc.detail ? dim(` (${rc.detail})`) : ""}. Add this line yourself:`);
|
|
847
|
+
console.log(` ${cyan(exportLine(apiKey))}`);
|
|
848
|
+
break;
|
|
849
|
+
case "no-rc":
|
|
850
|
+
console.log(` ${yellow("!")} No shell profile found for this shell. Add this line yourself:`);
|
|
851
|
+
console.log(` ${cyan(exportLine(apiKey))}`);
|
|
852
|
+
break;
|
|
853
|
+
case "written":
|
|
854
|
+
case "unchanged":
|
|
855
|
+
break;
|
|
856
|
+
}
|
|
857
|
+
return rc;
|
|
809
858
|
};
|
|
810
|
-
var setupCustom = async (
|
|
859
|
+
var setupCustom = async (apiKey, context) => {
|
|
811
860
|
console.log(`
|
|
812
|
-
${
|
|
861
|
+
${bold("Connect any other agent (MCP or HTTP)")}
|
|
813
862
|
`);
|
|
814
|
-
|
|
863
|
+
const wroteExport = context ? ["written", "updated", "unchanged"].includes(context.rc.kind) : true;
|
|
864
|
+
if (wroteExport) {
|
|
865
|
+
console.log(` ${dim("Your key is saved as")} ${cyan("$PUSHARY_API_KEY")} ${dim("in your shell profile.")}`);
|
|
866
|
+
} else {
|
|
867
|
+
console.log(` ${dim("Add this to your shell profile so")} ${cyan("$PUSHARY_API_KEY")} ${dim("resolves:")}`);
|
|
868
|
+
console.log(` ${cyan(exportLine(apiKey))}`);
|
|
869
|
+
}
|
|
815
870
|
console.log();
|
|
816
|
-
console.log(` ${
|
|
817
|
-
console.log(` ${
|
|
871
|
+
console.log(` ${dim("MCP clients (Windsurf, LangChain, custom) \u2014 add this server:")}`);
|
|
872
|
+
console.log(` ${cyan("https://pushary.com/api/mcp/mcp")} ${dim("with header")} ${cyan("Authorization: Bearer $PUSHARY_API_KEY")}`);
|
|
818
873
|
console.log();
|
|
819
|
-
console.log(` ${
|
|
820
|
-
console.log(` ${
|
|
874
|
+
console.log(` ${dim("No-code / HTTP (n8n, Zapier, scripts) \u2014 ask a human and wait:")}`);
|
|
875
|
+
console.log(` ${dim("POST")} ${cyan("https://pushary.com/api/v1/server/ask")} ${dim("body")} ${cyan('{"question":"Approve?","type":"confirm","wait":true}')}`);
|
|
821
876
|
console.log();
|
|
822
|
-
console.log(` ${
|
|
877
|
+
console.log(` ${dim("Full guide:")} ${cyan("https://pushary.com/docs/agents/connect-any-agent")}`);
|
|
823
878
|
};
|
|
824
879
|
var setupGemini = async (apiKey) => {
|
|
825
880
|
console.log(`
|
|
826
|
-
${
|
|
881
|
+
${bold("Setting up Gemini CLI")}
|
|
827
882
|
`);
|
|
828
883
|
if (!isInstalled("gemini")) {
|
|
829
|
-
console.log(` ${
|
|
830
|
-
console.log(` ${
|
|
831
|
-
return;
|
|
884
|
+
console.log(` ${yellow("!")} Gemini CLI not found. Skipping.`);
|
|
885
|
+
console.log(` ${dim("Install Gemini CLI and re-run setup to configure.")}`);
|
|
886
|
+
return "skipped";
|
|
832
887
|
}
|
|
833
|
-
await
|
|
834
|
-
const settings =
|
|
888
|
+
await installGlobally2();
|
|
889
|
+
const settings = readAgentJson(GEMINI_SETTINGS);
|
|
835
890
|
await spinner("Adding MCP server (~/.gemini/settings.json)", async () => {
|
|
836
891
|
addGeminiMcpServer(settings, apiKey);
|
|
837
892
|
});
|
|
@@ -839,19 +894,20 @@ var setupGemini = async (apiKey) => {
|
|
|
839
894
|
addGeminiHooks(settings, quoteIfNeeded(resolveGlobalBinary(GEMINI_HOOK_BINARY) ?? GEMINI_HOOK_BINARY));
|
|
840
895
|
});
|
|
841
896
|
await spinner(`Writing ${GEMINI_SETTINGS}`, async () => {
|
|
842
|
-
|
|
897
|
+
writeJsonAtomic(GEMINI_SETTINGS, settings, KEY_FILE_MODE);
|
|
843
898
|
});
|
|
844
899
|
await spinner("Teaching Gemini to ask via push (~/.gemini/GEMINI.md)", async () => {
|
|
845
900
|
writeInstructionBlock(GEMINI_MD, renderAgentInstructions("Gemini"));
|
|
846
901
|
});
|
|
847
902
|
console.log();
|
|
848
|
-
console.log(` ${
|
|
849
|
-
console.log(` ${
|
|
850
|
-
console.log(` ${
|
|
851
|
-
console.log(` ${
|
|
852
|
-
console.log(` ${
|
|
853
|
-
console.log(` ${
|
|
854
|
-
console.log(` ${
|
|
903
|
+
console.log(` ${dim("What this configured:")}`);
|
|
904
|
+
console.log(` ${dim("\u2022")} MCP server: Gemini can send notifications and ask questions`);
|
|
905
|
+
console.log(` ${dim("\u2022")} Proactive instructions: ~/.gemini/GEMINI.md tells Gemini to ask via push, not the terminal`);
|
|
906
|
+
console.log(` ${dim("\u2022")} Hooks: phone approvals, policy enforcement, kill switch, session tracking`);
|
|
907
|
+
console.log(` ${dim("\u2022")} Gate covers ${bold("run_shell_command")}, ${bold("write_file")}, and ${bold("replace")}`);
|
|
908
|
+
console.log(` ${dim("\u2022")} Auto-allowed tools: Pushary MCP calls run without a confirmation prompt`);
|
|
909
|
+
console.log(` ${dim("Restart Gemini CLI to load the new config.")}`);
|
|
910
|
+
noteManual("Restart Gemini CLI to load the new config.");
|
|
855
911
|
};
|
|
856
912
|
var AGENT_SETUP = {
|
|
857
913
|
claude_code: setupClaudeCode,
|
|
@@ -865,9 +921,9 @@ var PROJECT_INSTRUCTION_TARGETS = {
|
|
|
865
921
|
claude_code: { file: "CLAUDE.md", label: "Claude Code" },
|
|
866
922
|
codex: { file: "AGENTS.md", label: "Codex" }
|
|
867
923
|
};
|
|
868
|
-
var offerProjectInstructions = async (agents) => {
|
|
924
|
+
var offerProjectInstructions = async (agents, options) => {
|
|
869
925
|
const targets = agents.map((agent) => PROJECT_INSTRUCTION_TARGETS[agent]).filter((target) => target !== void 0);
|
|
870
|
-
if (targets.length === 0 || !process.stdout.isTTY) return;
|
|
926
|
+
if (targets.length === 0 || !process.stdout.isTTY || options.nonInteractive) return;
|
|
871
927
|
console.log();
|
|
872
928
|
const wanted = await confirm({
|
|
873
929
|
message: "Add Pushary instructions to this project's CLAUDE.md/AGENTS.md so your whole team's agents can use phone approvals?",
|
|
@@ -875,133 +931,310 @@ var offerProjectInstructions = async (agents) => {
|
|
|
875
931
|
});
|
|
876
932
|
if (!wanted) return;
|
|
877
933
|
for (const target of targets) {
|
|
878
|
-
await spinner(`Writing managed block to ${
|
|
879
|
-
writeInstructionBlock(
|
|
934
|
+
await spinner(`Writing managed block to ${join2(process.cwd(), target.file)}`, async () => {
|
|
935
|
+
writeInstructionBlock(join2(process.cwd(), target.file), renderProjectAgentInstructions(target.label));
|
|
880
936
|
}, { optional: true });
|
|
881
937
|
}
|
|
882
|
-
console.log(` ${
|
|
938
|
+
console.log(` ${dim("Commit the file to share it. Teammates without a key fall back to the terminal.")}`);
|
|
883
939
|
};
|
|
884
|
-
var offerClaudeAlias = async () => {
|
|
885
|
-
if (!process.stdout.isTTY) return;
|
|
940
|
+
var offerClaudeAlias = async (options) => {
|
|
941
|
+
if (!process.stdout.isTTY || options.nonInteractive) return;
|
|
886
942
|
const rc = resolveShellRc();
|
|
887
943
|
console.log();
|
|
888
944
|
if (!rc) {
|
|
889
|
-
console.log(` ${
|
|
945
|
+
console.log(` ${dim("Tip: alias")} ${cyan("claude='pushary claude'")} ${dim("in your shell to make plain")} ${cyan("claude")} ${dim("reachable from your phone.")}`);
|
|
890
946
|
return;
|
|
891
947
|
}
|
|
892
948
|
const wanted = await confirm({
|
|
893
|
-
message: `Make plain ${
|
|
949
|
+
message: `Make plain ${bold("claude")} reachable from your phone by default? (adds an alias to ${basename(rc.path)}, reversible)`,
|
|
894
950
|
default: false
|
|
895
951
|
});
|
|
896
952
|
if (!wanted) return;
|
|
897
953
|
const result = installClaudeAlias();
|
|
898
954
|
if (result.installed) {
|
|
899
|
-
console.log(` ${
|
|
900
|
-
console.log(` ${
|
|
955
|
+
console.log(` ${check} Aliased ${cyan("claude")} \u2192 ${cyan("pushary claude")} in ${dim(result.path ?? rc.path)}`);
|
|
956
|
+
console.log(` ${dim("Open a new terminal (or")} ${cyan(`source ${rc.path}`)}${dim(") to use it. Undo any time with")} ${cyan("pushary clean")}${dim(".")}`);
|
|
901
957
|
} else if (result.reason === "exists") {
|
|
902
|
-
console.log(` ${
|
|
958
|
+
console.log(` ${check} ${dim("The")} ${cyan("claude")} ${dim("alias is already set.")}`);
|
|
903
959
|
}
|
|
904
960
|
};
|
|
961
|
+
var verifyKeyOrExit = async (apiKey, options) => {
|
|
962
|
+
const live = process.stdout.isTTY === true && !options.json;
|
|
963
|
+
if (live) writeHuman(` ${dim("...")} Verifying your key with pushary.com`);
|
|
964
|
+
let result;
|
|
965
|
+
try {
|
|
966
|
+
result = await checkApiKey(apiKey);
|
|
967
|
+
} catch (err) {
|
|
968
|
+
result = { kind: "unreachable", detail: formatError(err) };
|
|
969
|
+
}
|
|
970
|
+
if (live) writeHuman("\r\x1B[2K");
|
|
971
|
+
const report = describeKeyCheck(result);
|
|
972
|
+
if (report.fatal) {
|
|
973
|
+
console.log(` ${yellow("!")} ${report.message}`);
|
|
974
|
+
for (const line of report.next) console.log(` ${dim(line)}`);
|
|
975
|
+
console.log();
|
|
976
|
+
process.exit(report.exitCode);
|
|
977
|
+
}
|
|
978
|
+
if (result.kind === "ok") {
|
|
979
|
+
const { siteSlug, siteName, plan } = result.identity;
|
|
980
|
+
console.log(` ${check} Key verified ${dim(`(${siteSlug || siteName} \xB7 plan ${plan})`)}`);
|
|
981
|
+
} else {
|
|
982
|
+
console.log(` ${yellow("!")} ${report.message}`);
|
|
983
|
+
for (const line of report.next) console.log(` ${dim(line)}`);
|
|
984
|
+
}
|
|
985
|
+
return result;
|
|
986
|
+
};
|
|
987
|
+
var AGENT_NAMES = {
|
|
988
|
+
claude_code: "Claude Code",
|
|
989
|
+
codex: "Codex",
|
|
990
|
+
gemini_cli: "Gemini CLI",
|
|
991
|
+
hermes: "Hermes",
|
|
992
|
+
cursor: "Cursor",
|
|
993
|
+
custom: "Other"
|
|
994
|
+
};
|
|
995
|
+
var AGENT_CAPABILITIES = {
|
|
996
|
+
claude_code: "MCP + hooks + auto-allowed tools",
|
|
997
|
+
codex: "MCP + native hooks + auto-allowed tools",
|
|
998
|
+
gemini_cli: "MCP + native hooks + auto-allowed tools",
|
|
999
|
+
hermes: "native plugin + auto-error notifications",
|
|
1000
|
+
cursor: "plugin + permission gate",
|
|
1001
|
+
custom: "any MCP or HTTP agent (Windsurf, n8n, custom)"
|
|
1002
|
+
};
|
|
1003
|
+
var NAME_COLUMN = Math.max(...Object.values(AGENT_NAMES).map((name) => name.length)) + 2;
|
|
1004
|
+
var AGENT_LABELS = Object.fromEntries(
|
|
1005
|
+
Object.keys(AGENT_NAMES).map((agent) => [
|
|
1006
|
+
agent,
|
|
1007
|
+
`${AGENT_NAMES[agent].padEnd(NAME_COLUMN)}${dim(AGENT_CAPABILITIES[agent])}`
|
|
1008
|
+
])
|
|
1009
|
+
);
|
|
1010
|
+
var detectAgents = () => {
|
|
1011
|
+
const map = {};
|
|
1012
|
+
for (const entry of detectAllAgents()) {
|
|
1013
|
+
map[entry.agent] = { found: isDetected(entry.result), evidence: describeDetection(entry.result) };
|
|
1014
|
+
}
|
|
1015
|
+
return map;
|
|
1016
|
+
};
|
|
1017
|
+
var canRunBrowserLogin = () => process.stdout.isTTY === true && !process.env.CI;
|
|
1018
|
+
var resolveAgents = async (options) => {
|
|
1019
|
+
if (options.agents.kind === "none") return [];
|
|
1020
|
+
if (options.agents.kind === "explicit") {
|
|
1021
|
+
const detected2 = detectAgents();
|
|
1022
|
+
for (const agent of options.agents.agents) {
|
|
1023
|
+
if (agent !== "custom" && !detected2[agent].found) {
|
|
1024
|
+
console.log(` ${yellow("!")} ${AGENT_NAMES[agent]} was not detected on this machine.`);
|
|
1025
|
+
console.log(` ${dim("Continuing anyway. The steps below say what was configured.")}`);
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
return options.agents.agents;
|
|
1029
|
+
}
|
|
1030
|
+
const detected = detectAgents();
|
|
1031
|
+
if (options.agents.kind === "auto") {
|
|
1032
|
+
const picked = Object.keys(detected).filter((agent) => detected[agent].found);
|
|
1033
|
+
if (picked.length === 0) {
|
|
1034
|
+
console.log(` ${yellow("!")} No agents detected, so nothing was configured. Your key is saved.`);
|
|
1035
|
+
console.log(` ${dim("Install an agent and re-run, name one with --agents <id>, or pass --agents none to accept this.")}`);
|
|
1036
|
+
console.log();
|
|
1037
|
+
process.exit(EXIT.PROBLEMS_FOUND);
|
|
1038
|
+
}
|
|
1039
|
+
console.log(` ${check} Selected automatically: ${picked.map((a) => AGENT_NAMES[a]).join(", ")} ${dim("(--agents to choose)")}`);
|
|
1040
|
+
return picked;
|
|
1041
|
+
}
|
|
1042
|
+
const hint = Object.values(detected).some((entry) => entry.found) ? "(detected agents pre-selected)" : "(space = toggle, enter = confirm)";
|
|
1043
|
+
return checkbox({
|
|
1044
|
+
message: "Which agents do you use? " + dim(hint),
|
|
1045
|
+
choices: Object.keys(AGENT_LABELS).map((agent) => ({
|
|
1046
|
+
// The evidence rides along so a detection the user disagrees with is
|
|
1047
|
+
// visible and correctable, rather than an unexplained pre-tick.
|
|
1048
|
+
name: detected[agent].evidence ? `${AGENT_LABELS[agent]} ${dim(`[${detected[agent].evidence}]`)}` : AGENT_LABELS[agent],
|
|
1049
|
+
value: agent,
|
|
1050
|
+
checked: detected[agent].found
|
|
1051
|
+
})),
|
|
1052
|
+
// Today an empty selection skips the loop and still prints "Setup complete."
|
|
1053
|
+
validate: (picked) => picked.length > 0 || "Select at least one agent, or re-run with --agents none."
|
|
1054
|
+
});
|
|
1055
|
+
};
|
|
1056
|
+
var AGENT_TARGETS = {
|
|
1057
|
+
claude_code: [CLAUDE_JSON, CLAUDE_SETTINGS, join2(CLAUDE_SKILL_DIR, "SKILL.md")],
|
|
1058
|
+
codex: [join2(CODEX_HOME, "config.toml"), CODEX_HOOKS_JSON, join2(CODEX_SKILL_DIR, "SKILL.md"), CODEX_AGENTS_MD],
|
|
1059
|
+
gemini_cli: [GEMINI_SETTINGS, GEMINI_MD],
|
|
1060
|
+
hermes: ["the Hermes virtualenv (pip install pushary-hermes)"],
|
|
1061
|
+
cursor: [join2(CURSOR_PLUGIN_DIR, "mcp.json"), CURSOR_USER_HOOKS],
|
|
1062
|
+
custom: ["nothing (prints connection details only)"]
|
|
1063
|
+
};
|
|
1064
|
+
var reportDryRun = (apiKey, agents, keyCheck) => {
|
|
1065
|
+
console.log();
|
|
1066
|
+
console.log(` ${bold("Dry run")} ${dim("\u2014 nothing below has been written")}`);
|
|
1067
|
+
console.log();
|
|
1068
|
+
console.log(` ${dim("Key:")} ${dim(`${apiKey.slice(0, 8)}...${apiKey.slice(-4)}`)} ${dim(`(${keyCheck?.kind ?? "not verified"})`)}`);
|
|
1069
|
+
console.log(` ${dim("Key store:")} ${PUSHARY_CONFIG_FILE}`);
|
|
1070
|
+
const rc = resolveShellRc();
|
|
1071
|
+
console.log(` ${dim("Shell rc:")} ${rc ? rc.path : dim("none for this shell; the export line would be printed instead")}`);
|
|
1072
|
+
console.log();
|
|
1073
|
+
if (agents.length === 0) {
|
|
1074
|
+
console.log(` ${dim("Agents:")} ${dim("none")}`);
|
|
1075
|
+
} else {
|
|
1076
|
+
console.log(` ${dim("Agents:")} ${agents.map((a) => a.replace("_", " ")).join(", ")}`);
|
|
1077
|
+
for (const agent of agents) {
|
|
1078
|
+
console.log();
|
|
1079
|
+
console.log(` ${bold(agent.replace("_", " "))}`);
|
|
1080
|
+
for (const target of AGENT_TARGETS[agent]) console.log(` ${dim("would write")} ${target}`);
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
console.log();
|
|
1084
|
+
console.log(` ${dim("Re-run without --dry-run to apply.")}`);
|
|
1085
|
+
console.log();
|
|
1086
|
+
};
|
|
1087
|
+
var printManualSteps = () => {
|
|
1088
|
+
if (manualSteps.length === 0) return;
|
|
1089
|
+
console.log(` ${bold("Still to do")}`);
|
|
1090
|
+
for (const step of manualSteps) console.log(` ${dim("\u2022")} ${step}`);
|
|
1091
|
+
console.log();
|
|
1092
|
+
};
|
|
1093
|
+
var emitJson = (options, payload) => {
|
|
1094
|
+
if (!options.json) return;
|
|
1095
|
+
process.stdout.write(`${JSON.stringify(payload, null, 2)}
|
|
1096
|
+
`);
|
|
1097
|
+
};
|
|
905
1098
|
var main = async () => {
|
|
1099
|
+
const options = parseSetupOptions({
|
|
1100
|
+
argv: process.argv.slice(2),
|
|
1101
|
+
stdinIsTty: process.stdin.isTTY === true
|
|
1102
|
+
});
|
|
1103
|
+
if (options.help) {
|
|
1104
|
+
process.stdout.write(`${renderCommandHelp("setup", getPackageVersion())}
|
|
1105
|
+
`);
|
|
1106
|
+
process.exit(EXIT.OK);
|
|
1107
|
+
}
|
|
1108
|
+
if (options.json) {
|
|
1109
|
+
humanStream = process.stderr;
|
|
1110
|
+
setHumanStream(process.stderr);
|
|
1111
|
+
console.log = (...args) => {
|
|
1112
|
+
process.stderr.write(`${args.map((a) => typeof a === "string" ? a : String(a)).join(" ")}
|
|
1113
|
+
`);
|
|
1114
|
+
};
|
|
1115
|
+
}
|
|
906
1116
|
const version = getPackageVersion();
|
|
907
1117
|
console.log();
|
|
908
|
-
console.log(` ${
|
|
909
|
-
console.log(` ${
|
|
1118
|
+
console.log(` ${bold("Pushary")} ${dim("v" + version)}`);
|
|
1119
|
+
console.log(` ${dim("Push notifications for AI agents")}`);
|
|
910
1120
|
console.log();
|
|
911
1121
|
await checkForUpdates(version);
|
|
912
|
-
const connectMode =
|
|
913
|
-
const flagKey =
|
|
1122
|
+
const connectMode = options.connectMode;
|
|
1123
|
+
const flagKey = await readKeyFromStdin(options) ?? options.key ?? void 0;
|
|
914
1124
|
const envKey = process.env.PUSHARY_API_KEY?.trim();
|
|
915
1125
|
let trimmedKey;
|
|
916
1126
|
if (connectMode === "app") {
|
|
917
|
-
console.log(` ${
|
|
1127
|
+
console.log(` ${dim("Pairing with your Pushary app.")}`);
|
|
918
1128
|
const paired = await connectViaAppPairing();
|
|
919
1129
|
if (!paired) {
|
|
920
1130
|
console.log();
|
|
921
|
-
console.log(` ${
|
|
922
|
-
console.log(` ${
|
|
1131
|
+
console.log(` ${yellow("!")} Pairing didn't complete. Re-run setup when you're ready to scan.`);
|
|
1132
|
+
console.log(` ${dim("No app yet? Get it at")} ${cyan("https://pushary.com/download")}`);
|
|
923
1133
|
process.exit(1);
|
|
924
1134
|
}
|
|
925
1135
|
trimmedKey = paired.apiKey;
|
|
926
1136
|
if (paired.handle) {
|
|
927
|
-
console.log(` ${
|
|
1137
|
+
console.log(` ${check} Connected to ${cyan("@" + paired.handle)}`);
|
|
928
1138
|
}
|
|
929
1139
|
console.log();
|
|
930
1140
|
} else if (flagKey && isValidApiKey(flagKey)) {
|
|
931
1141
|
const masked = `${flagKey.slice(0, 8)}...${flagKey.slice(-4)}`;
|
|
932
|
-
console.log(` ${
|
|
1142
|
+
console.log(` ${check} Using API key: ${dim(masked)}`);
|
|
933
1143
|
console.log();
|
|
934
1144
|
trimmedKey = flagKey;
|
|
935
1145
|
} else if (envKey && isValidApiKey(envKey)) {
|
|
936
1146
|
const masked = `${envKey.slice(0, 8)}...${envKey.slice(-4)}`;
|
|
937
|
-
console.log(` ${
|
|
1147
|
+
console.log(` ${check} Found API key in environment: ${dim(masked)}`);
|
|
938
1148
|
console.log();
|
|
939
|
-
const useExisting = await confirm({ message: "Use this key?", default: true });
|
|
1149
|
+
const useExisting = options.nonInteractive ? true : await confirm({ message: "Use this key?", default: true });
|
|
940
1150
|
if (useExisting) {
|
|
941
1151
|
trimmedKey = envKey;
|
|
942
1152
|
} else {
|
|
943
1153
|
const apiKey = await input({ message: "API key:" });
|
|
944
1154
|
trimmedKey = apiKey.trim();
|
|
945
1155
|
}
|
|
946
|
-
} else {
|
|
947
|
-
console.log(` ${
|
|
948
|
-
console.log(` ${
|
|
1156
|
+
} else if (!canRunBrowserLogin()) {
|
|
1157
|
+
console.log(` ${yellow("!")} No API key, and no terminal to sign in from.`);
|
|
1158
|
+
console.log(` ${dim("Pass --key <pk_xxx.xxx>, or --key-stdin, or export PUSHARY_API_KEY.")}`);
|
|
1159
|
+
console.log(` ${dim("On a machine with a terminal, run")} ${cyan("pushary login")} ${dim("once and re-run this.")}`);
|
|
949
1160
|
console.log();
|
|
950
|
-
|
|
951
|
-
|
|
1161
|
+
process.exit(EXIT.NOT_CONFIGURED);
|
|
1162
|
+
} else {
|
|
1163
|
+
const outcome = await runBrowserLogin(
|
|
1164
|
+
{ say: (line) => console.log(line), bold, dim, cyan, yellow },
|
|
1165
|
+
{ cliVersion: version }
|
|
1166
|
+
);
|
|
1167
|
+
if (outcome.kind === "ok") {
|
|
1168
|
+
trimmedKey = outcome.apiKey;
|
|
1169
|
+
console.log();
|
|
1170
|
+
console.log(` ${check} Signed in${outcome.siteSlug ? ` ${dim(`(${outcome.siteSlug})`)}` : ""}`);
|
|
1171
|
+
console.log();
|
|
1172
|
+
} else if (outcome.kind === "rejected") {
|
|
1173
|
+
console.log();
|
|
1174
|
+
console.log(` ${yellow("!")} That request was rejected in the browser. Nothing was written.`);
|
|
1175
|
+
console.log();
|
|
1176
|
+
process.exit(EXIT.NOT_CONFIGURED);
|
|
1177
|
+
} else {
|
|
1178
|
+
console.log();
|
|
1179
|
+
if (outcome.kind === "unavailable") {
|
|
1180
|
+
console.log(` ${yellow("!")} Couldn't reach the login service ${dim(`(${outcome.detail})`)}`);
|
|
1181
|
+
} else {
|
|
1182
|
+
console.log(` ${yellow("!")} The code expired before it was approved.`);
|
|
1183
|
+
}
|
|
1184
|
+
console.log(` ${dim("Paste a key instead, or press Ctrl-C and re-run setup for a fresh code.")}`);
|
|
1185
|
+
console.log(` ${dim("Keys live at")} ${cyan("pushary.com/dashboard/agent/settings")}`);
|
|
1186
|
+
console.log();
|
|
1187
|
+
const apiKey = await input({ message: "API key:" });
|
|
1188
|
+
trimmedKey = apiKey.trim();
|
|
1189
|
+
}
|
|
952
1190
|
}
|
|
953
1191
|
if (!trimmedKey || !isValidApiKey(trimmedKey)) {
|
|
954
1192
|
console.log(`
|
|
955
|
-
${
|
|
956
|
-
console.log(` ${
|
|
957
|
-
console.log(` ${
|
|
1193
|
+
${yellow("!")} Invalid key format. Expected: ${dim("pk_xxx.xxx")}`);
|
|
1194
|
+
console.log(` ${dim("Copy your key from")} ${cyan("https://pushary.com/dashboard/agent/settings")}`);
|
|
1195
|
+
console.log(` ${dim("Or sign up at")} ${cyan("https://pushary.com/sign-up?from=ai-coding")}
|
|
958
1196
|
`);
|
|
959
|
-
process.exit(
|
|
960
|
-
}
|
|
961
|
-
const
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
const
|
|
970
|
-
const
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
{ name: `Claude Code ${dim2("MCP + hooks + auto-allowed tools")}`, value: "claude_code", checked: detected.claude_code },
|
|
974
|
-
{ name: `Codex ${dim2("MCP + native hooks + auto-allowed tools")}`, value: "codex", checked: detected.codex },
|
|
975
|
-
{ name: `Gemini CLI ${dim2("MCP + native hooks + auto-allowed tools")}`, value: "gemini_cli", checked: detected.gemini_cli },
|
|
976
|
-
{ name: `Hermes ${dim2("native plugin + auto-error notifications")}`, value: "hermes", checked: detected.hermes },
|
|
977
|
-
{ name: `Cursor ${dim2("plugin + permission gate")}`, value: "cursor", checked: detected.cursor },
|
|
978
|
-
{ name: `Other ${dim2("any MCP or HTTP agent (Windsurf, n8n, custom)")}`, value: "custom", checked: false }
|
|
979
|
-
]
|
|
980
|
-
});
|
|
981
|
-
await saveApiKey(trimmedKey);
|
|
1197
|
+
process.exit(EXIT.USAGE);
|
|
1198
|
+
}
|
|
1199
|
+
const keyCheck = connectMode === "app" ? null : await verifyKeyOrExit(trimmedKey, options);
|
|
1200
|
+
const agents = await resolveAgents(options);
|
|
1201
|
+
if (options.dryRun) {
|
|
1202
|
+
reportDryRun(trimmedKey, agents, keyCheck);
|
|
1203
|
+
emitJson(options, { ok: true, dryRun: true, agents, keyCheck: keyCheck?.kind ?? "skipped" });
|
|
1204
|
+
process.exit(EXIT.OK);
|
|
1205
|
+
}
|
|
1206
|
+
const rc = await saveApiKey(trimmedKey);
|
|
1207
|
+
const context = { rc };
|
|
1208
|
+
const failed = [];
|
|
1209
|
+
const completed = [];
|
|
1210
|
+
const skipped = [];
|
|
982
1211
|
if (agents.length === 0) {
|
|
983
1212
|
console.log(`
|
|
984
|
-
${
|
|
1213
|
+
${check} API key saved. No agents configured.`);
|
|
1214
|
+
console.log(` ${dim("Run setup again with --agents <id> when you want one wired up.")}`);
|
|
985
1215
|
} else {
|
|
986
|
-
const failed = [];
|
|
987
|
-
const completed = [];
|
|
988
1216
|
for (const agent of agents) {
|
|
989
1217
|
try {
|
|
990
|
-
await AGENT_SETUP[agent](trimmedKey);
|
|
991
|
-
|
|
1218
|
+
const outcome = await AGENT_SETUP[agent](trimmedKey, context);
|
|
1219
|
+
if (outcome === "skipped") skipped.push(agent);
|
|
1220
|
+
else completed.push(agent);
|
|
992
1221
|
} catch (err) {
|
|
993
1222
|
failed.push(agent.replace("_", " "));
|
|
994
|
-
console.log(` ${
|
|
995
|
-
console.log(` ${
|
|
1223
|
+
console.log(` ${yellow("!")} ${agent.replace("_", " ")} setup failed: ${formatError(err)}`);
|
|
1224
|
+
console.log(` ${dim("Other agents will continue. Re-run setup to retry.")}`);
|
|
996
1225
|
}
|
|
997
1226
|
}
|
|
1227
|
+
if (skipped.length > 0) {
|
|
1228
|
+
console.log();
|
|
1229
|
+
console.log(` ${yellow("!")} Not configured: ${skipped.map((a) => a.replace("_", " ")).join(", ")} ${dim("(not installed on this machine)")}`);
|
|
1230
|
+
}
|
|
998
1231
|
if (failed.length > 0) {
|
|
999
1232
|
console.log();
|
|
1000
|
-
console.log(` ${
|
|
1233
|
+
console.log(` ${yellow("!")} Failed: ${failed.join(", ")} ${dim("(others completed successfully)")}`);
|
|
1001
1234
|
}
|
|
1002
|
-
await offerProjectInstructions(agents);
|
|
1235
|
+
await offerProjectInstructions(agents, options);
|
|
1003
1236
|
if (completed.includes("claude_code")) {
|
|
1004
|
-
await offerClaudeAlias();
|
|
1237
|
+
await offerClaudeAlias(options);
|
|
1005
1238
|
}
|
|
1006
1239
|
if (completed.length > 0) {
|
|
1007
1240
|
try {
|
|
@@ -1013,26 +1246,104 @@ var main = async () => {
|
|
|
1013
1246
|
}
|
|
1014
1247
|
}
|
|
1015
1248
|
}
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1249
|
+
const skipPhone = options.skipPhone;
|
|
1250
|
+
let phone = null;
|
|
1251
|
+
if (keyCheck?.kind === "locked") {
|
|
1252
|
+
console.log();
|
|
1253
|
+
console.log(` ${dim("Skipping the delivery check: no approval can be delivered until the plan is active.")}`);
|
|
1254
|
+
} else if (skipPhone) {
|
|
1255
|
+
console.log();
|
|
1256
|
+
console.log(` ${dim("Skipping the phone check (--skip-phone).")}`);
|
|
1257
|
+
} else if (!process.stdout.isTTY) {
|
|
1258
|
+
phone = await printConnectInstructions(trimmedKey);
|
|
1259
|
+
} else if (connectMode === "app") {
|
|
1260
|
+
phone = await confirmAppConnection(trimmedKey);
|
|
1261
|
+
} else {
|
|
1262
|
+
phone = await connectDevice(trimmedKey, { showCapabilities: !options.connectExplicit });
|
|
1023
1263
|
}
|
|
1024
1264
|
console.log();
|
|
1025
|
-
|
|
1265
|
+
if (keyCheck?.kind === "locked") {
|
|
1266
|
+
const report = describeKeyCheck(keyCheck);
|
|
1267
|
+
printManualSteps();
|
|
1268
|
+
console.log(` ${yellow(bold("Agents configured, approvals inactive."))}`);
|
|
1269
|
+
console.log();
|
|
1270
|
+
console.log(` ${report.message}`);
|
|
1271
|
+
for (const line of report.next) console.log(` ${dim(line)}`);
|
|
1272
|
+
console.log();
|
|
1273
|
+
emitJson(options, {
|
|
1274
|
+
ok: false,
|
|
1275
|
+
reason: "plan_inactive",
|
|
1276
|
+
configured: completed,
|
|
1277
|
+
skipped,
|
|
1278
|
+
manualSteps,
|
|
1279
|
+
failed,
|
|
1280
|
+
keyCheck: "locked",
|
|
1281
|
+
rc: rc.kind,
|
|
1282
|
+
exitCode: report.exitCode
|
|
1283
|
+
});
|
|
1284
|
+
process.exit(report.exitCode);
|
|
1285
|
+
}
|
|
1286
|
+
printManualSteps();
|
|
1287
|
+
const configuredNothing = agents.length > 0 && completed.length === 0;
|
|
1288
|
+
const agentsOk = failed.length === 0 && !configuredNothing;
|
|
1289
|
+
const reachChecked = phone !== null;
|
|
1290
|
+
const reachesYou = phone?.reachesYou ?? true;
|
|
1291
|
+
const closing = describeClosing({
|
|
1292
|
+
agentsOk,
|
|
1293
|
+
configuredNothing,
|
|
1294
|
+
completed,
|
|
1295
|
+
failed,
|
|
1296
|
+
reachChecked,
|
|
1297
|
+
reachesYou
|
|
1298
|
+
});
|
|
1299
|
+
console.log(` ${closing.ok ? green(bold(closing.headline)) : yellow(bold(closing.headline))}`);
|
|
1300
|
+
if (failed.length > 0) {
|
|
1301
|
+
console.log();
|
|
1302
|
+
for (const agent of completed) console.log(` ${check} ${AGENT_NAMES[agent] ?? agent}`);
|
|
1303
|
+
for (const item of failed) console.log(` ${yellow("!")} ${item}`);
|
|
1304
|
+
}
|
|
1026
1305
|
console.log();
|
|
1027
|
-
console.log(` ${
|
|
1028
|
-
console.log(` ${dim2("1.")} Restart your agent to load the new config`);
|
|
1029
|
-
console.log(` ${dim2("2.")} Run ${cyan2("npx @pushary/agent-hooks doctor")} to verify`);
|
|
1306
|
+
for (const line of closing.next) console.log(` ${line}`);
|
|
1030
1307
|
console.log();
|
|
1308
|
+
const exitCode = closing.exitCode;
|
|
1309
|
+
emitJson(options, {
|
|
1310
|
+
ok: closing.ok,
|
|
1311
|
+
configured: completed,
|
|
1312
|
+
skipped,
|
|
1313
|
+
manualSteps,
|
|
1314
|
+
failed,
|
|
1315
|
+
keyCheck: keyCheck?.kind ?? "skipped",
|
|
1316
|
+
reach: phone === null ? "skipped" : phone.reachesYou ? "yours" : phone.outcome?.kind ?? phone.detail ?? "unreachable",
|
|
1317
|
+
rc: rc.kind,
|
|
1318
|
+
rcPath: rc.path,
|
|
1319
|
+
exitCode
|
|
1320
|
+
});
|
|
1321
|
+
if (exitCode !== EXIT.OK) {
|
|
1322
|
+
if (configuredNothing && failed.length === 0) {
|
|
1323
|
+
console.log(` ${yellow("!")} Nothing was configured: none of the agents you asked for are installed here.`);
|
|
1324
|
+
console.log(` ${dim("Your key is saved. Install one and re-run, or pass --agents none to accept this.")}`);
|
|
1325
|
+
console.log();
|
|
1326
|
+
}
|
|
1327
|
+
process.exit(exitCode);
|
|
1328
|
+
}
|
|
1031
1329
|
};
|
|
1032
1330
|
main().catch((err) => {
|
|
1331
|
+
if (err instanceof UsageError) {
|
|
1332
|
+
console.log();
|
|
1333
|
+
console.log(` ${yellow("!")} ${err.message}`);
|
|
1334
|
+
console.log(` ${dim("Run")} ${cyan("pushary setup --help")} ${dim("for the full list.")}`);
|
|
1335
|
+
console.log();
|
|
1336
|
+
process.exit(EXIT.USAGE);
|
|
1337
|
+
}
|
|
1338
|
+
if (isCancellation(err)) {
|
|
1339
|
+
console.log();
|
|
1340
|
+
console.log(` ${dim("Cancelled. Nothing further was changed.")}`);
|
|
1341
|
+
console.log();
|
|
1342
|
+
process.exit(130);
|
|
1343
|
+
}
|
|
1033
1344
|
console.log();
|
|
1034
|
-
console.log(` ${
|
|
1035
|
-
console.log(` ${
|
|
1345
|
+
console.log(` ${yellow("!")} Setup failed: ${formatError(err)}`);
|
|
1346
|
+
console.log(` ${dim("Run")} ${cyan("npx @pushary/agent-hooks doctor")} ${dim("after fixing the issue, then rerun setup.")}`);
|
|
1036
1347
|
console.log();
|
|
1037
1348
|
process.exit(1);
|
|
1038
1349
|
});
|