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